mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-06 23:33:13 +08:00
Compare commits
153 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0af7b75af5 | ||
|
|
36a42cb064 | ||
|
|
4db0acc0e7 | ||
|
|
8f4800aa47 | ||
|
|
4745a37549 | ||
|
|
8fc949898b | ||
|
|
d4758b0e91 | ||
|
|
a07e4b17be | ||
|
|
b9dd975bc6 | ||
|
|
9964d3e24b | ||
|
|
58e8c8e182 | ||
|
|
c7339f6cfa | ||
|
|
1aa78f766a | ||
|
|
bf56e6adaf | ||
|
|
e915b7e499 | ||
|
|
de0470d200 | ||
|
|
3d7cf6992e | ||
|
|
06db082e3f | ||
|
|
4a383cecaf | ||
|
|
9a0f9207be | ||
|
|
0e3a0a1eec | ||
|
|
fd6d180eba | ||
|
|
d152e2ef9a | ||
|
|
b530cf4461 | ||
|
|
bbe85cde31 | ||
|
|
2c3cbe7146 | ||
|
|
2358308f4d | ||
|
|
58bff4fa73 | ||
|
|
098f7afc70 | ||
|
|
4070b1e23d | ||
|
|
d9d4c8a70c | ||
|
|
05abe49d8b | ||
|
|
a71245b883 | ||
|
|
057f88a36a | ||
|
|
32fcdd9d94 | ||
|
|
313927827d | ||
|
|
358b2b661e | ||
|
|
3eddacdff8 | ||
|
|
95d4cde649 | ||
|
|
d1d947bf12 | ||
|
|
40815e5b38 | ||
|
|
8fc1d23e03 | ||
|
|
5ec8c229a1 | ||
|
|
2412ec2195 | ||
|
|
bfb96b0ae8 | ||
|
|
f64920e510 | ||
|
|
664dc333ac | ||
|
|
521f6f2b18 | ||
|
|
6986d03c53 | ||
|
|
e6e38e3ca2 | ||
|
|
951d08f914 | ||
|
|
8e1e8ba7de | ||
|
|
877b098b09 | ||
|
|
e046eddda9 | ||
|
|
084b1169d7 | ||
|
|
f7ceeff05a | ||
|
|
e0d41a2b8a | ||
|
|
6b9f9f0f44 | ||
|
|
025135b8c6 | ||
|
|
77d810b735 | ||
|
|
e1222de05b | ||
|
|
459f807e67 | ||
|
|
32df1370a6 | ||
|
|
f18f8c89ec | ||
|
|
787b791651 | ||
|
|
2eca0da852 | ||
|
|
9e49604ce2 | ||
|
|
5f85c61d6a | ||
|
|
cd58855e1f | ||
|
|
13c64b0db0 | ||
|
|
55e021ba20 | ||
|
|
26fd61a3ed | ||
|
|
46482522bb | ||
|
|
98e3e22896 | ||
|
|
15d49e97c0 | ||
|
|
d5e7ce38ac | ||
|
|
162d0560db | ||
|
|
1de05047ca | ||
|
|
2af5de1199 | ||
|
|
e66a724d2b | ||
|
|
9f4c2ac8d7 | ||
|
|
44f0011445 | ||
|
|
545094cddf | ||
|
|
99f45d8853 | ||
|
|
c25b5f9938 | ||
|
|
db1304c11a | ||
|
|
57714b373c | ||
|
|
5be7f839f3 | ||
|
|
aa441eb58b | ||
|
|
e6b6a40fa6 | ||
|
|
f6dde6f4c1 | ||
|
|
36ab475578 | ||
|
|
a74fe62da6 | ||
|
|
1e4de7fec4 | ||
|
|
47117d1fb7 | ||
|
|
cb8ef408a4 | ||
|
|
e5f21a88fa | ||
|
|
0458c4f798 | ||
|
|
75f6160432 | ||
|
|
5337eb48e7 | ||
|
|
86c30ee731 | ||
|
|
d68dc38959 | ||
|
|
0525639329 | ||
|
|
0d9c7282df | ||
|
|
3b95925217 | ||
|
|
fa595f7aa3 | ||
|
|
ff80f32f72 | ||
|
|
e55dbef2fc | ||
|
|
ebf2e459e8 | ||
|
|
1b5081ae21 | ||
|
|
d5736756f7 | ||
|
|
757cf2e193 | ||
|
|
3a75ac0486 | ||
|
|
3c3ef6fa05 | ||
|
|
3282bfd03b | ||
|
|
0813df6b29 | ||
|
|
df74a04085 | ||
|
|
8323b87076 | ||
|
|
48f01921e1 | ||
|
|
ae9d99257e | ||
|
|
0183c8a4b7 | ||
|
|
9a2ca35e6e | ||
|
|
2edb48e314 | ||
|
|
a81fd497d4 | ||
|
|
49cca5cd69 | ||
|
|
f92cff6241 | ||
|
|
1b4343ffc2 | ||
|
|
d5392a5f59 | ||
|
|
a65ce649ac | ||
|
|
d5dc6cea99 | ||
|
|
5ecfe9f0f0 | ||
|
|
0f5fb066d1 | ||
|
|
8e61639598 | ||
|
|
e88468d867 | ||
|
|
bc46e2f509 | ||
|
|
2241279bb6 | ||
|
|
25ffbed756 | ||
|
|
8784843a7a | ||
|
|
a964e2b3c9 | ||
|
|
7f7efd45ab | ||
|
|
af8f5afef8 | ||
|
|
dcfaed437c | ||
|
|
47e2322e33 | ||
|
|
00e7d57245 | ||
|
|
d53a3222d0 | ||
|
|
80fe3a73e2 | ||
|
|
7ab993b764 | ||
|
|
622edd2ed1 | ||
|
|
1f5faee356 | ||
|
|
076b0cf867 | ||
|
|
d4168bebc6 | ||
|
|
13c3629cd6 | ||
|
|
1eff9801e8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,5 +5,6 @@ composer.phar
|
|||||||
vendor/
|
vendor/
|
||||||
.env
|
.env
|
||||||
data/database.sqlite
|
data/database.sqlite
|
||||||
|
data/GeoLite2-City.mmdb
|
||||||
docs/swagger-ui
|
docs/swagger-ui
|
||||||
docker-compose.override.yml
|
docker-compose.override.yml
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
tools:
|
tools:
|
||||||
external_code_coverage: true
|
external_code_coverage: true
|
||||||
checks:
|
checks:
|
||||||
php:
|
php:
|
||||||
code_rating: true
|
code_rating: true
|
||||||
duplication: true
|
duplication: true
|
||||||
|
build:
|
||||||
|
nodes:
|
||||||
|
analysis:
|
||||||
|
tests:
|
||||||
|
override:
|
||||||
|
- php-scrutinizer-run
|
||||||
|
|||||||
34
.travis.yml
34
.travis.yml
@@ -1,5 +1,7 @@
|
|||||||
language: php
|
language: php
|
||||||
|
|
||||||
|
sudo: false # Use containerized environment
|
||||||
|
|
||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- /.*/
|
- /.*/
|
||||||
@@ -7,12 +9,18 @@ branches:
|
|||||||
php:
|
php:
|
||||||
- 7.1
|
- 7.1
|
||||||
- 7.2
|
- 7.2
|
||||||
|
- 7.3
|
||||||
|
|
||||||
|
matrix:
|
||||||
|
allow_failures:
|
||||||
|
- php: 7.3
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- phpenv config-add data/infra/travis-php/memcached.ini
|
- echo 'extension = memcached.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||||
- phpenv config-add data/infra/travis-php/apcu.ini
|
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||||
|
- phpenv config-rm xdebug.ini || return 0
|
||||||
|
|
||||||
before_script:
|
install:
|
||||||
- composer self-update
|
- composer self-update
|
||||||
- composer install --no-interaction
|
- composer install --no-interaction
|
||||||
|
|
||||||
@@ -20,9 +28,23 @@ script:
|
|||||||
- mkdir build
|
- mkdir build
|
||||||
- composer check
|
- composer check
|
||||||
|
|
||||||
after_script:
|
after_success:
|
||||||
- vendor/bin/phpcov merge build --clover build/clover.xml
|
- rm -f build/clover.xml
|
||||||
|
- phpdbg -qrr vendor/bin/phpcov merge build --clover build/clover.xml
|
||||||
- wget https://scrutinizer-ci.com/ocular.phar
|
- wget https://scrutinizer-ci.com/ocular.phar
|
||||||
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
|
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
|
||||||
|
|
||||||
sudo: false
|
# Before deploying, build dist file for current travis tag
|
||||||
|
before_deploy:
|
||||||
|
- rm -f ocular.phar
|
||||||
|
- ./build.sh ${TRAVIS_TAG#?}
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: releases
|
||||||
|
api_key:
|
||||||
|
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
|
||||||
|
file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip"
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
php: 7.1
|
||||||
|
|||||||
136
CHANGELOG.md
136
CHANGELOG.md
@@ -1,5 +1,141 @@
|
|||||||
# CHANGELOG
|
# CHANGELOG
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## 1.14.0 - 2018-11-16
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
* [#236](https://github.com/shlinkio/shlink/issues/236) Added option to define a redirection to a custom URL when a user hits an invalid short URL.
|
||||||
|
|
||||||
|
It only affects URLs matched as "short URL" where the short code is invalid, not any 404 that happens in the app. For example, a request to the path `/foo/bar` will keep returning a 404.
|
||||||
|
|
||||||
|
This new option will be asked by the installer both for new shlink installations and for any previous shlink version which is updated.
|
||||||
|
|
||||||
|
* [#189](https://github.com/shlinkio/shlink/issues/189) and [#240](https://github.com/shlinkio/shlink/issues/240) Added new [GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/)-based geolocation service which is faster and more reliable than previous one.
|
||||||
|
|
||||||
|
It does not have API limit problems, since it uses a local database file.
|
||||||
|
|
||||||
|
Previous service is still used as a fallback in case GeoLite DB does not contain any IP address.
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
* [#241](https://github.com/shlinkio/shlink/issues/241) Fixed columns in `visit_locations` table, to be snake_case instead of camelCase.
|
||||||
|
* [#228](https://github.com/shlinkio/shlink/issues/228) Updated how exceptions are serialized into logs, by using monlog's `PsrLogMessageProcessor`.
|
||||||
|
* [#225](https://github.com/shlinkio/shlink/issues/225) Performance and maintainability slightly improved by enforcing via code sniffer that all global namespace classes, functions and constants are explicitly imported.
|
||||||
|
* [#196](https://github.com/shlinkio/shlink/issues/196) Reduced anemic model in entities, defining more expressive public APIs instead.
|
||||||
|
* [#249](https://github.com/shlinkio/shlink/issues/249) Added [functional-php](https://github.com/lstrojny/functional-php) to ease collections handling.
|
||||||
|
* [#253](https://github.com/shlinkio/shlink/issues/253) Increased `user_agent` column length in `visits` table to 512.
|
||||||
|
* [#256](https://github.com/shlinkio/shlink/issues/256) Updated to Infection v0.11.
|
||||||
|
* [#202](https://github.com/shlinkio/shlink/issues/202) Added missing response examples to OpenAPI docs.
|
||||||
|
|
||||||
|
#### Deprecated
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Removed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
* [#223](https://github.com/shlinkio/shlink/issues/223) Fixed PHPStan errors produced with symfony/console 4.1.5
|
||||||
|
|
||||||
|
|
||||||
|
## 1.13.2 - 2018-10-18
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
* [#233](https://github.com/shlinkio/shlink/issues/233) Added PHP 7.3 to build matrix allowing its failure.
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
* [#235](https://github.com/shlinkio/shlink/issues/235) Improved update instructions (thanks to [tivyhosting](https://github.com/tivyhosting)).
|
||||||
|
|
||||||
|
#### Deprecated
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Removed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
* [#237](https://github.com/shlinkio/shlink/issues/233) Solved errors when trying to geo-locate `null` IP addresses.
|
||||||
|
|
||||||
|
Also improved how visitor IP addresses are discovered, thanks to [akrabat/ip-address-middleware](https://github.com/akrabat/ip-address-middleware) package.
|
||||||
|
|
||||||
|
|
||||||
|
## 1.13.1 - 2018-10-16
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Deprecated
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Removed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
* [#231](https://github.com/shlinkio/shlink/issues/197) Fixed error when processing visits.
|
||||||
|
|
||||||
|
|
||||||
|
## 1.13.0 - 2018-10-06
|
||||||
|
|
||||||
|
#### Added
|
||||||
|
|
||||||
|
* [#197](https://github.com/shlinkio/shlink/issues/197) Added [cakephp/chronos](https://book.cakephp.org/3.0/en/chronos.html) library for date manipulations.
|
||||||
|
* [#214](https://github.com/shlinkio/shlink/issues/214) Improved build script, which allows builds to be done without "jumping" outside the project directory, and generates smaller dist files.
|
||||||
|
|
||||||
|
It also allows automating the dist file generation in travis-ci builds.
|
||||||
|
|
||||||
|
* [#207](https://github.com/shlinkio/shlink/issues/207) Added two new config options which are asked during installation process. The config options already existed in previous shlink version, but you had to manually set their values.
|
||||||
|
|
||||||
|
These are the new options:
|
||||||
|
|
||||||
|
* Visits threshold to allow short URLs to be deleted.
|
||||||
|
* Check the visits threshold when trying to delete a short URL via REST API.
|
||||||
|
|
||||||
|
#### Changed
|
||||||
|
|
||||||
|
* [#211](https://github.com/shlinkio/shlink/issues/211) Extracted installer to its own module, which will simplify moving it to a separated package in the future.
|
||||||
|
* [#200](https://github.com/shlinkio/shlink/issues/200) and [#201](https://github.com/shlinkio/shlink/issues/201) Renamed REST Action classes and CLI Command classes to use the concept of `ShortUrl` instead of the concept of `ShortCode` when referring to the entity, and left the `short code` concept to the identifier which is used as a unique code for a specific `Short URL`.
|
||||||
|
* [#181](https://github.com/shlinkio/shlink/issues/181) When importing the configuration from a previous shlink installation, it no longer asks to import every block. Instead, it is capable of detecting only new config options introduced in the new version, and ask only for those.
|
||||||
|
|
||||||
|
If no new options are found and you have selected to import config, no further questions will be asked and shlink will just import the old config.
|
||||||
|
|
||||||
|
#### Deprecated
|
||||||
|
|
||||||
|
* [#205](https://github.com/shlinkio/shlink/issues/205) Deprecated `[POST /authenticate]` endpoint, and allowed any API request to be automatically authenticated using the `X-Api-Key` header with a valid API key.
|
||||||
|
|
||||||
|
This effectively deprecates the `Authorization: Bearer <JWT>` authentication form, but it will keep working.
|
||||||
|
|
||||||
|
* As of [#200](https://github.com/shlinkio/shlink/issues/200) and [#201](https://github.com/shlinkio/shlink/issues/201) REST urls have changed from `/short-codes/...` to `/short-urls/...`, and the command namespaces have changed from `short-code:...` to `short-url:...`.
|
||||||
|
|
||||||
|
In both cases, backwards compatibility has been retained and the old ones are aliases for the new ones, but the old ones are considered deprecated.
|
||||||
|
|
||||||
|
#### Removed
|
||||||
|
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
#### Fixed
|
||||||
|
|
||||||
|
* [#203](https://github.com/shlinkio/shlink/issues/203) Fixed some warnings thrown while unzipping distributable files.
|
||||||
|
* [#206](https://github.com/shlinkio/shlink/issues/206) An error is now thrown during installation if any required param is left empty, making the installer display a message and ask again until a value is set.
|
||||||
|
|
||||||
|
|
||||||
## 1.12.0 - 2018-09-15
|
## 1.12.0 - 2018-09-15
|
||||||
|
|
||||||
#### Added
|
#### Added
|
||||||
|
|||||||
69
README.md
69
README.md
@@ -37,13 +37,13 @@ Then, you will need a built version of the project. There are a few ways to get
|
|||||||
|
|
||||||
After that, you will have a `shlink_x.x.x_dist.zip` dist file inside the `build` directory.
|
After that, you will have a `shlink_x.x.x_dist.zip` dist file inside the `build` directory.
|
||||||
|
|
||||||
This is the process used when releasing new shlink versions. After tagging the new version with git, the Github release is created, attaching generated dist file to it.
|
This is the process used when releasing new shlink versions. After tagging the new version with git, the Github release is automatically created by [travis](https://travis-ci.org/shlinkio/shlink), attaching generated dist file to it.
|
||||||
|
|
||||||
Despite how you built the project, you are going to need to install it now, by following these steps:
|
Despite how you built the project, you are going to need to install it now, by following these steps:
|
||||||
|
|
||||||
* If you are going to use MySQL or PostgreSQL, create an empty database with the name of your choice.
|
* If you are going to use MySQL or PostgreSQL, 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.
|
* 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 will guide you through the installation process.
|
* 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.**
|
||||||
* Configure the web server of your choice to serve shlink using your short domain.
|
* Configure the web server of your choice to serve shlink using your short domain.
|
||||||
|
|
||||||
For example, assuming your domain is doma.in and shlink is in the `/path/to/shlink` folder, this would be the basic configuration for Nginx and Apache.
|
For example, assuming your domain is doma.in and shlink is in the `/path/to/shlink` folder, this would be the basic configuration for Nginx and Apache.
|
||||||
@@ -104,21 +104,27 @@ Those tasks can be performed using shlink's CLI, so it should be easy to schedul
|
|||||||
|
|
||||||
If you don't run this command regularly, the stats will say all visits come from *unknown* locations.
|
If you don't run this command regularly, the stats will say all visits come from *unknown* locations.
|
||||||
|
|
||||||
* Generate website previews: `/path/to/shlink/bin/cli shortcode:process-previews`
|
* 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.
|
||||||
|
|
||||||
|
* 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.
|
Running this will improve the performance of the `doma.in/abc123/preview` URLs, which return a preview of the site.
|
||||||
|
|
||||||
## Update to new version
|
## Update to new version
|
||||||
|
|
||||||
When a new Shlink version is available, you don't need to repeat the whole process yourself.
|
When a new Shlink version is available, you don't need to repeat the entire process yourself. Instead, follow these steps:
|
||||||
|
|
||||||
Instead, get the latest version as explained in previous step, and then, run the script `bin/update`.
|
1. Rename your existing Shlink directory to something else (ie. `shlink` ---> `shlink-old`)
|
||||||
|
2. Download and extract the new version of Shlink, and set the directories name to that of the old version. (ie. `shlink`)
|
||||||
|
3. Run the `bin/update` script in the new version's directory to migrate your configuration over.
|
||||||
|
|
||||||
The script will ask you for the location from previous shlink version, and use it in order to import the configuration.
|
The script will ask you for the location from previous shlink version, and use it in order to import the configuration. It will then update the database and generate some the assets neccessary for Shlink to function.
|
||||||
|
|
||||||
It will then update the database and generate some assets.
|
Right now, it does not import cached info (like website previews), but it will. For now you will need to regenerate them again.
|
||||||
|
|
||||||
Right now, it does not import cached info (like website previews), but it will. By now you will need to regenerate them again.
|
|
||||||
|
|
||||||
**Important!** It is recommended that you don't skip any version when using this process. The update gets better on every version, but older versions might make assumptions.
|
**Important!** It is recommended that you don't skip any version when using this process. The update gets better on every version, but older versions might make assumptions.
|
||||||
|
|
||||||
@@ -134,7 +140,7 @@ Currently the image does not expose an entry point which let's you interact with
|
|||||||
|
|
||||||
Once shlink is installed, there are two main ways to interact with it:
|
Once shlink is installed, there are two main ways to interact with it:
|
||||||
|
|
||||||
* **The command line**. Try running `bin/cli` and see all the available commands.
|
* **The command line**. Try running `bin/cli` and see all the [available commands](#shlink-cli-help).
|
||||||
|
|
||||||
All of those commands can be run with the `--help`/`-h` flag in order to see how to use them and all the available options.
|
All of those commands can be run with the `--help`/`-h` flag in order to see how to use them and all the available options.
|
||||||
|
|
||||||
@@ -145,3 +151,46 @@ Once shlink is installed, there are two main ways to interact with it:
|
|||||||
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.
|
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.
|
||||||
|
|
||||||
Both the API and CLI allow you to do the same operations, except for API key management, which can be done from the command line interface only.
|
Both the API and CLI allow you to do the same operations, except for API key management, which can be done from the command line interface only.
|
||||||
|
|
||||||
|
### Shlink CLI Help
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage:
|
||||||
|
command [options] [arguments]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --help Display this help message
|
||||||
|
-q, --quiet Do not output any message
|
||||||
|
-V, --version Display this application version
|
||||||
|
--ansi Force ANSI output
|
||||||
|
--no-ansi Disable ANSI output
|
||||||
|
-n, --no-interaction Do not ask any interactive question
|
||||||
|
-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
|
||||||
|
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 Generates a character set sample just by shuffling the default one, "123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ". Then it can be set in the SHORTCODE_CHARS environment variable
|
||||||
|
config:generate-secret Generates a random secret string that can be used for JWT token encryption
|
||||||
|
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] 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
|
||||||
|
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:process Processes visits where location is not set yet
|
||||||
|
```
|
||||||
|
|
||||||
|
> This product includes GeoLite2 data created by MaxMind, available from [https://www.maxmind.com](https://www.maxmind.com)
|
||||||
|
|||||||
29
bin/install
29
bin/install
@@ -1,29 +1,12 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Installer;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
|
|
||||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizer;
|
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
use Symfony\Component\Filesystem\Filesystem;
|
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
|
||||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
|
||||||
use Zend\ServiceManager\ServiceManager;
|
|
||||||
|
|
||||||
chdir(dirname(__DIR__));
|
/** @var ServiceLocatorInterface $container */
|
||||||
|
$container = include __DIR__ . '/../config/install-container.php';
|
||||||
require __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
$container = new ServiceManager([
|
|
||||||
'factories' => [
|
|
||||||
Application::class => InstallApplicationFactory::class,
|
|
||||||
Filesystem::class => InvokableFactory::class,
|
|
||||||
],
|
|
||||||
'services' => [
|
|
||||||
'config' => [
|
|
||||||
ConfigAbstractFactory::class => [
|
|
||||||
DatabaseConfigCustomizer::class => [Filesystem::class]
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
$container->build(Application::class)->run();
|
$container->build(Application::class)->run();
|
||||||
|
|||||||
29
bin/update
29
bin/update
@@ -1,29 +1,12 @@
|
|||||||
#!/usr/bin/env php
|
#!/usr/bin/env php
|
||||||
<?php
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Installer;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
|
|
||||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizer;
|
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
use Symfony\Component\Filesystem\Filesystem;
|
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
|
||||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
|
||||||
use Zend\ServiceManager\ServiceManager;
|
|
||||||
|
|
||||||
chdir(dirname(__DIR__));
|
/** @var ServiceLocatorInterface $container */
|
||||||
|
$container = include __DIR__ . '/../config/install-container.php';
|
||||||
require __DIR__ . '/../vendor/autoload.php';
|
|
||||||
|
|
||||||
$container = new ServiceManager([
|
|
||||||
'factories' => [
|
|
||||||
Application::class => InstallApplicationFactory::class,
|
|
||||||
Filesystem::class => InvokableFactory::class,
|
|
||||||
],
|
|
||||||
'services' => [
|
|
||||||
'config' => [
|
|
||||||
ConfigAbstractFactory::class => [
|
|
||||||
DatabaseConfigCustomizer::class => [Filesystem::class]
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
$container->build(Application::class, ['isUpdate' => true])->run();
|
$container->build(Application::class, ['isUpdate' => true])->run();
|
||||||
|
|||||||
54
build.sh
54
build.sh
@@ -8,53 +8,53 @@ if [ "$#" -ne 1 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
version=$1
|
version=$1
|
||||||
builtcontent=$(readlink -f "../shlink_${version}_dist")
|
builtcontent="./build/shlink_${version}_dist"
|
||||||
projectdir=$(pwd)
|
projectdir=$(pwd)
|
||||||
[ -f ./composer.phar ] && composerBin='./composer.phar' || composerBin='composer'
|
[ -f ./composer.phar ] && composerBin='./composer.phar' || composerBin='composer'
|
||||||
|
|
||||||
# Copy project content to temp dir
|
# Copy project content to temp dir
|
||||||
echo 'Copying project files...'
|
echo 'Copying project files...'
|
||||||
rm -rf "${builtcontent}"
|
rm -rf "${builtcontent}"
|
||||||
mkdir "${builtcontent}"
|
mkdir -p "${builtcontent}"
|
||||||
sudo chmod -R 777 "${projectdir}"/data/infra/{database,nginx}
|
rsync -av * "${builtcontent}" \
|
||||||
cp -R "${projectdir}"/* "${builtcontent}"
|
--exclude=data/infra \
|
||||||
|
--exclude=data/migrations_template.txt \
|
||||||
|
--exclude=data/GeoLite2-City.mmdb \
|
||||||
|
--exclude=**/.gitignore \
|
||||||
|
--exclude=CHANGELOG.md \
|
||||||
|
--exclude=composer.lock \
|
||||||
|
--exclude=vendor \
|
||||||
|
--exclude=docs \
|
||||||
|
--exclude=indocker \
|
||||||
|
--exclude=docker* \
|
||||||
|
--exclude=func_tests_bootstrap.php \
|
||||||
|
--exclude=php* \
|
||||||
|
--exclude=infection.json \
|
||||||
|
--exclude=phpstan.neon \
|
||||||
|
--exclude=config/autoload/*local* \
|
||||||
|
--exclude=**/test* \
|
||||||
|
--exclude=build*
|
||||||
cd "${builtcontent}"
|
cd "${builtcontent}"
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
echo "Installing dependencies with $composerBin..."
|
echo "Installing dependencies with $composerBin..."
|
||||||
rm -rf vendor
|
${composerBin} self-update
|
||||||
rm -f composer.lock
|
${composerBin} install --no-dev --optimize-autoloader --no-progress --no-interaction
|
||||||
$composerBin self-update
|
|
||||||
$composerBin install --no-dev --optimize-autoloader --no-progress --no-interaction
|
|
||||||
|
|
||||||
# Delete development files
|
# Delete development files
|
||||||
echo 'Deleting dev files...'
|
echo 'Deleting dev files...'
|
||||||
rm build.sh
|
|
||||||
rm CHANGELOG.md
|
|
||||||
rm composer.*
|
rm composer.*
|
||||||
rm LICENSE
|
rm -f data/database.sqlite
|
||||||
rm indocker
|
|
||||||
rm docker-compose.yml
|
|
||||||
rm docker-compose.override.yml
|
|
||||||
rm docker-compose.override.yml.dist
|
|
||||||
rm func_tests_bootstrap.php
|
|
||||||
rm php*
|
|
||||||
rm README.md
|
|
||||||
rm infection.json
|
|
||||||
rm -rf build
|
|
||||||
rm -ff data/database.sqlite
|
|
||||||
rm -rf data/infra
|
|
||||||
rm -rf data/{cache,log,proxies}/{*,.gitignore}
|
|
||||||
rm -rf config/params/{*,.gitignore}
|
|
||||||
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}
|
|
||||||
|
|
||||||
# Update shlink version in config
|
# Update shlink version in config
|
||||||
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
||||||
|
|
||||||
# Compressing file
|
# Compressing file
|
||||||
echo 'Compressing files...'
|
echo 'Compressing files...'
|
||||||
rm -f "${projectdir}"/build/shlink_${version}_dist.zip
|
cd "${projectdir}"/build
|
||||||
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip "../shlink_${version}_dist"
|
rm -f ./shlink_${version}_dist.zip
|
||||||
|
zip -ry ./shlink_${version}_dist.zip ./shlink_${version}_dist
|
||||||
|
cd "${projectdir}"
|
||||||
rm -rf "${builtcontent}"
|
rm -rf "${builtcontent}"
|
||||||
|
|
||||||
echo 'Done!'
|
echo 'Done!'
|
||||||
|
|||||||
@@ -16,23 +16,27 @@
|
|||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"acelaya/ze-content-based-error-handler": "^2.2",
|
"acelaya/ze-content-based-error-handler": "^2.2",
|
||||||
|
"akrabat/ip-address-middleware": "^1.0",
|
||||||
|
"cakephp/chronos": "^1.2",
|
||||||
"cocur/slugify": "^3.0",
|
"cocur/slugify": "^3.0",
|
||||||
"doctrine/cache": "^1.6",
|
"doctrine/cache": "^1.6",
|
||||||
"doctrine/migrations": "^1.4",
|
"doctrine/migrations": "^1.4",
|
||||||
"doctrine/orm": "^2.5",
|
"doctrine/orm": "^2.5",
|
||||||
"endroid/qr-code": "^1.7",
|
"endroid/qr-code": "^1.7",
|
||||||
"firebase/php-jwt": "^4.0",
|
"firebase/php-jwt": "^4.0",
|
||||||
|
"geoip2/geoip2": "^2.9",
|
||||||
"guzzlehttp/guzzle": "^6.2",
|
"guzzlehttp/guzzle": "^6.2",
|
||||||
|
"lstrojny/functional-php": "^1.8",
|
||||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||||
"monolog/monolog": "^1.21",
|
"monolog/monolog": "^1.21",
|
||||||
"roave/security-advisories": "dev-master",
|
"roave/security-advisories": "dev-master",
|
||||||
"symfony/console": "^4.0",
|
"symfony/console": "^4.1",
|
||||||
"symfony/filesystem": "^4.0",
|
"symfony/filesystem": "^4.1",
|
||||||
"symfony/process": "^4.0",
|
"symfony/process": "^4.1",
|
||||||
"theorchard/monolog-cascade": "^0.4",
|
"theorchard/monolog-cascade": "^0.4",
|
||||||
"zendframework/zend-config": "^3.0",
|
"zendframework/zend-config": "^3.0",
|
||||||
"zendframework/zend-config-aggregator": "^1.0",
|
"zendframework/zend-config-aggregator": "^1.0",
|
||||||
"zendframework/zend-diactoros": "^1.7",
|
"zendframework/zend-diactoros": "^2.0",
|
||||||
"zendframework/zend-expressive": "^3.0",
|
"zendframework/zend-expressive": "^3.0",
|
||||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||||
"zendframework/zend-expressive-helpers": "^5.0",
|
"zendframework/zend-expressive-helpers": "^5.0",
|
||||||
@@ -44,11 +48,12 @@
|
|||||||
"zendframework/zend-stdlib": "^3.0"
|
"zendframework/zend-stdlib": "^3.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
"devster/ubench": "^2.0",
|
||||||
"filp/whoops": "^2.0",
|
"filp/whoops": "^2.0",
|
||||||
"infection/infection": "^0.9.0",
|
"infection/infection": "^0.11.0",
|
||||||
"phpstan/phpstan": "^0.10.0",
|
"phpstan/phpstan": "^0.10.0",
|
||||||
"phpunit/phpcov": "^5.0",
|
"phpunit/phpcov": "^5.0",
|
||||||
"phpunit/phpunit": "^7.0",
|
"phpunit/phpunit": "^7.3",
|
||||||
"slevomat/coding-standard": "^4.0",
|
"slevomat/coding-standard": "^4.0",
|
||||||
"squizlabs/php_codesniffer": "^3.2.3",
|
"squizlabs/php_codesniffer": "^3.2.3",
|
||||||
"symfony/dotenv": "^4.0",
|
"symfony/dotenv": "^4.0",
|
||||||
@@ -61,7 +66,8 @@
|
|||||||
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
|
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
|
||||||
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
|
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
|
||||||
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
|
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
|
||||||
"Shlinkio\\Shlink\\Common\\": "module/Common/src"
|
"Shlinkio\\Shlink\\Common\\": "module/Common/src",
|
||||||
|
"Shlinkio\\Shlink\\Installer\\": "module/Installer/src"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"module/Common/functions/functions.php"
|
"module/Common/functions/functions.php"
|
||||||
@@ -78,38 +84,66 @@
|
|||||||
"ShlinkioTest\\Shlink\\Common\\": [
|
"ShlinkioTest\\Shlink\\Common\\": [
|
||||||
"module/Common/test",
|
"module/Common/test",
|
||||||
"module/Common/test-func"
|
"module/Common/test-func"
|
||||||
]
|
],
|
||||||
|
"ShlinkioTest\\Shlink\\Installer\\": "module/Installer/test"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"check": [
|
"check": [
|
||||||
"@cs",
|
"@cs",
|
||||||
"@stan",
|
"@stan",
|
||||||
"@test",
|
"@test:ci",
|
||||||
"@func-test",
|
"@infect:ci"
|
||||||
"@infect"
|
|
||||||
],
|
],
|
||||||
|
"ci": [
|
||||||
|
"echo \"This command is DEPRECATED. Use check instead\"",
|
||||||
|
"@check"
|
||||||
|
],
|
||||||
|
|
||||||
"cs": "phpcs",
|
"cs": "phpcs",
|
||||||
"cs-fix": "phpcbf",
|
"cs:fix": "phpcbf",
|
||||||
"serve": "php -S 0.0.0.0:8000 -t public/",
|
"stan": "phpstan analyse module/*/src/ --level=5 -c phpstan.neon",
|
||||||
"test": "phpunit --coverage-php build/coverage-unit.cov",
|
|
||||||
"pretty-test": "phpunit --coverage-html build/coverage",
|
"test": [
|
||||||
"func-test": "phpunit -c phpunit-func.xml --coverage-php build/coverage-func.cov",
|
"@test:unit",
|
||||||
"complete-pretty-test": [
|
"@test:func"
|
||||||
|
],
|
||||||
|
"test:ci": [
|
||||||
|
"@test:unit:ci",
|
||||||
|
"@test:func"
|
||||||
|
],
|
||||||
|
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov",
|
||||||
|
"test:unit:ci": "phpdbg -qrr vendor/bin/phpunit --order-by=random --coverage-php build/coverage-unit.cov --coverage-clover=build/clover.xml --coverage-xml=build/coverage-xml --log-junit=build/phpunit.junit.xml",
|
||||||
|
"test:func": "phpdbg -qrr vendor/bin/phpunit --order-by=random -c phpunit-func.xml --coverage-php build/coverage-func.cov",
|
||||||
|
|
||||||
|
"test:pretty": [
|
||||||
"@test",
|
"@test",
|
||||||
"@func-test",
|
|
||||||
"phpcov merge build --html build/html"
|
"phpcov merge build --html build/html"
|
||||||
],
|
],
|
||||||
"stan": "phpstan analyse module/*/src/ --level=6 -c phpstan.neon",
|
"test:unit:pretty": "phpdbg -qrr vendor/bin/phpunit --coverage-html build/coverage --order-by=random",
|
||||||
"infect": "infection --threads=4 --min-msi=55 --only-covered --log-verbosity=2",
|
|
||||||
"infect-show": "infection --threads=4 --min-msi=55 --only-covered --log-verbosity=2 --show-mutations",
|
"infect": "infection --threads=4 --min-msi=60 --log-verbosity=2 --only-covered",
|
||||||
"expressive": "expressive"
|
"infect:ci": "infection --threads=4 --min-msi=60 --log-verbosity=2 --only-covered --coverage=build",
|
||||||
|
"infect:show": "infection --threads=4 --min-msi=60 --log-verbosity=2 --only-covered --show-mutations"
|
||||||
|
},
|
||||||
|
"scripts-descriptions": {
|
||||||
|
"check": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"test\" and \"infect\"</>",
|
||||||
|
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"test:ci\" and \"infect:ci\"</>",
|
||||||
|
"cs": "<fg=blue;options=bold>Checks coding styles</>",
|
||||||
|
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
|
||||||
|
"stan": "<fg=blue;options=bold>Inspects code with phpstan</>",
|
||||||
|
"test": "<fg=blue;options=bold>Runs all test suites</>",
|
||||||
|
"test:ci": "<fg=blue;options=bold>Runs all test suites, generating all needed reports and logs for CI envs</>",
|
||||||
|
"test:unit": "<fg=blue;options=bold>Runs unit test suites</>",
|
||||||
|
"test:unit:ci": "<fg=blue;options=bold>Runs unit test suites, generating all needed reports and logs for CI envs</>",
|
||||||
|
"test:func": "<fg=blue;options=bold>Runs functional test suites (covering entity repositories)</>",
|
||||||
|
"test:pretty": "<fg=blue;options=bold>Runs all test suites and generates an HTML code coverage report</>",
|
||||||
|
"test:unit:pretty": "<fg=blue;options=bold>Runs unit test suites and generates an HTML code coverage report</>",
|
||||||
|
"infect": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing</>",
|
||||||
|
"infect:ci": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing with existing reports and logs</>",
|
||||||
|
"infect:show": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing and shows applied mutators</>"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"process-timeout": 0,
|
"sort-packages": true
|
||||||
"sort-packages": true,
|
|
||||||
"platform": {
|
|
||||||
"php": "7.1.8"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ return [
|
|||||||
Container\ApplicationConfigInjectionDelegator::class,
|
Container\ApplicationConfigInjectionDelegator::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'lazy_services' => [
|
||||||
|
'proxies_target_dir' => 'data/proxies',
|
||||||
|
'proxies_namespace' => 'ShlinkProxy',
|
||||||
|
'write_proxy_files' => true,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
12
config/autoload/dependencies.local.php.dist
Normal file
12
config/autoload/dependencies.local.php.dist
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'dependencies' => [
|
||||||
|
'lazy_services' => [
|
||||||
|
'write_proxy_files' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
12
config/autoload/geolite2.global.php
Normal file
12
config/autoload/geolite2.global.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'geolite2' => [
|
||||||
|
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
|
||||||
|
'temp_dir' => sys_get_temp_dir(),
|
||||||
|
'download_from' => 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink;
|
||||||
|
|
||||||
use Monolog\Handler\RotatingFileHandler;
|
use Monolog\Handler\RotatingFileHandler;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
|
use Monolog\Processor;
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'logger' => [
|
'logger' => [
|
||||||
'formatters' => [
|
'formatters' => [
|
||||||
'dashed' => [
|
'dashed' => [
|
||||||
'format' => '[%datetime%] %channel%.%level_name% - %message% %context%' . PHP_EOL,
|
'format' => '[%datetime%] %channel%.%level_name% - %message%' . PHP_EOL,
|
||||||
'include_stacktraces' => true,
|
'include_stacktraces' => true,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -24,9 +28,19 @@ return [
|
|||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'processors' => [
|
||||||
|
'exception_with_new_line' => [
|
||||||
|
'class' => Common\Logger\Processor\ExceptionWithNewLineProcessor::class,
|
||||||
|
],
|
||||||
|
'psr3' => [
|
||||||
|
'class' => Processor\PsrLogMessageProcessor::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
'loggers' => [
|
'loggers' => [
|
||||||
'Shlink' => [
|
'Shlink' => [
|
||||||
'handlers' => ['rotating_file_handler'],
|
'handlers' => ['rotating_file_handler'],
|
||||||
|
'processors' => ['exception_with_new_line', 'psr3'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
namespace Shlinkio\Shlink;
|
||||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
|
||||||
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
|
|
||||||
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
|
|
||||||
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
|
|
||||||
use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware;
|
|
||||||
use Zend\Expressive;
|
use Zend\Expressive;
|
||||||
use Zend\Stratigility\Middleware\ErrorHandler;
|
use Zend\Stratigility\Middleware\ErrorHandler;
|
||||||
|
|
||||||
@@ -17,14 +13,15 @@ return [
|
|||||||
'middleware' => [
|
'middleware' => [
|
||||||
ErrorHandler::class,
|
ErrorHandler::class,
|
||||||
Expressive\Helper\ContentLengthMiddleware::class,
|
Expressive\Helper\ContentLengthMiddleware::class,
|
||||||
LocaleMiddleware::class,
|
Common\Middleware\LocaleMiddleware::class,
|
||||||
],
|
],
|
||||||
'priority' => 11,
|
'priority' => 12,
|
||||||
],
|
],
|
||||||
'pre-routing-rest' => [
|
'pre-routing-rest' => [
|
||||||
'path' => '/rest',
|
'path' => '/rest',
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
PathVersionMiddleware::class,
|
Rest\Middleware\PathVersionMiddleware::class,
|
||||||
|
Rest\Middleware\ShortUrl\ShortCodePathMiddleware::class,
|
||||||
],
|
],
|
||||||
'priority' => 11,
|
'priority' => 11,
|
||||||
],
|
],
|
||||||
@@ -39,10 +36,10 @@ return [
|
|||||||
'rest' => [
|
'rest' => [
|
||||||
'path' => '/rest',
|
'path' => '/rest',
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
CrossDomainMiddleware::class,
|
Rest\Middleware\CrossDomainMiddleware::class,
|
||||||
Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
|
Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||||
BodyParserMiddleware::class,
|
Rest\Middleware\BodyParserMiddleware::class,
|
||||||
CheckAuthenticationMiddleware::class,
|
Rest\Middleware\AuthenticationMiddleware::class,
|
||||||
],
|
],
|
||||||
'priority' => 5,
|
'priority' => 5,
|
||||||
],
|
],
|
||||||
@@ -50,7 +47,7 @@ return [
|
|||||||
'post-routing' => [
|
'post-routing' => [
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
Expressive\Router\Middleware\DispatchMiddleware::class,
|
Expressive\Router\Middleware\DispatchMiddleware::class,
|
||||||
NotFoundHandler::class,
|
Core\Response\NotFoundHandler::class,
|
||||||
],
|
],
|
||||||
'priority' => 1,
|
'priority' => 1,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ return [
|
|||||||
],
|
],
|
||||||
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
|
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
|
||||||
'validate_url' => true,
|
'validate_url' => true,
|
||||||
|
'not_found_short_url' => [
|
||||||
|
'enable_redirection' => false,
|
||||||
|
'redirect_to' => null,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink;
|
||||||
|
|
||||||
use Acelaya\ExpressiveErrorHandler;
|
use Acelaya\ExpressiveErrorHandler;
|
||||||
use Shlinkio\Shlink\CLI;
|
|
||||||
use Shlinkio\Shlink\Common;
|
|
||||||
use Shlinkio\Shlink\Core;
|
|
||||||
use Shlinkio\Shlink\Rest;
|
|
||||||
use Zend\ConfigAggregator;
|
use Zend\ConfigAggregator;
|
||||||
use Zend\Expressive;
|
use Zend\Expressive;
|
||||||
|
use function class_exists;
|
||||||
/**
|
|
||||||
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
|
|
||||||
* then ``local.php`` and finally ``*.local.php``. This way local settings overwrite global settings.
|
|
||||||
*
|
|
||||||
* The configuration can be cached. This can be done by setting ``config_cache_enabled`` to ``true``.
|
|
||||||
*
|
|
||||||
* Obviously, if you use closures in your config you can't cache it.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return (new ConfigAggregator\ConfigAggregator([
|
return (new ConfigAggregator\ConfigAggregator([
|
||||||
Expressive\ConfigProvider::class,
|
Expressive\ConfigProvider::class,
|
||||||
@@ -24,13 +14,14 @@ return (new ConfigAggregator\ConfigAggregator([
|
|||||||
Expressive\Router\FastRouteRouter\ConfigProvider::class,
|
Expressive\Router\FastRouteRouter\ConfigProvider::class,
|
||||||
Expressive\Plates\ConfigProvider::class,
|
Expressive\Plates\ConfigProvider::class,
|
||||||
Expressive\Helper\ConfigProvider::class,
|
Expressive\Helper\ConfigProvider::class,
|
||||||
\class_exists(Expressive\Swoole\ConfigProvider::class)
|
class_exists(Expressive\Swoole\ConfigProvider::class)
|
||||||
? Expressive\Swoole\ConfigProvider::class
|
? Expressive\Swoole\ConfigProvider::class
|
||||||
: new ConfigAggregator\ArrayProvider([]),
|
: new ConfigAggregator\ArrayProvider([]),
|
||||||
ExpressiveErrorHandler\ConfigProvider::class,
|
ExpressiveErrorHandler\ConfigProvider::class,
|
||||||
Common\ConfigProvider::class,
|
Common\ConfigProvider::class,
|
||||||
Core\ConfigProvider::class,
|
Core\ConfigProvider::class,
|
||||||
CLI\ConfigProvider::class,
|
CLI\ConfigProvider::class,
|
||||||
|
Installer\ConfigProvider::class,
|
||||||
Rest\ConfigProvider::class,
|
Rest\ConfigProvider::class,
|
||||||
new ConfigAggregator\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
|
new ConfigAggregator\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
|
||||||
], 'data/cache/app_config.php'))->getMergedConfig();
|
], 'data/cache/app_config.php'))->getMergedConfig();
|
||||||
|
|||||||
29
config/install-container.php
Normal file
29
config/install-container.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Installer\Config\Plugin\DatabaseConfigCustomizer;
|
||||||
|
use Shlinkio\Shlink\Installer\Factory\InstallApplicationFactory;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
|
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||||
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
$container = new ServiceManager([
|
||||||
|
'factories' => [
|
||||||
|
Application::class => InstallApplicationFactory::class,
|
||||||
|
Filesystem::class => InvokableFactory::class,
|
||||||
|
],
|
||||||
|
'services' => [
|
||||||
|
'config' => [
|
||||||
|
ConfigAbstractFactory::class => [
|
||||||
|
DatabaseConfigCustomizer::class => [Filesystem::class],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
return $container;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM php:7.1-fpm-alpine
|
FROM php:7.1.22-fpm-alpine
|
||||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
extension="apcu.so"
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
extension="memcached.so"
|
|
||||||
@@ -6,6 +6,7 @@ namespace ShlinkMigrations;
|
|||||||
use Doctrine\DBAL\DBALException;
|
use Doctrine\DBAL\DBALException;
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
use PDO;
|
||||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||||
|
|
||||||
@@ -38,7 +39,7 @@ final class Version20180913205455 extends AbstractMigration
|
|||||||
->set('v.remote_addr', ':obfuscatedAddr')
|
->set('v.remote_addr', ':obfuscatedAddr')
|
||||||
->where('v.id=:id');
|
->where('v.id=:id');
|
||||||
|
|
||||||
while ($row = $st->fetch(\PDO::FETCH_ASSOC)) {
|
while ($row = $st->fetch(PDO::FETCH_ASSOC)) {
|
||||||
$addr = $row['remote_addr'] ?? null;
|
$addr = $row['remote_addr'] ?? null;
|
||||||
if ($addr === null) {
|
if ($addr === null) {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
68
data/migrations/Version20181020060559.php
Normal file
68
data/migrations/Version20181020060559.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkMigrations;
|
||||||
|
|
||||||
|
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\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20181020060559 extends AbstractMigration
|
||||||
|
{
|
||||||
|
private const COLUMNS = [
|
||||||
|
'countryCode' => 'country_code',
|
||||||
|
'countryName' => 'country_name',
|
||||||
|
'regionName' => 'region_name',
|
||||||
|
'cityName' => 'city_name',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
* @throws SchemaException
|
||||||
|
*/
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->createColumns($schema->getTable('visit_locations'), self::COLUMNS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createColumns(Table $visitLocations, array $columnNames): void
|
||||||
|
{
|
||||||
|
foreach ($columnNames as $name) {
|
||||||
|
if (! $visitLocations->hasColumn($name)) {
|
||||||
|
$visitLocations->addColumn($name, Type::STRING, ['notnull' => false]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws SchemaException
|
||||||
|
* @throws DBALException
|
||||||
|
*/
|
||||||
|
public function postUp(Schema $schema): void
|
||||||
|
{
|
||||||
|
$visitLocations = $schema->getTable('visit_locations');
|
||||||
|
|
||||||
|
// If the camel case columns do not exist, do nothing
|
||||||
|
if (! $visitLocations->hasColumn('countryCode')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$qb = $this->connection->createQueryBuilder();
|
||||||
|
$qb->update('visit_locations');
|
||||||
|
foreach (self::COLUMNS as $camelCaseName => $snakeCaseName) {
|
||||||
|
$qb->set($snakeCaseName, $camelCaseName);
|
||||||
|
}
|
||||||
|
$qb->execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// No down
|
||||||
|
}
|
||||||
|
}
|
||||||
40
data/migrations/Version20181020065148.php
Normal file
40
data/migrations/Version20181020065148.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\DBAL\Schema\SchemaException;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20181020065148 extends AbstractMigration
|
||||||
|
{
|
||||||
|
private const CAMEL_CASE_COLUMNS = [
|
||||||
|
'countryCode',
|
||||||
|
'countryName',
|
||||||
|
'regionName',
|
||||||
|
'cityName',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws SchemaException
|
||||||
|
*/
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$visitLocations = $schema->getTable('visit_locations');
|
||||||
|
|
||||||
|
foreach (self::CAMEL_CASE_COLUMNS as $name) {
|
||||||
|
if ($visitLocations->hasColumn($name)) {
|
||||||
|
$visitLocations->dropColumn($name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// No down
|
||||||
|
}
|
||||||
|
}
|
||||||
36
data/migrations/Version20181110175521.php
Normal file
36
data/migrations/Version20181110175521.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Column;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\DBAL\Schema\SchemaException;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20181110175521 extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws SchemaException
|
||||||
|
*/
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->getUserAgentColumn($schema)->setLength(512);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws SchemaException
|
||||||
|
*/
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->getUserAgentColumn($schema)->setLength(256);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws SchemaException
|
||||||
|
*/
|
||||||
|
private function getUserAgentColumn(Schema $schema): Column
|
||||||
|
{
|
||||||
|
return $schema->getTable('visits')->getColumn('user_agent');
|
||||||
|
}
|
||||||
|
}
|
||||||
20
data/migrations_template.txt
Normal file
20
data/migrations_template.txt
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace <namespace>;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version<version> extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
<up>
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
<down>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
"post": {
|
"post": {
|
||||||
|
"deprecated": true,
|
||||||
|
"operationId": "authenticate",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Authentication"
|
"Authentication"
|
||||||
],
|
],
|
||||||
"summary": "Perform authentication",
|
"summary": "[Deprecated] Perform authentication",
|
||||||
"description": "Performs an authentication",
|
"description": "**This endpoint is deprecated, since the authentication can be performed via API key now**. Performs an authentication.",
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
"description": "Request body.",
|
"description": "Request body.",
|
||||||
"required": true,
|
"required": true,
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"get": {
|
"get": {
|
||||||
|
"operationId": "listShortUrls",
|
||||||
"tags": [
|
"tags": [
|
||||||
"ShortCodes"
|
"Short URLs"
|
||||||
],
|
],
|
||||||
"summary": "List short URLs",
|
"summary": "List short URLs",
|
||||||
"description": "Returns the list of short codes",
|
"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.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "page",
|
"name": "page",
|
||||||
@@ -53,6 +54,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
@@ -142,12 +146,16 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"post": {
|
"post": {
|
||||||
|
"operationId": "createShortUrl",
|
||||||
"tags": [
|
"tags": [
|
||||||
"ShortCodes"
|
"Short URLs"
|
||||||
],
|
],
|
||||||
"summary": "Create short URL",
|
"summary": "Create short URL",
|
||||||
"description": "Creates a new short code",
|
"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.",
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
@@ -201,23 +209,22 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "../definitions/ShortUrl.json"
|
||||||
"properties": {
|
|
||||||
"longUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The original long URL that has been parsed"
|
|
||||||
},
|
|
||||||
"shortUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The generated short URL"
|
|
||||||
},
|
|
||||||
"shortCode": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "the short code that is being used in the short URL"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"application/json": {
|
||||||
|
"shortCode": "12C18",
|
||||||
|
"shortUrl": "https://doma.in/12C18",
|
||||||
|
"longUrl": "https://store.steampowered.com",
|
||||||
|
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||||
|
"visitsCount": 0,
|
||||||
|
"tags": [
|
||||||
|
"games",
|
||||||
|
"tech"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"get": {
|
"get": {
|
||||||
|
"operationId": "shortenUrl",
|
||||||
"tags": [
|
"tags": [
|
||||||
"ShortCodes"
|
"Short URLs"
|
||||||
],
|
],
|
||||||
"summary": "Create a short URL",
|
"summary": "Create a short URL",
|
||||||
"description": "Creates a short URL in a single API call. Useful for third party integrations",
|
"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.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "apiKey",
|
"name": "apiKey",
|
||||||
@@ -44,21 +45,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"$ref": "../definitions/ShortUrl.json"
|
||||||
"properties": {
|
|
||||||
"longUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The original long URL that has been shortened"
|
|
||||||
},
|
|
||||||
"shortUrl": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "The generated short URL"
|
|
||||||
},
|
|
||||||
"shortCode": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "the short code that is being used in the short URL"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"text/plain": {
|
"text/plain": {
|
||||||
@@ -70,10 +57,16 @@
|
|||||||
"examples": {
|
"examples": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"longUrl": "https://github.com/shlinkio/shlink",
|
"longUrl": "https://github.com/shlinkio/shlink",
|
||||||
"shortUrl": "https://dom.ain/abc123",
|
"shortUrl": "https://doma.in/abc123",
|
||||||
"shortCode": "abc123"
|
"shortCode": "abc123",
|
||||||
|
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||||
|
"visitsCount": 0,
|
||||||
|
"tags": [
|
||||||
|
"games",
|
||||||
|
"tech"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"text/plain": "https://dom.ain/abc123"
|
"text/plain": "https://doma.in/abc123"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
{
|
{
|
||||||
"get": {
|
"get": {
|
||||||
|
"operationId": "getShortUrl",
|
||||||
"tags": [
|
"tags": [
|
||||||
"ShortCodes"
|
"Short URLs"
|
||||||
],
|
],
|
||||||
"summary": "Parse short code",
|
"summary": "Parse short code",
|
||||||
"description": "Get the long URL behind a 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.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"name": "shortCode",
|
||||||
@@ -17,6 +18,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
@@ -78,11 +82,12 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"put": {
|
"put": {
|
||||||
|
"operationId": "editShortUrl",
|
||||||
"tags": [
|
"tags": [
|
||||||
"ShortCodes"
|
"Short URLs"
|
||||||
],
|
],
|
||||||
"summary": "Edit short code",
|
"summary": "Edit short URL",
|
||||||
"description": "Update certain meta arguments from an existing 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.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"name": "shortCode",
|
||||||
@@ -120,6 +125,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
@@ -162,11 +170,12 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"delete": {
|
"delete": {
|
||||||
|
"operationId": "deleteShortUrl",
|
||||||
"tags": [
|
"tags": [
|
||||||
"ShortCodes"
|
"Short URLs"
|
||||||
],
|
],
|
||||||
"summary": "Delete short code",
|
"summary": "Delete short URL",
|
||||||
"description": "Deletes the short URL for provided short code.",
|
"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.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"name": "shortCode",
|
||||||
@@ -179,13 +188,16 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"204": {
|
"204": {
|
||||||
"description": "The short code has been properly deleted."
|
"description": "The short URL has been properly deleted."
|
||||||
},
|
},
|
||||||
"400": {
|
"400": {
|
||||||
"description": "The visits threshold in shlink does not allow this short URL to be deleted.",
|
"description": "The visits threshold in shlink does not allow this short URL to be deleted.",
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"put": {
|
"put": {
|
||||||
|
"operationId": "editShortUrlTags",
|
||||||
"tags": [
|
"tags": [
|
||||||
"ShortCodes",
|
"Short URLs"
|
||||||
"Tags"
|
|
||||||
],
|
],
|
||||||
"summary": "Edit tags on short URL",
|
"summary": "Edit tags on short URL",
|
||||||
"description": "Edit the tags on provided short code.",
|
"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.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"name": "shortCode",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"description": "The shortCode in which we want to edit tags.",
|
"description": "The short code for the short URL in which we want to edit tags.",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -41,6 +41,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"get": {
|
"get": {
|
||||||
|
"operationId": "getShortUrlVisits",
|
||||||
"tags": [
|
"tags": [
|
||||||
"ShortCodes",
|
|
||||||
"Visits"
|
"Visits"
|
||||||
],
|
],
|
||||||
"summary": "List visits for short URL",
|
"summary": "List visits for short URL",
|
||||||
"description": "Get the list of visits on provided short code.",
|
"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.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "shortCode",
|
"name": "shortCode",
|
||||||
"in": "path",
|
"in": "path",
|
||||||
"description": "The shortCode from which we want to get the visits.",
|
"description": "The short code for the short URL from which we want to get the visits.",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
@@ -36,6 +36,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"get": {
|
"get": {
|
||||||
|
"operationId": "listTags",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Tags"
|
"Tags"
|
||||||
],
|
],
|
||||||
"summary": "List existing tags",
|
"summary": "List existing tags",
|
||||||
"description": "Returns the list of all tags used in any short URL, ordered by name",
|
"description": "Returns the list of all tags used in any short URL, ordered by name",
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
@@ -60,12 +64,16 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"post": {
|
"post": {
|
||||||
|
"operationId": "createTags",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Tags"
|
"Tags"
|
||||||
],
|
],
|
||||||
"summary": "Create tags",
|
"summary": "Create tags",
|
||||||
"description": "Provided a list of tags, creates all that do not yet exist",
|
"description": "Provided a list of tags, creates all that do not yet exist",
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
@@ -143,12 +151,16 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"put": {
|
"put": {
|
||||||
|
"operationId": "renameTag",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Tags"
|
"Tags"
|
||||||
],
|
],
|
||||||
"summary": "Rename tag",
|
"summary": "Rename tag",
|
||||||
"description": "Renames one existing tag",
|
"description": "Renames one existing tag",
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
@@ -216,6 +228,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
"delete": {
|
"delete": {
|
||||||
|
"operationId": "deleteTags",
|
||||||
"tags": [
|
"tags": [
|
||||||
"Tags"
|
"Tags"
|
||||||
],
|
],
|
||||||
@@ -236,6 +249,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": [
|
"security": [
|
||||||
|
{
|
||||||
|
"ApiKey": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"Bearer": []
|
"Bearer": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,8 +23,14 @@
|
|||||||
|
|
||||||
"components": {
|
"components": {
|
||||||
"securitySchemes": {
|
"securitySchemes": {
|
||||||
|
"ApiKey": {
|
||||||
|
"description": "A valid shlink API key",
|
||||||
|
"type": "apiKey",
|
||||||
|
"in": "header",
|
||||||
|
"name": "X-Api-Key"
|
||||||
|
},
|
||||||
"Bearer": {
|
"Bearer": {
|
||||||
"description": "The JWT identifying a previously logged API key",
|
"description": "**[Deprecated]** The JWT identifying a previously authenticated API key",
|
||||||
"type": "http",
|
"type": "http",
|
||||||
"scheme": "bearer",
|
"scheme": "bearer",
|
||||||
"bearerFormat": "JWT"
|
"bearerFormat": "JWT"
|
||||||
@@ -32,30 +38,49 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
"paths": {
|
"tags": [
|
||||||
"/v1/authenticate": {
|
{
|
||||||
"$ref": "paths/v1_authenticate.json"
|
"name": "Short URLs",
|
||||||
|
"description": "Operations that can be performed on short URLs"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Tags",
|
||||||
|
"description": "Let you handle the list of available tags"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Visits",
|
||||||
|
"description": "Operations to manage visits on short URLs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Authentication",
|
||||||
|
"description": "Authentication-related endpoints"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
|
||||||
"/v1/short-codes": {
|
"paths": {
|
||||||
"$ref": "paths/v1_short-codes.json"
|
"/v1/short-urls": {
|
||||||
|
"$ref": "paths/v1_short-urls.json"
|
||||||
},
|
},
|
||||||
"/v1/short-codes/shorten": {
|
"/v1/short-urls/shorten": {
|
||||||
"$ref": "paths/v1_short-codes_shorten.json"
|
"$ref": "paths/v1_short-urls_shorten.json"
|
||||||
},
|
},
|
||||||
"/v1/short-codes/{shortCode}": {
|
"/v1/short-urls/{shortCode}": {
|
||||||
"$ref": "paths/v1_short-codes_{shortCode}.json"
|
"$ref": "paths/v1_short-urls_{shortCode}.json"
|
||||||
},
|
},
|
||||||
"/v1/short-codes/{shortCode}/tags": {
|
"/v1/short-urls/{shortCode}/tags": {
|
||||||
"$ref": "paths/v1_short-codes_{shortCode}_tags.json"
|
"$ref": "paths/v1_short-urls_{shortCode}_tags.json"
|
||||||
},
|
},
|
||||||
|
|
||||||
"/v1/tags": {
|
"/v1/tags": {
|
||||||
"$ref": "paths/v1_tags.json"
|
"$ref": "paths/v1_tags.json"
|
||||||
},
|
},
|
||||||
|
|
||||||
"/v1/short-codes/{shortCode}/visits": {
|
"/v1/short-urls/{shortCode}/visits": {
|
||||||
"$ref": "paths/v1_short-codes_{shortCode}_visits.json"
|
"$ref": "paths/v1_short-urls_{shortCode}_visits.json"
|
||||||
|
},
|
||||||
|
|
||||||
|
"/v1/authenticate": {
|
||||||
|
"$ref": "paths/v1_authenticate.json"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"directories": [
|
"directories": [
|
||||||
"module/*/src"
|
"module/*/src"
|
||||||
],
|
]
|
||||||
"excludes": []
|
|
||||||
},
|
},
|
||||||
"timeout": 10,
|
"timeout": 10,
|
||||||
"logs": {
|
"logs": {
|
||||||
@@ -17,6 +16,7 @@
|
|||||||
},
|
},
|
||||||
"mutators": {
|
"mutators": {
|
||||||
"@default": true,
|
"@default": true,
|
||||||
"IdenticalEqual": false
|
"IdenticalEqual": false,
|
||||||
|
"NotIdenticalNotEqual": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ name: ShlinkMigrations
|
|||||||
migrations_namespace: ShlinkMigrations
|
migrations_namespace: ShlinkMigrations
|
||||||
table_name: migrations
|
table_name: migrations
|
||||||
migrations_directory: data/migrations
|
migrations_directory: data/migrations
|
||||||
|
custom_template: data/migrations_template.txt
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Command;
|
namespace Shlinkio\Shlink\CLI;
|
||||||
use Shlinkio\Shlink\Common;
|
|
||||||
|
use function Shlinkio\Shlink\Common\env;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'cli' => [
|
'cli' => [
|
||||||
'locale' => Common\env('CLI_LOCALE', 'en'),
|
'locale' => env('CLI_LOCALE', 'en'),
|
||||||
'commands' => [
|
'commands' => [
|
||||||
Command\Shortcode\GenerateShortcodeCommand::NAME => Command\Shortcode\GenerateShortcodeCommand::class,
|
Command\ShortUrl\GenerateShortUrlCommand::NAME => Command\ShortUrl\GenerateShortUrlCommand::class,
|
||||||
Command\Shortcode\ResolveUrlCommand::NAME => Command\Shortcode\ResolveUrlCommand::class,
|
Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,
|
||||||
Command\Shortcode\ListShortcodesCommand::NAME => Command\Shortcode\ListShortcodesCommand::class,
|
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
|
||||||
Command\Shortcode\GetVisitsCommand::NAME => Command\Shortcode\GetVisitsCommand::class,
|
Command\ShortUrl\GetVisitsCommand::NAME => Command\ShortUrl\GetVisitsCommand::class,
|
||||||
Command\Shortcode\GeneratePreviewCommand::NAME => Command\Shortcode\GeneratePreviewCommand::class,
|
Command\ShortUrl\GeneratePreviewCommand::NAME => Command\ShortUrl\GeneratePreviewCommand::class,
|
||||||
Command\Shortcode\DeleteShortCodeCommand::NAME => Command\Shortcode\DeleteShortCodeCommand::class,
|
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||||
|
|
||||||
Command\Visit\ProcessVisitsCommand::NAME => Command\Visit\ProcessVisitsCommand::class,
|
Command\Visit\ProcessVisitsCommand::NAME => Command\Visit\ProcessVisitsCommand::class,
|
||||||
|
Command\Visit\UpdateDbCommand::NAME => Command\Visit\UpdateDbCommand::class,
|
||||||
|
|
||||||
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
|
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
|
||||||
Command\Config\GenerateSecretCommand::NAME => Command\Config\GenerateSecretCommand::class,
|
Command\Config\GenerateSecretCommand::NAME => Command\Config\GenerateSecretCommand::class,
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Command;
|
namespace Shlinkio\Shlink\CLI;
|
||||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
|
||||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdater;
|
||||||
|
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
use Shlinkio\Shlink\Core\Service;
|
use Shlinkio\Shlink\Core\Service;
|
||||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
@@ -15,16 +16,17 @@ return [
|
|||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
Application::class => ApplicationFactory::class,
|
Application::class => Factory\ApplicationFactory::class,
|
||||||
|
|
||||||
Command\Shortcode\GenerateShortcodeCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\GenerateShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Shortcode\ResolveUrlCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Shortcode\ListShortcodesCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Shortcode\GetVisitsCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Shortcode\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Shortcode\DeleteShortCodeCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class,
|
Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
Command\Visit\UpdateDbCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Command\Config\GenerateCharsetCommand::class => ConfigAbstractFactory::class,
|
Command\Config\GenerateCharsetCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Config\GenerateSecretCommand::class => ConfigAbstractFactory::class,
|
Command\Config\GenerateSecretCommand::class => ConfigAbstractFactory::class,
|
||||||
@@ -41,33 +43,34 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
Command\Shortcode\GenerateShortcodeCommand::class => [
|
Command\ShortUrl\GenerateShortUrlCommand::class => [
|
||||||
Service\UrlShortener::class,
|
Service\UrlShortener::class,
|
||||||
'translator',
|
'translator',
|
||||||
'config.url_shortener.domain',
|
'config.url_shortener.domain',
|
||||||
],
|
],
|
||||||
Command\Shortcode\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'],
|
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'],
|
||||||
Command\Shortcode\ListShortcodesCommand::class => [
|
Command\ShortUrl\ListShortUrlsCommand::class => [
|
||||||
Service\ShortUrlService::class,
|
Service\ShortUrlService::class,
|
||||||
'translator',
|
'translator',
|
||||||
'config.url_shortener.domain',
|
'config.url_shortener.domain',
|
||||||
],
|
],
|
||||||
Command\Shortcode\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'],
|
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'],
|
||||||
Command\Shortcode\GeneratePreviewCommand::class => [
|
Command\ShortUrl\GeneratePreviewCommand::class => [
|
||||||
Service\ShortUrlService::class,
|
Service\ShortUrlService::class,
|
||||||
PreviewGenerator::class,
|
PreviewGenerator::class,
|
||||||
'translator',
|
'translator',
|
||||||
],
|
],
|
||||||
Command\Shortcode\DeleteShortCodeCommand::class => [
|
Command\ShortUrl\DeleteShortUrlCommand::class => [
|
||||||
Service\ShortUrl\DeleteShortUrlService::class,
|
Service\ShortUrl\DeleteShortUrlService::class,
|
||||||
'translator',
|
'translator',
|
||||||
],
|
],
|
||||||
|
|
||||||
Command\Visit\ProcessVisitsCommand::class => [
|
Command\Visit\ProcessVisitsCommand::class => [
|
||||||
Service\VisitService::class,
|
Service\VisitService::class,
|
||||||
IpApiLocationResolver::class,
|
IpLocationResolverInterface::class,
|
||||||
'translator',
|
'translator',
|
||||||
],
|
],
|
||||||
|
Command\Visit\UpdateDbCommand::class => [DbUpdater::class, 'translator'],
|
||||||
|
|
||||||
Command\Config\GenerateCharsetCommand::class => ['translator'],
|
Command\Config\GenerateCharsetCommand::class => ['translator'],
|
||||||
Command\Config\GenerateSecretCommand::class => ['translator'],
|
Command\Config\GenerateSecretCommand::class => ['translator'],
|
||||||
|
|||||||
Binary file not shown.
@@ -1,8 +1,8 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Shlink 1.0\n"
|
"Project-Id-Version: Shlink 1.0\n"
|
||||||
"POT-Creation-Date: 2018-09-15 17:57+0200\n"
|
"POT-Creation-Date: 2018-11-12 21:01+0100\n"
|
||||||
"PO-Revision-Date: 2018-09-15 18:02+0200\n"
|
"PO-Revision-Date: 2018-11-12 21:03+0100\n"
|
||||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
@@ -83,8 +83,8 @@ msgstr "Clave secreta: \"%s\""
|
|||||||
msgid "Deletes a short URL"
|
msgid "Deletes a short URL"
|
||||||
msgstr "Elimina una URL"
|
msgstr "Elimina una URL"
|
||||||
|
|
||||||
msgid "The short code to be deleted"
|
msgid "The short code for the short URL to be deleted"
|
||||||
msgstr "El código corto a eliminar"
|
msgstr "El código corto de la URL corta a eliminar"
|
||||||
|
|
||||||
msgid ""
|
msgid ""
|
||||||
"Ignores the safety visits threshold check, which could make short URLs with "
|
"Ignores the safety visits threshold check, which could make short URLs with "
|
||||||
@@ -135,9 +135,8 @@ msgstr " <info>¡Correcto!</info>"
|
|||||||
msgid "Error"
|
msgid "Error"
|
||||||
msgstr "Error"
|
msgstr "Error"
|
||||||
|
|
||||||
msgid "Generates a short code for provided URL and returns the short URL"
|
msgid "Generates a short URL for provided long URL and returns it"
|
||||||
msgstr ""
|
msgstr "Genera una URL corta para la URL larga proporcionada y la devuelve"
|
||||||
"Genera un código corto para la URL proporcionada y devuelve la URL acortada"
|
|
||||||
|
|
||||||
msgid "The long URL to parse"
|
msgid "The long URL to parse"
|
||||||
msgstr "La URL larga a procesar"
|
msgstr "La URL larga a procesar"
|
||||||
@@ -268,8 +267,8 @@ msgstr "Número de visitas"
|
|||||||
msgid "Tags"
|
msgid "Tags"
|
||||||
msgstr "Etiquetas"
|
msgstr "Etiquetas"
|
||||||
|
|
||||||
msgid "Short codes properly listed"
|
msgid "Short URLs properly listed"
|
||||||
msgstr "Códigos cortos correctamente listados"
|
msgstr "URLs cortas listadas correctamente"
|
||||||
|
|
||||||
msgid "Continue with page"
|
msgid "Continue with page"
|
||||||
msgstr "Continuar con la página"
|
msgstr "Continuar con la página"
|
||||||
@@ -341,6 +340,9 @@ msgstr "Una etiqueta con nombre \"%s\" no ha sido encontrada"
|
|||||||
msgid "Processes visits where location is not set yet"
|
msgid "Processes visits where location is not set yet"
|
||||||
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
|
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
|
||||||
|
|
||||||
|
msgid "Ignored visit with no IP address"
|
||||||
|
msgstr "Ignorada visita sin dirección IP"
|
||||||
|
|
||||||
msgid "Processing IP"
|
msgid "Processing IP"
|
||||||
msgstr "Procesando IP"
|
msgstr "Procesando IP"
|
||||||
|
|
||||||
@@ -351,16 +353,33 @@ msgstr "Ignorada IP de localhost"
|
|||||||
msgid "Address located at \"%s\""
|
msgid "Address located at \"%s\""
|
||||||
msgstr "Dirección localizada en \"%s\""
|
msgstr "Dirección localizada en \"%s\""
|
||||||
|
|
||||||
msgid "An error occurred while locating IP"
|
msgid "An error occurred while locating IP. Skipped"
|
||||||
msgstr "Se produjo un error al localizar la IP"
|
msgstr "Se produjo un error al localizar la IP. Ignorado"
|
||||||
|
|
||||||
#, php-format
|
|
||||||
msgid "IP location resolver limit reached. Waiting %s seconds..."
|
|
||||||
msgstr "Limite del localizador de IPs alcanzado. Esperando %s segundos..."
|
|
||||||
|
|
||||||
msgid "Finished processing all IPs"
|
msgid "Finished processing all IPs"
|
||||||
msgstr "Finalizado el procesado de todas las IPs"
|
msgstr "Finalizado el procesado de todas las IPs"
|
||||||
|
|
||||||
|
msgid "Updates the GeoLite2 database file used to geolocate IP addresses"
|
||||||
|
msgstr ""
|
||||||
|
"Actualiza el fichero de base de datos de GeoLite2 usado para geolocalizar "
|
||||||
|
"direcciones IP"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"The GeoLite2 database is updated first Tuesday every month, so this command "
|
||||||
|
"should be ideally run every first Wednesday"
|
||||||
|
msgstr ""
|
||||||
|
"La base de datos de GeoLite2 se actualiza el primer Martes de cada mes, por "
|
||||||
|
"lo que la opción ideal es ejecutar este comando cada primer miércoles de mes"
|
||||||
|
|
||||||
|
msgid "GeoLite2 database properly updated"
|
||||||
|
msgstr "Base de datos de GeoLite2 correctamente actualizada"
|
||||||
|
|
||||||
|
msgid "An error occurred while updating GeoLite2 database"
|
||||||
|
msgstr "Se produjo un error al actualizar la base de datos de GeoLite2"
|
||||||
|
|
||||||
|
#~ msgid "IP location resolver limit reached. Waiting %s seconds..."
|
||||||
|
#~ msgstr "Limite del localizador de IPs alcanzado. Esperando %s segundos..."
|
||||||
|
|
||||||
#~ msgid "Remote Address"
|
#~ msgid "Remote Address"
|
||||||
#~ msgstr "Dirección remota"
|
#~ msgstr "Dirección remota"
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
@@ -10,10 +11,11 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class DisableKeyCommand extends Command
|
class DisableKeyCommand extends Command
|
||||||
{
|
{
|
||||||
const NAME = 'api-key:disable';
|
public const NAME = 'api-key:disable';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ApiKeyServiceInterface
|
* @var ApiKeyServiceInterface
|
||||||
@@ -31,14 +33,14 @@ class DisableKeyCommand extends Command
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure()
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setName(self::NAME)
|
$this->setName(self::NAME)
|
||||||
->setDescription($this->translator->translate('Disables an API key.'))
|
->setDescription($this->translator->translate('Disables an API key.'))
|
||||||
->addArgument('apiKey', InputArgument::REQUIRED, $this->translator->translate('The API key to disable'));
|
->addArgument('apiKey', InputArgument::REQUIRED, $this->translator->translate('The API key to disable'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$apiKey = $input->getArgument('apiKey');
|
$apiKey = $input->getArgument('apiKey');
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
@@ -46,7 +48,7 @@ class DisableKeyCommand extends Command
|
|||||||
try {
|
try {
|
||||||
$this->apiKeyService->disable($apiKey);
|
$this->apiKeyService->disable($apiKey);
|
||||||
$io->success(sprintf($this->translator->translate('API key "%s" properly disabled'), $apiKey));
|
$io->success(sprintf($this->translator->translate('API key "%s" properly disabled'), $apiKey));
|
||||||
} catch (\InvalidArgumentException $e) {
|
} catch (InvalidArgumentException $e) {
|
||||||
$io->error(sprintf($this->translator->translate('API key "%s" does not exist.'), $apiKey));
|
$io->error(sprintf($this->translator->translate('API key "%s" does not exist.'), $apiKey));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -10,10 +11,11 @@ use Symfony\Component\Console\Input\InputOption;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class GenerateKeyCommand extends Command
|
class GenerateKeyCommand extends Command
|
||||||
{
|
{
|
||||||
const NAME = 'api-key:generate';
|
public const NAME = 'api-key:generate';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ApiKeyServiceInterface
|
* @var ApiKeyServiceInterface
|
||||||
@@ -31,7 +33,7 @@ class GenerateKeyCommand extends Command
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure()
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setName(self::NAME)
|
$this->setName(self::NAME)
|
||||||
->setDescription($this->translator->translate('Generates a new valid API key.'))
|
->setDescription($this->translator->translate('Generates a new valid API key.'))
|
||||||
@@ -43,10 +45,10 @@ class GenerateKeyCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$expirationDate = $input->getOption('expirationDate');
|
$expirationDate = $input->getOption('expirationDate');
|
||||||
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? new \DateTime($expirationDate) : null);
|
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? Chronos::parse($expirationDate) : null);
|
||||||
|
|
||||||
(new SymfonyStyle($input, $output))->success(
|
(new SymfonyStyle($input, $output))->success(
|
||||||
sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey)
|
sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey)
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ use Symfony\Component\Console\Input\InputOption;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function array_filter;
|
||||||
|
use function array_map;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class ListKeysCommand extends Command
|
class ListKeysCommand extends Command
|
||||||
{
|
{
|
||||||
@@ -36,7 +39,7 @@ class ListKeysCommand extends Command
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure()
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setName(self::NAME)
|
$this->setName(self::NAME)
|
||||||
->setDescription($this->translator->translate('Lists all the available API keys.'))
|
->setDescription($this->translator->translate('Lists all the available API keys.'))
|
||||||
@@ -48,30 +51,26 @@ class ListKeysCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
$enabledOnly = $input->getOption('enabledOnly');
|
$enabledOnly = $input->getOption('enabledOnly');
|
||||||
$list = $this->apiKeyService->listKeys($enabledOnly);
|
|
||||||
$rows = [];
|
|
||||||
|
|
||||||
/** @var ApiKey $row */
|
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
|
||||||
foreach ($list as $row) {
|
$key = (string) $apiKey;
|
||||||
$key = $row->getKey();
|
$expiration = $apiKey->getExpirationDate();
|
||||||
$expiration = $row->getExpirationDate();
|
$messagePattern = $this->determineMessagePattern($apiKey);
|
||||||
$messagePattern = $this->determineMessagePattern($row);
|
|
||||||
|
|
||||||
// Set columns for this row
|
// Set columns for this row
|
||||||
$rowData = [\sprintf($messagePattern, $key)];
|
$rowData = [sprintf($messagePattern, $key)];
|
||||||
if (! $enabledOnly) {
|
if (! $enabledOnly) {
|
||||||
$rowData[] = \sprintf($messagePattern, $this->getEnabledSymbol($row));
|
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
|
||||||
}
|
}
|
||||||
$rowData[] = $expiration !== null ? $expiration->format(\DateTime::ATOM) : '-';
|
$rowData[] = $expiration !== null ? $expiration->toAtomString() : '-';
|
||||||
|
return $rowData;
|
||||||
|
}, $this->apiKeyService->listKeys($enabledOnly));
|
||||||
|
|
||||||
$rows[] = $rowData;
|
$io->table(array_filter([
|
||||||
}
|
|
||||||
|
|
||||||
$io->table(\array_filter([
|
|
||||||
$this->translator->translate('Key'),
|
$this->translator->translate('Key'),
|
||||||
! $enabledOnly ? $this->translator->translate('Is enabled') : null,
|
! $enabledOnly ? $this->translator->translate('Is enabled') : null,
|
||||||
$this->translator->translate('Expiration date'),
|
$this->translator->translate('Expiration date'),
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function sprintf;
|
||||||
|
use function str_shuffle;
|
||||||
|
|
||||||
class GenerateCharsetCommand extends Command
|
class GenerateCharsetCommand extends Command
|
||||||
{
|
{
|
||||||
const NAME = 'config:generate-charset';
|
public const NAME = 'config:generate-charset';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TranslatorInterface
|
* @var TranslatorInterface
|
||||||
@@ -25,7 +27,7 @@ class GenerateCharsetCommand extends Command
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure()
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setName(self::NAME)
|
$this->setName(self::NAME)
|
||||||
->setDescription(sprintf($this->translator->translate(
|
->setDescription(sprintf($this->translator->translate(
|
||||||
@@ -34,11 +36,11 @@ class GenerateCharsetCommand extends Command
|
|||||||
), UrlShortener::DEFAULT_CHARS));
|
), UrlShortener::DEFAULT_CHARS));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
|
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
|
||||||
(new SymfonyStyle($input, $output))->success(
|
(new SymfonyStyle($input, $output))->success(
|
||||||
\sprintf($this->translator->translate('Character set: "%s"'), $charSet)
|
sprintf($this->translator->translate('Character set: "%s"'), $charSet)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,12 +9,13 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class GenerateSecretCommand extends Command
|
class GenerateSecretCommand extends Command
|
||||||
{
|
{
|
||||||
use StringUtilsTrait;
|
use StringUtilsTrait;
|
||||||
|
|
||||||
const NAME = 'config:generate-secret';
|
public const NAME = 'config:generate-secret';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TranslatorInterface
|
* @var TranslatorInterface
|
||||||
@@ -27,7 +28,7 @@ class GenerateSecretCommand extends Command
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure()
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setName(self::NAME)
|
$this->setName(self::NAME)
|
||||||
->setDescription($this->translator->translate(
|
->setDescription($this->translator->translate(
|
||||||
@@ -35,7 +36,7 @@ class GenerateSecretCommand extends Command
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$secret = $this->generateRandomString(32);
|
$secret = $this->generateRandomString(32);
|
||||||
(new SymfonyStyle($input, $output))->success(
|
(new SymfonyStyle($input, $output))->success(
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\Core\Exception;
|
use Shlinkio\Shlink\Core\Exception;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||||
@@ -12,11 +12,12 @@ use Symfony\Component\Console\Input\InputOption;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class DeleteShortCodeCommand extends Command
|
class DeleteShortUrlCommand extends Command
|
||||||
{
|
{
|
||||||
public const NAME = 'short-code:delete';
|
public const NAME = 'short-url:delete';
|
||||||
private const ALIASES = [];
|
private const ALIASES = ['short-code:delete'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var DeleteShortUrlServiceInterface
|
* @var DeleteShortUrlServiceInterface
|
||||||
@@ -45,7 +46,7 @@ class DeleteShortCodeCommand extends Command
|
|||||||
->addArgument(
|
->addArgument(
|
||||||
'shortCode',
|
'shortCode',
|
||||||
InputArgument::REQUIRED,
|
InputArgument::REQUIRED,
|
||||||
$this->translator->translate('The short code to be deleted')
|
$this->translator->translate('The short code for the short URL to be deleted')
|
||||||
)
|
)
|
||||||
->addOption(
|
->addOption(
|
||||||
'ignore-threshold',
|
'ignore-threshold',
|
||||||
@@ -68,7 +69,7 @@ class DeleteShortCodeCommand extends Command
|
|||||||
$this->runDelete($io, $shortCode, $ignoreThreshold);
|
$this->runDelete($io, $shortCode, $ignoreThreshold);
|
||||||
} catch (Exception\InvalidShortCodeException $e) {
|
} catch (Exception\InvalidShortCodeException $e) {
|
||||||
$io->error(
|
$io->error(
|
||||||
\sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
||||||
);
|
);
|
||||||
} catch (Exception\DeleteShortUrlException $e) {
|
} catch (Exception\DeleteShortUrlException $e) {
|
||||||
$this->retry($io, $shortCode, $e);
|
$this->retry($io, $shortCode, $e);
|
||||||
@@ -77,7 +78,7 @@ class DeleteShortCodeCommand extends Command
|
|||||||
|
|
||||||
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): void
|
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): void
|
||||||
{
|
{
|
||||||
$warningMsg = \sprintf($this->translator->translate(
|
$warningMsg = sprintf($this->translator->translate(
|
||||||
'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.'
|
'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.'
|
||||||
), $shortCode, $e->getVisitsThreshold());
|
), $shortCode, $e->getVisitsThreshold());
|
||||||
$io->writeln('<bg=yellow>' . $warningMsg . '</>');
|
$io->writeln('<bg=yellow>' . $warningMsg . '</>');
|
||||||
@@ -93,7 +94,7 @@ class DeleteShortCodeCommand extends Command
|
|||||||
private function runDelete(SymfonyStyle $io, string $shortCode, bool $ignoreThreshold): void
|
private function runDelete(SymfonyStyle $io, string $shortCode, bool $ignoreThreshold): void
|
||||||
{
|
{
|
||||||
$this->deleteShortUrlService->deleteByShortCode($shortCode, $ignoreThreshold);
|
$this->deleteShortUrlService->deleteByShortCode($shortCode, $ignoreThreshold);
|
||||||
$io->success(\sprintf(
|
$io->success(sprintf(
|
||||||
$this->translator->translate('Short URL with short code "%s" successfully deleted.'),
|
$this->translator->translate('Short URL with short code "%s" successfully deleted.'),
|
||||||
$shortCode
|
$shortCode
|
||||||
));
|
));
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||||
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
|
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
|
||||||
@@ -11,11 +11,12 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class GeneratePreviewCommand extends Command
|
class GeneratePreviewCommand extends Command
|
||||||
{
|
{
|
||||||
public const NAME = 'short-code:process-previews';
|
public const NAME = 'short-url:process-previews';
|
||||||
private const ALIASES = ['shortcode:process-previews'];
|
private const ALIASES = ['shortcode:process-previews', 'short-code:process-previews'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var PreviewGeneratorInterface
|
* @var PreviewGeneratorInterface
|
||||||
@@ -61,7 +62,7 @@ class GeneratePreviewCommand extends Command
|
|||||||
$page += 1;
|
$page += 1;
|
||||||
|
|
||||||
foreach ($shortUrls as $shortUrl) {
|
foreach ($shortUrls as $shortUrl) {
|
||||||
$this->processUrl($shortUrl->getOriginalUrl(), $output);
|
$this->processUrl($shortUrl->getLongUrl(), $output);
|
||||||
}
|
}
|
||||||
} while ($page <= $shortUrls->count());
|
} while ($page <= $shortUrls->count());
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ class GeneratePreviewCommand extends Command
|
|||||||
private function processUrl($url, OutputInterface $output): void
|
private function processUrl($url, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$output->write(\sprintf($this->translator->translate('Processing URL %s...'), $url));
|
$output->write(sprintf($this->translator->translate('Processing URL %s...'), $url));
|
||||||
$this->previewGenerator->generatePreview($url);
|
$this->previewGenerator->generatePreview($url);
|
||||||
$output->writeln($this->translator->translate(' <info>Success!</info>'));
|
$output->writeln($this->translator->translate(' <info>Success!</info>'));
|
||||||
} catch (PreviewGenerationException $e) {
|
} catch (PreviewGenerationException $e) {
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||||
@@ -15,13 +16,16 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\Diactoros\Uri;
|
use Zend\Diactoros\Uri;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function array_merge;
|
||||||
|
use function explode;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class GenerateShortcodeCommand extends Command
|
class GenerateShortUrlCommand extends Command
|
||||||
{
|
{
|
||||||
use ShortUrlBuilderTrait;
|
use ShortUrlBuilderTrait;
|
||||||
|
|
||||||
public const NAME = 'short-code:generate';
|
public const NAME = 'short-url:generate';
|
||||||
private const ALIASES = ['shortcode:generate'];
|
private const ALIASES = ['shortcode:generate', 'short-code:generate'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var UrlShortenerInterface
|
* @var UrlShortenerInterface
|
||||||
@@ -53,7 +57,7 @@ class GenerateShortcodeCommand extends Command
|
|||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
->setAliases(self::ALIASES)
|
->setAliases(self::ALIASES)
|
||||||
->setDescription(
|
->setDescription(
|
||||||
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
|
$this->translator->translate('Generates a short URL for provided long URL and returns it')
|
||||||
)
|
)
|
||||||
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
|
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
|
||||||
->addOption(
|
->addOption(
|
||||||
@@ -106,8 +110,8 @@ class GenerateShortcodeCommand extends Command
|
|||||||
$tags = $input->getOption('tags');
|
$tags = $input->getOption('tags');
|
||||||
$processedTags = [];
|
$processedTags = [];
|
||||||
foreach ($tags as $key => $tag) {
|
foreach ($tags as $key => $tag) {
|
||||||
$explodedTags = \explode(',', $tag);
|
$explodedTags = explode(',', $tag);
|
||||||
$processedTags = \array_merge($processedTags, $explodedTags);
|
$processedTags = array_merge($processedTags, $explodedTags);
|
||||||
}
|
}
|
||||||
$tags = $processedTags;
|
$tags = $processedTags;
|
||||||
$customSlug = $input->getOption('customSlug');
|
$customSlug = $input->getOption('customSlug');
|
||||||
@@ -125,16 +129,16 @@ class GenerateShortcodeCommand extends Command
|
|||||||
$shortUrl = $this->buildShortUrl($this->domainConfig, $shortCode);
|
$shortUrl = $this->buildShortUrl($this->domainConfig, $shortCode);
|
||||||
|
|
||||||
$io->writeln([
|
$io->writeln([
|
||||||
\sprintf('%s <info>%s</info>', $this->translator->translate('Processed long URL:'), $longUrl),
|
sprintf('%s <info>%s</info>', $this->translator->translate('Processed long URL:'), $longUrl),
|
||||||
\sprintf('%s <info>%s</info>', $this->translator->translate('Generated short URL:'), $shortUrl),
|
sprintf('%s <info>%s</info>', $this->translator->translate('Generated short URL:'), $shortUrl),
|
||||||
]);
|
]);
|
||||||
} catch (InvalidUrlException $e) {
|
} catch (InvalidUrlException $e) {
|
||||||
$io->error(\sprintf(
|
$io->error(sprintf(
|
||||||
$this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'),
|
$this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'),
|
||||||
$longUrl
|
$longUrl
|
||||||
));
|
));
|
||||||
} catch (NonUniqueSlugException $e) {
|
} catch (NonUniqueSlugException $e) {
|
||||||
$io->error(\sprintf(
|
$io->error(sprintf(
|
||||||
$this->translator->translate(
|
$this->translator->translate(
|
||||||
'Provided slug "%s" is already in use by another URL. Try with a different one.'
|
'Provided slug "%s" is already in use by another URL. Try with a different one.'
|
||||||
),
|
),
|
||||||
@@ -143,9 +147,9 @@ class GenerateShortcodeCommand extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getOptionalDate(InputInterface $input, string $fieldName): ?\DateTime
|
private function getOptionalDate(InputInterface $input, string $fieldName): ?Chronos
|
||||||
{
|
{
|
||||||
$since = $input->getOption($fieldName);
|
$since = $input->getOption($fieldName);
|
||||||
return $since !== null ? new \DateTime($since) : null;
|
return $since !== null ? Chronos::parse($since) : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
@@ -12,11 +14,13 @@ use Symfony\Component\Console\Input\InputOption;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function array_map;
|
||||||
|
use function Functional\select_keys;
|
||||||
|
|
||||||
class GetVisitsCommand extends Command
|
class GetVisitsCommand extends Command
|
||||||
{
|
{
|
||||||
public const NAME = 'short-code:visits';
|
public const NAME = 'short-url:visits';
|
||||||
private const ALIASES = ['shortcode:visits'];
|
private const ALIASES = ['shortcode:visits', 'short-code:visits'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var VisitsTrackerInterface
|
* @var VisitsTrackerInterface
|
||||||
@@ -85,17 +89,11 @@ class GetVisitsCommand extends Command
|
|||||||
$endDate = $this->getDateOption($input, 'endDate');
|
$endDate = $this->getDateOption($input, 'endDate');
|
||||||
|
|
||||||
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
|
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
|
||||||
$rows = [];
|
$rows = array_map(function (Visit $visit) {
|
||||||
foreach ($visits as $row) {
|
$rowData = $visit->jsonSerialize();
|
||||||
$rowData = $row->jsonSerialize();
|
$rowData['country'] = $visit->getVisitLocation()->getCountryName();
|
||||||
|
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
|
||||||
// Unset location info and remote addr
|
}, $visits);
|
||||||
unset($rowData['visitLocation'], $rowData['remoteAddr']);
|
|
||||||
|
|
||||||
$rowData['country'] = $row->getVisitLocation()->getCountryName();
|
|
||||||
|
|
||||||
$rows[] = \array_values($rowData);
|
|
||||||
}
|
|
||||||
$io->table([
|
$io->table([
|
||||||
$this->translator->translate('Referer'),
|
$this->translator->translate('Referer'),
|
||||||
$this->translator->translate('Date'),
|
$this->translator->translate('Date'),
|
||||||
@@ -107,10 +105,6 @@ class GetVisitsCommand extends Command
|
|||||||
private function getDateOption(InputInterface $input, $key)
|
private function getDateOption(InputInterface $input, $key)
|
||||||
{
|
{
|
||||||
$value = $input->getOption($key);
|
$value = $input->getOption($key);
|
||||||
if (! empty($value)) {
|
return ! empty($value) ? Chronos::parse($value) : $value;
|
||||||
$value = new \DateTime($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||||
@@ -13,13 +13,18 @@ use Symfony\Component\Console\Input\InputOption;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function array_values;
|
||||||
|
use function count;
|
||||||
|
use function explode;
|
||||||
|
use function implode;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class ListShortcodesCommand extends Command
|
class ListShortUrlsCommand extends Command
|
||||||
{
|
{
|
||||||
use PaginatorUtilsTrait;
|
use PaginatorUtilsTrait;
|
||||||
|
|
||||||
public const NAME = 'short-code:list';
|
public const NAME = 'short-url:list';
|
||||||
private const ALIASES = ['shortcode:list'];
|
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ShortUrlServiceInterface
|
* @var ShortUrlServiceInterface
|
||||||
@@ -59,7 +64,7 @@ class ListShortcodesCommand extends Command
|
|||||||
$this->translator->translate('The first page to list (%s items per page)'),
|
$this->translator->translate('The first page to list (%s items per page)'),
|
||||||
PaginableRepositoryAdapter::ITEMS_PER_PAGE
|
PaginableRepositoryAdapter::ITEMS_PER_PAGE
|
||||||
),
|
),
|
||||||
1
|
'1'
|
||||||
)
|
)
|
||||||
->addOption(
|
->addOption(
|
||||||
'searchTerm',
|
'searchTerm',
|
||||||
@@ -97,7 +102,7 @@ class ListShortcodesCommand extends Command
|
|||||||
$page = (int) $input->getOption('page');
|
$page = (int) $input->getOption('page');
|
||||||
$searchTerm = $input->getOption('searchTerm');
|
$searchTerm = $input->getOption('searchTerm');
|
||||||
$tags = $input->getOption('tags');
|
$tags = $input->getOption('tags');
|
||||||
$tags = ! empty($tags) ? \explode(',', $tags) : [];
|
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
||||||
$showTags = $input->getOption('showTags');
|
$showTags = $input->getOption('showTags');
|
||||||
$transformer = new ShortUrlDataTransformer($this->domainConfig);
|
$transformer = new ShortUrlDataTransformer($this->domainConfig);
|
||||||
|
|
||||||
@@ -120,22 +125,22 @@ class ListShortcodesCommand extends Command
|
|||||||
foreach ($result as $row) {
|
foreach ($result as $row) {
|
||||||
$shortUrl = $transformer->transform($row);
|
$shortUrl = $transformer->transform($row);
|
||||||
if ($showTags) {
|
if ($showTags) {
|
||||||
$shortUrl['tags'] = \implode(', ', $shortUrl['tags']);
|
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
|
||||||
} else {
|
} else {
|
||||||
unset($shortUrl['tags']);
|
unset($shortUrl['tags']);
|
||||||
}
|
}
|
||||||
|
|
||||||
unset($shortUrl['originalUrl']);
|
unset($shortUrl['originalUrl']);
|
||||||
$rows[] = \array_values($shortUrl);
|
$rows[] = array_values($shortUrl);
|
||||||
}
|
}
|
||||||
$io->table($headers, $rows);
|
$io->table($headers, $rows);
|
||||||
|
|
||||||
if ($this->isLastPage($result)) {
|
if ($this->isLastPage($result)) {
|
||||||
$continue = false;
|
$continue = false;
|
||||||
$io->success($this->translator->translate('Short codes properly listed'));
|
$io->success($this->translator->translate('Short URLs properly listed'));
|
||||||
} else {
|
} else {
|
||||||
$continue = $io->confirm(
|
$continue = $io->confirm(
|
||||||
\sprintf($this->translator->translate('Continue with page') . ' <options=bold>%s</>?', $page),
|
sprintf($this->translator->translate('Continue with page') . ' <options=bold>%s</>?', $page),
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -149,7 +154,7 @@ class ListShortcodesCommand extends Command
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$orderBy = \explode(',', $orderBy);
|
$orderBy = explode(',', $orderBy);
|
||||||
return \count($orderBy) === 1 ? $orderBy[0] : [$orderBy[0] => $orderBy[1]];
|
return count($orderBy) === 1 ? $orderBy[0] : [$orderBy[0] => $orderBy[1]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
@@ -12,11 +12,12 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class ResolveUrlCommand extends Command
|
class ResolveUrlCommand extends Command
|
||||||
{
|
{
|
||||||
public const NAME = 'short-code:parse';
|
public const NAME = 'short-url:parse';
|
||||||
private const ALIASES = ['shortcode:parse'];
|
private const ALIASES = ['shortcode:parse', 'short-code:parse'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var UrlShortenerInterface
|
* @var UrlShortenerInterface
|
||||||
@@ -71,15 +72,15 @@ class ResolveUrlCommand extends Command
|
|||||||
try {
|
try {
|
||||||
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||||
$output->writeln(
|
$output->writeln(
|
||||||
\sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $url->getLongUrl())
|
sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $url->getLongUrl())
|
||||||
);
|
);
|
||||||
} catch (InvalidShortCodeException $e) {
|
} catch (InvalidShortCodeException $e) {
|
||||||
$io->error(
|
$io->error(
|
||||||
\sprintf($this->translator->translate('Provided short code "%s" has an invalid format.'), $shortCode)
|
sprintf($this->translator->translate('Provided short code "%s" has an invalid format.'), $shortCode)
|
||||||
);
|
);
|
||||||
} catch (EntityDoesNotExistException $e) {
|
} catch (EntityDoesNotExistException $e) {
|
||||||
$io->error(
|
$io->error(
|
||||||
\sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ use Zend\I18n\Translator\TranslatorInterface;
|
|||||||
|
|
||||||
class CreateTagCommand extends Command
|
class CreateTagCommand extends Command
|
||||||
{
|
{
|
||||||
const NAME = 'tag:create';
|
public const NAME = 'tag:create';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TagServiceInterface
|
* @var TagServiceInterface
|
||||||
@@ -31,7 +31,7 @@ class CreateTagCommand extends Command
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure()
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
@@ -44,7 +44,7 @@ class CreateTagCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
$tagNames = $input->getOption('name');
|
$tagNames = $input->getOption('name');
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use Zend\I18n\Translator\TranslatorInterface;
|
|||||||
|
|
||||||
class DeleteTagsCommand extends Command
|
class DeleteTagsCommand extends Command
|
||||||
{
|
{
|
||||||
const NAME = 'tag:delete';
|
public const NAME = 'tag:delete';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TagServiceInterface
|
* @var TagServiceInterface
|
||||||
@@ -31,7 +31,7 @@ class DeleteTagsCommand extends Command
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure()
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
@@ -44,7 +44,7 @@ class DeleteTagsCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
$tagNames = $input->getOption('name');
|
$tagNames = $input->getOption('name');
|
||||||
|
|||||||
@@ -10,10 +10,11 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function Functional\map;
|
||||||
|
|
||||||
class ListTagsCommand extends Command
|
class ListTagsCommand extends Command
|
||||||
{
|
{
|
||||||
const NAME = 'tag:list';
|
public const NAME = 'tag:list';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TagServiceInterface
|
* @var TagServiceInterface
|
||||||
@@ -31,28 +32,28 @@ class ListTagsCommand extends Command
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure()
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
->setDescription($this->translator->translate('Lists existing tags.'));
|
->setDescription($this->translator->translate('Lists existing tags.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
$io->table([$this->translator->translate('Name')], $this->getTagsRows());
|
$io->table([$this->translator->translate('Name')], $this->getTagsRows());
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getTagsRows()
|
private function getTagsRows(): array
|
||||||
{
|
{
|
||||||
$tags = $this->tagService->listTags();
|
$tags = $this->tagService->listTags();
|
||||||
if (empty($tags)) {
|
if (empty($tags)) {
|
||||||
return [[$this->translator->translate('No tags yet')]];
|
return [[$this->translator->translate('No tags yet')]];
|
||||||
}
|
}
|
||||||
|
|
||||||
return \array_map(function (Tag $tag) {
|
return map($tags, function (Tag $tag) {
|
||||||
return [$tag->getName()];
|
return [(string) $tag];
|
||||||
}, $tags);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class RenameTagCommand extends Command
|
class RenameTagCommand extends Command
|
||||||
{
|
{
|
||||||
const NAME = 'tag:rename';
|
public const NAME = 'tag:rename';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var TagServiceInterface
|
* @var TagServiceInterface
|
||||||
@@ -32,7 +33,7 @@ class RenameTagCommand extends Command
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure()
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this
|
$this
|
||||||
->setName(self::NAME)
|
->setName(self::NAME)
|
||||||
@@ -41,7 +42,7 @@ class RenameTagCommand extends Command
|
|||||||
->addArgument('newName', InputArgument::REQUIRED, $this->translator->translate('New name of the tag.'));
|
->addArgument('newName', InputArgument::REQUIRED, $this->translator->translate('New name of the tag.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
$oldName = $input->getArgument('oldName');
|
$oldName = $input->getArgument('oldName');
|
||||||
@@ -51,7 +52,7 @@ class RenameTagCommand extends Command
|
|||||||
$this->tagService->renameTag($oldName, $newName);
|
$this->tagService->renameTag($oldName, $newName);
|
||||||
$io->success($this->translator->translate('Tag properly renamed.'));
|
$io->success($this->translator->translate('Tag properly renamed.'));
|
||||||
} catch (EntityDoesNotExistException $e) {
|
} catch (EntityDoesNotExistException $e) {
|
||||||
$io->error(\sprintf($this->translator->translate('A tag with name "%s" was not found'), $oldName));
|
$io->error(sprintf($this->translator->translate('A tag with name "%s" was not found'), $oldName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
use Shlinkio\Shlink\Common\Service\IpLocationResolverInterface;
|
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
||||||
@@ -13,6 +13,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class ProcessVisitsCommand extends Command
|
class ProcessVisitsCommand extends Command
|
||||||
{
|
{
|
||||||
@@ -39,10 +40,10 @@ class ProcessVisitsCommand extends Command
|
|||||||
$this->visitService = $visitService;
|
$this->visitService = $visitService;
|
||||||
$this->ipLocationResolver = $ipLocationResolver;
|
$this->ipLocationResolver = $ipLocationResolver;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
parent::__construct(null);
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configure()
|
protected function configure(): void
|
||||||
{
|
{
|
||||||
$this->setName(self::NAME)
|
$this->setName(self::NAME)
|
||||||
->setDescription(
|
->setDescription(
|
||||||
@@ -50,53 +51,51 @@ class ProcessVisitsCommand extends Command
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(InputInterface $input, OutputInterface $output)
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
$io = new SymfonyStyle($input, $output);
|
||||||
$visits = $this->visitService->getUnlocatedVisits();
|
$visits = $this->visitService->getUnlocatedVisits();
|
||||||
|
|
||||||
$count = 0;
|
|
||||||
foreach ($visits as $visit) {
|
foreach ($visits as $visit) {
|
||||||
$ipAddr = $visit->getRemoteAddr();
|
if (! $visit->hasRemoteAddr()) {
|
||||||
$io->write(\sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
|
||||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
|
||||||
$io->writeln(
|
$io->writeln(
|
||||||
\sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
sprintf('<comment>%s</comment>', $this->translator->translate('Ignored visit with no IP address')),
|
||||||
|
OutputInterface::VERBOSITY_VERBOSE
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ipAddr = $visit->getRemoteAddr();
|
||||||
|
$io->write(sprintf('%s <fg=blue>%s</>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||||
|
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||||
|
$io->writeln(
|
||||||
|
sprintf(' [<comment>%s</comment>]', $this->translator->translate('Ignored localhost address'))
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$count++;
|
|
||||||
try {
|
try {
|
||||||
$result = $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
$result = $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||||
|
|
||||||
$location = new VisitLocation();
|
$location = new VisitLocation($result);
|
||||||
$location->exchangeArray($result);
|
|
||||||
$visit->setVisitLocation($location);
|
$visit->setVisitLocation($location);
|
||||||
$this->visitService->saveVisit($visit);
|
$this->visitService->saveVisit($visit);
|
||||||
|
|
||||||
$io->writeln(\sprintf(
|
$io->writeln(sprintf(
|
||||||
' (' . $this->translator->translate('Address located at "%s"') . ')',
|
' [<info>' . $this->translator->translate('Address located at "%s"') . '</info>]',
|
||||||
$location->getCityName()
|
$location->getCountryName()
|
||||||
));
|
));
|
||||||
} catch (WrongIpException $e) {
|
} catch (WrongIpException $e) {
|
||||||
$io->writeln(
|
$io->writeln(
|
||||||
\sprintf(' <error>%s</error>', $this->translator->translate('An error occurred while locating IP'))
|
sprintf(
|
||||||
|
' [<fg=red>%s</>]',
|
||||||
|
$this->translator->translate('An error occurred while locating IP. Skipped')
|
||||||
|
)
|
||||||
);
|
);
|
||||||
if ($io->isVerbose()) {
|
if ($io->isVerbose()) {
|
||||||
$this->getApplication()->renderException($e, $output);
|
$this->getApplication()->renderException($e, $output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($count === $this->ipLocationResolver->getApiLimit()) {
|
|
||||||
$count = 0;
|
|
||||||
$seconds = $this->ipLocationResolver->getApiInterval();
|
|
||||||
$io->note(\sprintf(
|
|
||||||
$this->translator->translate('IP location resolver limit reached. Waiting %s seconds...'),
|
|
||||||
$seconds
|
|
||||||
));
|
|
||||||
\sleep($seconds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$io->success($this->translator->translate('Finished processing all IPs'));
|
$io->success($this->translator->translate('Finished processing all IPs'));
|
||||||
|
|||||||
74
module/CLI/src/Command/Visit/UpdateDbCommand.php
Normal file
74
module/CLI/src/Command/Visit/UpdateDbCommand.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||||
|
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Helper\ProgressBar;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
|
||||||
|
class UpdateDbCommand extends Command
|
||||||
|
{
|
||||||
|
public const NAME = 'visit:update-db';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var DbUpdaterInterface
|
||||||
|
*/
|
||||||
|
private $geoLiteDbUpdater;
|
||||||
|
/**
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
private $translator;
|
||||||
|
|
||||||
|
public function __construct(DbUpdaterInterface $geoLiteDbUpdater, TranslatorInterface $translator)
|
||||||
|
{
|
||||||
|
$this->geoLiteDbUpdater = $geoLiteDbUpdater;
|
||||||
|
$this->translator = $translator;
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName(self::NAME)
|
||||||
|
->setDescription(
|
||||||
|
$this->translator->translate('Updates the GeoLite2 database file used to geolocate IP addresses')
|
||||||
|
)
|
||||||
|
->setHelp($this->translator->translate(
|
||||||
|
'The GeoLite2 database is updated first Tuesday every month, so this command should be ideally run '
|
||||||
|
. 'every first Wednesday'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||||
|
{
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$progressBar = new ProgressBar($output);
|
||||||
|
$progressBar->start();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->geoLiteDbUpdater->downloadFreshCopy(function (int $total, int $downloaded) use ($progressBar) {
|
||||||
|
$progressBar->setMaxSteps($total);
|
||||||
|
$progressBar->setProgress($downloaded);
|
||||||
|
});
|
||||||
|
|
||||||
|
$progressBar->finish();
|
||||||
|
$io->writeln('');
|
||||||
|
|
||||||
|
$io->success($this->translator->translate('GeoLite2 database properly updated'));
|
||||||
|
} catch (RuntimeException $e) {
|
||||||
|
$progressBar->finish();
|
||||||
|
$io->writeln('');
|
||||||
|
|
||||||
|
$io->error($this->translator->translate('An error occurred while updating GeoLite2 database'));
|
||||||
|
if ($io->isVerbose()) {
|
||||||
|
$this->getApplication()->renderException($e, $output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
|
||||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
class ApplicationConfigCustomizer implements ConfigCustomizerInterface
|
|
||||||
{
|
|
||||||
use StringUtilsTrait;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param SymfonyStyle $io
|
|
||||||
* @param CustomizableAppConfig $appConfig
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
|
||||||
{
|
|
||||||
$io->title('APPLICATION');
|
|
||||||
|
|
||||||
if ($appConfig->hasApp() && $io->confirm('Do you want to keep imported application config?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$validator = function ($value) {
|
|
||||||
return $value;
|
|
||||||
};
|
|
||||||
$appConfig->setApp([
|
|
||||||
'SECRET' => $io->ask(
|
|
||||||
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
|
|
||||||
null,
|
|
||||||
$validator
|
|
||||||
) ?: $this->generateRandomString(32),
|
|
||||||
'DISABLE_TRACK_PARAM' => $io->ask(
|
|
||||||
'Provide a parameter name that you will be able to use to disable tracking on specific request to '
|
|
||||||
. 'short URLs (leave empty and this feature won\'t be enabled)',
|
|
||||||
null,
|
|
||||||
$validator
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
interface ConfigCustomizerInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param SymfonyStyle $io
|
|
||||||
* @param CustomizableAppConfig $appConfig
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig);
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
use Symfony\Component\Filesystem\Exception\IOException;
|
|
||||||
use Symfony\Component\Filesystem\Filesystem;
|
|
||||||
|
|
||||||
class DatabaseConfigCustomizer implements ConfigCustomizerInterface
|
|
||||||
{
|
|
||||||
const DATABASE_DRIVERS = [
|
|
||||||
'MySQL' => 'pdo_mysql',
|
|
||||||
'PostgreSQL' => 'pdo_pgsql',
|
|
||||||
'SQLite' => 'pdo_sqlite',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var Filesystem
|
|
||||||
*/
|
|
||||||
private $filesystem;
|
|
||||||
|
|
||||||
public function __construct(Filesystem $filesystem)
|
|
||||||
{
|
|
||||||
$this->filesystem = $filesystem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param SymfonyStyle $io
|
|
||||||
* @param CustomizableAppConfig $appConfig
|
|
||||||
* @return void
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
|
||||||
{
|
|
||||||
$io->title('DATABASE');
|
|
||||||
|
|
||||||
if ($appConfig->hasDatabase() && $io->confirm('Do you want to keep imported database config?')) {
|
|
||||||
// If the user selected to keep DB config and is configured to use sqlite, copy DB file
|
|
||||||
if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) {
|
|
||||||
try {
|
|
||||||
$this->filesystem->copy(
|
|
||||||
$appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH,
|
|
||||||
CustomizableAppConfig::SQLITE_DB_PATH
|
|
||||||
);
|
|
||||||
} catch (IOException $e) {
|
|
||||||
$io->error('It wasn\'t possible to import the SQLite database');
|
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select database type
|
|
||||||
$params = [];
|
|
||||||
$databases = \array_keys(self::DATABASE_DRIVERS);
|
|
||||||
$dbType = $io->choice('Select database type', $databases, $databases[0]);
|
|
||||||
$params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
|
|
||||||
|
|
||||||
// Ask for connection params if database is not SQLite
|
|
||||||
if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
|
|
||||||
$params['NAME'] = $io->ask('Database name', 'shlink');
|
|
||||||
$params['USER'] = $io->ask('Database username');
|
|
||||||
$params['PASSWORD'] = $io->ask('Database password');
|
|
||||||
$params['HOST'] = $io->ask('Database host', 'localhost');
|
|
||||||
$params['PORT'] = $io->ask('Database port', $this->getDefaultDbPort($params['DRIVER']));
|
|
||||||
}
|
|
||||||
|
|
||||||
$appConfig->setDatabase($params);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getDefaultDbPort(string $driver): string
|
|
||||||
{
|
|
||||||
return $driver === 'pdo_mysql' ? '3306' : '5432';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
class LanguageConfigCustomizer implements ConfigCustomizerInterface
|
|
||||||
{
|
|
||||||
const SUPPORTED_LANGUAGES = ['en', 'es'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param SymfonyStyle $io
|
|
||||||
* @param CustomizableAppConfig $appConfig
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
|
||||||
{
|
|
||||||
$io->title('LANGUAGE');
|
|
||||||
|
|
||||||
if ($appConfig->hasLanguage() && $io->confirm('Do you want to keep imported language?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$appConfig->setLanguage([
|
|
||||||
'DEFAULT' => $this->chooseLanguage('Select default language for the application in general', $io),
|
|
||||||
'CLI' => $this->chooseLanguage('Select default language for CLI executions', $io),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private function chooseLanguage(string $message, SymfonyStyle $io): string
|
|
||||||
{
|
|
||||||
return $io->choice($message, self::SUPPORTED_LANGUAGES, self::SUPPORTED_LANGUAGES[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
class UrlShortenerConfigCustomizer implements ConfigCustomizerInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param SymfonyStyle $io
|
|
||||||
* @param CustomizableAppConfig $appConfig
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
|
||||||
{
|
|
||||||
$io->title('URL SHORTENER');
|
|
||||||
|
|
||||||
if ($appConfig->hasUrlShortener() && $io->confirm('Do you want to keep imported URL shortener config?')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask for URL shortener params
|
|
||||||
$appConfig->setUrlShortener([
|
|
||||||
'SCHEMA' => $io->choice(
|
|
||||||
'Select schema for generated short URLs',
|
|
||||||
['http', 'https'],
|
|
||||||
'http'
|
|
||||||
),
|
|
||||||
'HOSTNAME' => $io->ask('Hostname for generated URLs'),
|
|
||||||
'CHARS' => $io->ask(
|
|
||||||
'Character set for generated short codes (leave empty to autogenerate one)',
|
|
||||||
null,
|
|
||||||
function ($value) {
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
) ?: \str_shuffle(UrlShortener::DEFAULT_CHARS),
|
|
||||||
'VALIDATE_URL' => $io->confirm('Do you want to validate long urls by 200 HTTP status code on response'),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,270 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Model;
|
|
||||||
|
|
||||||
use Zend\Stdlib\ArraySerializableInterface;
|
|
||||||
|
|
||||||
final class CustomizableAppConfig implements ArraySerializableInterface
|
|
||||||
{
|
|
||||||
const SQLITE_DB_PATH = 'data/database.sqlite';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $database;
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $urlShortener;
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $language;
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $app;
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
private $importedInstallationPath;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getDatabase()
|
|
||||||
{
|
|
||||||
return $this->database;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $database
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setDatabase(array $database)
|
|
||||||
{
|
|
||||||
$this->database = $database;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function hasDatabase()
|
|
||||||
{
|
|
||||||
return ! empty($this->database);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getUrlShortener()
|
|
||||||
{
|
|
||||||
return $this->urlShortener;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $urlShortener
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setUrlShortener(array $urlShortener)
|
|
||||||
{
|
|
||||||
$this->urlShortener = $urlShortener;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function hasUrlShortener()
|
|
||||||
{
|
|
||||||
return ! empty($this->urlShortener);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getLanguage()
|
|
||||||
{
|
|
||||||
return $this->language;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $language
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setLanguage(array $language)
|
|
||||||
{
|
|
||||||
$this->language = $language;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function hasLanguage()
|
|
||||||
{
|
|
||||||
return ! empty($this->language);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getApp()
|
|
||||||
{
|
|
||||||
return $this->app;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $app
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setApp(array $app)
|
|
||||||
{
|
|
||||||
$this->app = $app;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function hasApp()
|
|
||||||
{
|
|
||||||
return ! empty($this->app);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getImportedInstallationPath()
|
|
||||||
{
|
|
||||||
return $this->importedInstallationPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $importedInstallationPath
|
|
||||||
* @return $this|self
|
|
||||||
*/
|
|
||||||
public function setImportedInstallationPath($importedInstallationPath)
|
|
||||||
{
|
|
||||||
$this->importedInstallationPath = $importedInstallationPath;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function hasImportedInstallationPath()
|
|
||||||
{
|
|
||||||
return $this->importedInstallationPath !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exchange internal values from provided array
|
|
||||||
*
|
|
||||||
* @param array $array
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function exchangeArray(array $array)
|
|
||||||
{
|
|
||||||
if (isset($array['app_options'], $array['app_options']['secret_key'])) {
|
|
||||||
$this->setApp([
|
|
||||||
'SECRET' => $array['app_options']['secret_key'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($array['entity_manager'], $array['entity_manager']['connection'])) {
|
|
||||||
$this->deserializeDatabase($array['entity_manager']['connection']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($array['translator'], $array['translator']['locale'], $array['cli'], $array['cli']['locale'])) {
|
|
||||||
$this->setLanguage([
|
|
||||||
'DEFAULT' => $array['translator']['locale'],
|
|
||||||
'CLI' => $array['cli']['locale'],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($array['url_shortener'])) {
|
|
||||||
$urlShortener = $array['url_shortener'];
|
|
||||||
$this->setUrlShortener([
|
|
||||||
'SCHEMA' => $urlShortener['domain']['schema'],
|
|
||||||
'HOSTNAME' => $urlShortener['domain']['hostname'],
|
|
||||||
'CHARS' => $urlShortener['shortcode_chars'],
|
|
||||||
'VALIDATE_URL' => $urlShortener['validate_url'] ?? true,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private function deserializeDatabase(array $conn)
|
|
||||||
{
|
|
||||||
if (! isset($conn['driver'])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$driver = $conn['driver'];
|
|
||||||
|
|
||||||
$params = ['DRIVER' => $driver];
|
|
||||||
if ($driver !== 'pdo_sqlite') {
|
|
||||||
$params['USER'] = $conn['user'];
|
|
||||||
$params['PASSWORD'] = $conn['password'];
|
|
||||||
$params['NAME'] = $conn['dbname'];
|
|
||||||
$params['HOST'] = $conn['host'];
|
|
||||||
$params['PORT'] = $conn['port'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setDatabase($params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return an array representation of the object
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getArrayCopy()
|
|
||||||
{
|
|
||||||
$config = [
|
|
||||||
'app_options' => [
|
|
||||||
'secret_key' => $this->app['SECRET'],
|
|
||||||
'disable_track_param' => $this->app['DISABLE_TRACK_PARAM'] ?? null,
|
|
||||||
],
|
|
||||||
'entity_manager' => [
|
|
||||||
'connection' => [
|
|
||||||
'driver' => $this->database['DRIVER'],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'translator' => [
|
|
||||||
'locale' => $this->language['DEFAULT'],
|
|
||||||
],
|
|
||||||
'cli' => [
|
|
||||||
'locale' => $this->language['CLI'],
|
|
||||||
],
|
|
||||||
'url_shortener' => [
|
|
||||||
'domain' => [
|
|
||||||
'schema' => $this->urlShortener['SCHEMA'],
|
|
||||||
'hostname' => $this->urlShortener['HOSTNAME'],
|
|
||||||
],
|
|
||||||
'shortcode_chars' => $this->urlShortener['CHARS'],
|
|
||||||
'validate_url' => $this->urlShortener['VALIDATE_URL'],
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
// Build dynamic database config based on selected driver
|
|
||||||
if ($this->database['DRIVER'] === 'pdo_sqlite') {
|
|
||||||
$config['entity_manager']['connection']['path'] = self::SQLITE_DB_PATH;
|
|
||||||
} else {
|
|
||||||
$config['entity_manager']['connection']['user'] = $this->database['USER'];
|
|
||||||
$config['entity_manager']['connection']['password'] = $this->database['PASSWORD'];
|
|
||||||
$config['entity_manager']['connection']['dbname'] = $this->database['NAME'];
|
|
||||||
$config['entity_manager']['connection']['host'] = $this->database['HOST'];
|
|
||||||
$config['entity_manager']['connection']['port'] = $this->database['PORT'];
|
|
||||||
|
|
||||||
if ($this->database['DRIVER'] === 'pdo_mysql') {
|
|
||||||
$config['entity_manager']['connection']['driverOptions'] = [
|
|
||||||
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $config;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,7 @@ class DisableKeyCommandTest extends TestCase
|
|||||||
public function providedApiKeyIsDisabled()
|
public function providedApiKeyIsDisabled()
|
||||||
{
|
{
|
||||||
$apiKey = 'abcd1234';
|
$apiKey = 'abcd1234';
|
||||||
$this->apiKeyService->disable($apiKey)->shouldBeCalledTimes(1);
|
$this->apiKeyService->disable($apiKey)->shouldBeCalledOnce();
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'api-key:disable',
|
'command' => 'api-key:disable',
|
||||||
'apiKey' => $apiKey,
|
'apiKey' => $apiKey,
|
||||||
@@ -52,7 +52,7 @@ class DisableKeyCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$apiKey = 'abcd1234';
|
$apiKey = 'abcd1234';
|
||||||
$this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class)
|
$this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class)
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'api-key:disable',
|
'command' => 'api-key:disable',
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
@@ -38,7 +39,7 @@ class GenerateKeyCommandTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function noExpirationDateIsDefinedIfNotProvided()
|
public function noExpirationDateIsDefinedIfNotProvided()
|
||||||
{
|
{
|
||||||
$this->apiKeyService->create(null)->shouldBeCalledTimes(1)
|
$this->apiKeyService->create(null)->shouldBeCalledOnce()
|
||||||
->willReturn(new ApiKey());
|
->willReturn(new ApiKey());
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'api-key:generate',
|
'command' => 'api-key:generate',
|
||||||
@@ -50,8 +51,8 @@ class GenerateKeyCommandTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function expirationDateIsDefinedIfProvided()
|
public function expirationDateIsDefinedIfProvided()
|
||||||
{
|
{
|
||||||
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1)
|
$this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledOnce()
|
||||||
->willReturn(new ApiKey());
|
->willReturn(new ApiKey());
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'api-key:generate',
|
'command' => 'api-key:generate',
|
||||||
'--expirationDate' => '2016-01-01',
|
'--expirationDate' => '2016-01-01',
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class ListKeysCommandTest extends TestCase
|
|||||||
new ApiKey(),
|
new ApiKey(),
|
||||||
new ApiKey(),
|
new ApiKey(),
|
||||||
new ApiKey(),
|
new ApiKey(),
|
||||||
])->shouldBeCalledTimes(1);
|
])->shouldBeCalledOnce();
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'api-key:list',
|
'command' => 'api-key:list',
|
||||||
]);
|
]);
|
||||||
@@ -55,7 +55,7 @@ class ListKeysCommandTest extends TestCase
|
|||||||
$this->apiKeyService->listKeys(true)->willReturn([
|
$this->apiKeyService->listKeys(true)->willReturn([
|
||||||
new ApiKey(),
|
new ApiKey(),
|
||||||
new ApiKey(),
|
new ApiKey(),
|
||||||
])->shouldBeCalledTimes(1);
|
])->shouldBeCalledOnce();
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'api-key:list',
|
'command' => 'api-key:list',
|
||||||
'--enabledOnly' => true,
|
'--enabledOnly' => true,
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
|
|||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
|
use function implode;
|
||||||
|
use function sort;
|
||||||
|
use function str_split;
|
||||||
|
|
||||||
class GenerateCharsetCommandTest extends TestCase
|
class GenerateCharsetCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Shortcode\DeleteShortCodeCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
||||||
use Shlinkio\Shlink\Core\Exception;
|
use Shlinkio\Shlink\Core\Exception;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
|
use function array_pop;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class DeleteShortCodeCommandTest extends TestCase
|
class DeleteShortCodeCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -28,7 +30,7 @@ class DeleteShortCodeCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
|
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
|
||||||
|
|
||||||
$command = new DeleteShortCodeCommand($this->service->reveal(), Translator::factory([]));
|
$command = new DeleteShortUrlCommand($this->service->reveal(), Translator::factory([]));
|
||||||
$app = new Application();
|
$app = new Application();
|
||||||
$app->add($command);
|
$app->add($command);
|
||||||
|
|
||||||
@@ -47,8 +49,8 @@ class DeleteShortCodeCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertContains(\sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output);
|
$this->assertContains(sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output);
|
||||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,8 +66,8 @@ class DeleteShortCodeCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertContains(\sprintf('Provided short code "%s" could not be found.', $shortCode), $output);
|
$this->assertContains(sprintf('Provided short code "%s" could not be found.', $shortCode), $output);
|
||||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,7 +78,7 @@ class DeleteShortCodeCommandTest extends TestCase
|
|||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, Argument::type('bool'))->will(
|
$deleteByShortCode = $this->service->deleteByShortCode($shortCode, Argument::type('bool'))->will(
|
||||||
function (array $args) {
|
function (array $args) {
|
||||||
$ignoreThreshold = \array_pop($args);
|
$ignoreThreshold = array_pop($args);
|
||||||
|
|
||||||
if (!$ignoreThreshold) {
|
if (!$ignoreThreshold) {
|
||||||
throw new Exception\DeleteShortUrlException(10);
|
throw new Exception\DeleteShortUrlException(10);
|
||||||
@@ -88,11 +90,11 @@ class DeleteShortCodeCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertContains(\sprintf(
|
$this->assertContains(sprintf(
|
||||||
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.',
|
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.',
|
||||||
$shortCode
|
$shortCode
|
||||||
), $output);
|
), $output);
|
||||||
$this->assertContains(\sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output);
|
$this->assertContains(sprintf('Short URL with short code "%s" successfully deleted.', $shortCode), $output);
|
||||||
$deleteByShortCode->shouldHaveBeenCalledTimes(2);
|
$deleteByShortCode->shouldHaveBeenCalledTimes(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,11 +112,11 @@ class DeleteShortCodeCommandTest extends TestCase
|
|||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
$this->assertContains(\sprintf(
|
$this->assertContains(sprintf(
|
||||||
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.',
|
'It was not possible to delete the short URL with short code "%s" because it has more than 10 visits.',
|
||||||
$shortCode
|
$shortCode
|
||||||
), $output);
|
), $output);
|
||||||
$this->assertContains('Short URL was not deleted.', $output);
|
$this->assertContains('Short URL was not deleted.', $output);
|
||||||
$deleteByShortCode->shouldHaveBeenCalledTimes(1);
|
$deleteByShortCode->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Shortcode\GeneratePreviewCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\GeneratePreviewCommand;
|
||||||
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
@@ -16,6 +16,8 @@ use Symfony\Component\Console\Tester\CommandTester;
|
|||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||||
use Zend\Paginator\Paginator;
|
use Zend\Paginator\Paginator;
|
||||||
|
use function count;
|
||||||
|
use function substr_count;
|
||||||
|
|
||||||
class GeneratePreviewCommandTest extends TestCase
|
class GeneratePreviewCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -54,15 +56,15 @@ class GeneratePreviewCommandTest extends TestCase
|
|||||||
public function previewsForEveryUrlAreGenerated()
|
public function previewsForEveryUrlAreGenerated()
|
||||||
{
|
{
|
||||||
$paginator = $this->createPaginator([
|
$paginator = $this->createPaginator([
|
||||||
(new ShortUrl())->setOriginalUrl('http://foo.com'),
|
new ShortUrl('http://foo.com'),
|
||||||
(new ShortUrl())->setOriginalUrl('https://bar.com'),
|
new ShortUrl('https://bar.com'),
|
||||||
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
|
new ShortUrl('http://baz.com/something'),
|
||||||
]);
|
]);
|
||||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
|
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->previewGenerator->generatePreview('http://foo.com')->shouldBeCalledTimes(1);
|
$this->previewGenerator->generatePreview('http://foo.com')->shouldBeCalledOnce();
|
||||||
$this->previewGenerator->generatePreview('https://bar.com')->shouldBeCalledTimes(1);
|
$this->previewGenerator->generatePreview('https://bar.com')->shouldBeCalledOnce();
|
||||||
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledTimes(1);
|
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:process-previews',
|
'command' => 'shortcode:process-previews',
|
||||||
@@ -75,12 +77,12 @@ class GeneratePreviewCommandTest extends TestCase
|
|||||||
public function exceptionWillOutputError()
|
public function exceptionWillOutputError()
|
||||||
{
|
{
|
||||||
$items = [
|
$items = [
|
||||||
(new ShortUrl())->setOriginalUrl('http://foo.com'),
|
new ShortUrl('http://foo.com'),
|
||||||
(new ShortUrl())->setOriginalUrl('https://bar.com'),
|
new ShortUrl('https://bar.com'),
|
||||||
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
|
new ShortUrl('http://baz.com/something'),
|
||||||
];
|
];
|
||||||
$paginator = $this->createPaginator($items);
|
$paginator = $this->createPaginator($items);
|
||||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
|
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledOnce();
|
||||||
$this->previewGenerator->generatePreview(Argument::any())->willThrow(PreviewGenerationException::class)
|
$this->previewGenerator->generatePreview(Argument::any())->willThrow(PreviewGenerationException::class)
|
||||||
->shouldBeCalledTimes(count($items));
|
->shouldBeCalledTimes(count($items));
|
||||||
|
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Shortcode\GenerateShortcodeCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
|
use function strpos;
|
||||||
|
|
||||||
class GenerateShortcodeCommandTest extends TestCase
|
class GenerateShortcodeCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -28,7 +29,7 @@ class GenerateShortcodeCommandTest extends TestCase
|
|||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||||
$command = new GenerateShortcodeCommand($this->urlShortener->reveal(), Translator::factory([]), [
|
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), Translator::factory([]), [
|
||||||
'schema' => 'http',
|
'schema' => 'http',
|
||||||
'hostname' => 'foo.com',
|
'hostname' => 'foo.com',
|
||||||
]);
|
]);
|
||||||
@@ -44,10 +45,9 @@ class GenerateShortcodeCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$this->urlShortener->urlToShortCode(Argument::cetera())
|
$this->urlShortener->urlToShortCode(Argument::cetera())
|
||||||
->willReturn(
|
->willReturn(
|
||||||
(new ShortUrl())->setShortCode('abc123')
|
(new ShortUrl(''))->setShortCode('abc123')
|
||||||
->setLongUrl('')
|
|
||||||
)
|
)
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:generate',
|
'command' => 'shortcode:generate',
|
||||||
@@ -63,7 +63,7 @@ class GenerateShortcodeCommandTest extends TestCase
|
|||||||
public function exceptionWhileParsingLongUrlOutputsError()
|
public function exceptionWhileParsingLongUrlOutputsError()
|
||||||
{
|
{
|
||||||
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
|
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:generate',
|
'command' => 'shortcode:generate',
|
||||||
@@ -1,19 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use Cake\Chronos\Chronos;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Shortcode\GetVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
|
use function strpos;
|
||||||
|
|
||||||
class GetVisitsCommandTest extends TestCase
|
class GetVisitsCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -42,7 +46,7 @@ class GetVisitsCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([])
|
$this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([])
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:visits',
|
'command' => 'shortcode:visits',
|
||||||
@@ -58,9 +62,9 @@ class GetVisitsCommandTest extends TestCase
|
|||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$startDate = '2016-01-01';
|
$startDate = '2016-01-01';
|
||||||
$endDate = '2016-02-01';
|
$endDate = '2016-02-01';
|
||||||
$this->visitsTracker->info($shortCode, new DateRange(new \DateTime($startDate), new \DateTime($endDate)))
|
$this->visitsTracker->info($shortCode, new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
|
||||||
->willReturn([])
|
->willReturn([])
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:visits',
|
'command' => 'shortcode:visits',
|
||||||
@@ -77,18 +81,18 @@ class GetVisitsCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$this->visitsTracker->info($shortCode, Argument::any())->willReturn([
|
$this->visitsTracker->info($shortCode, Argument::any())->willReturn([
|
||||||
(new Visit())->setReferer('foo')
|
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->setVisitLocation(
|
||||||
->setVisitLocation((new VisitLocation())->setCountryName('Spain'))
|
new VisitLocation(['country_name' => 'Spain'])
|
||||||
->setUserAgent('bar'),
|
),
|
||||||
])->shouldBeCalledTimes(1);
|
])->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:visits',
|
'command' => 'shortcode:visits',
|
||||||
'shortCode' => $shortCode,
|
'shortCode' => $shortCode,
|
||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
$this->assertGreaterThan(0, \strpos($output, 'foo'));
|
$this->assertGreaterThan(0, strpos($output, 'foo'));
|
||||||
$this->assertGreaterThan(0, \strpos($output, 'Spain'));
|
$this->assertGreaterThan(0, strpos($output, 'Spain'));
|
||||||
$this->assertGreaterThan(0, \strpos($output, 'bar'));
|
$this->assertGreaterThan(0, strpos($output, 'bar'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Shortcode\ListShortcodesCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
@@ -30,7 +30,7 @@ class ListShortcodesCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
|
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
|
||||||
$app = new Application();
|
$app = new Application();
|
||||||
$command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([]), []);
|
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), Translator::factory([]), []);
|
||||||
$app->add($command);
|
$app->add($command);
|
||||||
$this->commandTester = new CommandTester($command);
|
$this->commandTester = new CommandTester($command);
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ class ListShortcodesCommandTest extends TestCase
|
|||||||
public function noInputCallsListJustOnce()
|
public function noInputCallsListJustOnce()
|
||||||
{
|
{
|
||||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->setInputs(['n']);
|
$this->commandTester->setInputs(['n']);
|
||||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||||
@@ -55,7 +55,7 @@ class ListShortcodesCommandTest extends TestCase
|
|||||||
// The paginator will return more than one page for the first 3 times
|
// The paginator will return more than one page for the first 3 times
|
||||||
$data = [];
|
$data = [];
|
||||||
for ($i = 0; $i < 50; $i++) {
|
for ($i = 0; $i < 50; $i++) {
|
||||||
$data[] = (new ShortUrl())->setLongUrl('url_' . $i);
|
$data[] = new ShortUrl('url_' . $i);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->shortUrlService->listShortUrls(Argument::cetera())->will(function () use (&$data) {
|
$this->shortUrlService->listShortUrls(Argument::cetera())->will(function () use (&$data) {
|
||||||
@@ -74,11 +74,11 @@ class ListShortcodesCommandTest extends TestCase
|
|||||||
// The paginator will return more than one page
|
// The paginator will return more than one page
|
||||||
$data = [];
|
$data = [];
|
||||||
for ($i = 0; $i < 30; $i++) {
|
for ($i = 0; $i < 30; $i++) {
|
||||||
$data[] = (new ShortUrl())->setLongUrl('url_' . $i);
|
$data[] = new ShortUrl('url_' . $i);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data)))
|
$this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data)))
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->setInputs(['n']);
|
$this->commandTester->setInputs(['n']);
|
||||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||||
@@ -91,7 +91,7 @@ class ListShortcodesCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$page = 5;
|
$page = 5;
|
||||||
$this->shortUrlService->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
$this->shortUrlService->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->setInputs(['y']);
|
$this->commandTester->setInputs(['y']);
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
@@ -106,7 +106,7 @@ class ListShortcodesCommandTest extends TestCase
|
|||||||
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
|
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
|
||||||
{
|
{
|
||||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->setInputs(['y']);
|
$this->commandTester->setInputs(['y']);
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
@@ -13,6 +13,7 @@ use Shlinkio\Shlink\Core\Service\UrlShortener;
|
|||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
|
use const PHP_EOL;
|
||||||
|
|
||||||
class ResolveUrlCommandTest extends TestCase
|
class ResolveUrlCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
@@ -42,9 +43,9 @@ class ResolveUrlCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$expectedUrl = 'http://domain.com/foo/bar';
|
$expectedUrl = 'http://domain.com/foo/bar';
|
||||||
$shortUrl = (new ShortUrl())->setLongUrl($expectedUrl);
|
$shortUrl = new ShortUrl($expectedUrl);
|
||||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)
|
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:parse',
|
'command' => 'shortcode:parse',
|
||||||
@@ -61,7 +62,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
|
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:parse',
|
'command' => 'shortcode:parse',
|
||||||
@@ -78,7 +79,7 @@ class ResolveUrlCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(new InvalidShortCodeException())
|
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(new InvalidShortCodeException())
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:parse',
|
'command' => 'shortcode:parse',
|
||||||
@@ -61,8 +61,8 @@ class ListTagsCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
/** @var MethodProphecy $listTags */
|
/** @var MethodProphecy $listTags */
|
||||||
$listTags = $this->tagService->listTags()->willReturn([
|
$listTags = $this->tagService->listTags()->willReturn([
|
||||||
(new Tag())->setName('foo'),
|
new Tag('foo'),
|
||||||
(new Tag())->setName('bar'),
|
new Tag('bar'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$this->commandTester->execute([]);
|
$this->commandTester->execute([]);
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class RenameTagCommandTest extends TestCase
|
|||||||
$oldName = 'foo';
|
$oldName = 'foo';
|
||||||
$newName = 'bar';
|
$newName = 'bar';
|
||||||
/** @var MethodProphecy $renameTag */
|
/** @var MethodProphecy $renameTag */
|
||||||
$renameTag = $this->tagService->renameTag($oldName, $newName)->willReturn(new Tag());
|
$renameTag = $this->tagService->renameTag($oldName, $newName)->willReturn(new Tag($newName));
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'oldName' => $oldName,
|
'oldName' => $oldName,
|
||||||
|
|||||||
@@ -7,33 +7,36 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
||||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
|
use function count;
|
||||||
|
|
||||||
class ProcessVisitsCommandTest extends TestCase
|
class ProcessVisitsCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var CommandTester
|
* @var CommandTester
|
||||||
*/
|
*/
|
||||||
protected $commandTester;
|
private $commandTester;
|
||||||
/**
|
/**
|
||||||
* @var ObjectProphecy
|
* @var ObjectProphecy
|
||||||
*/
|
*/
|
||||||
protected $visitService;
|
private $visitService;
|
||||||
/**
|
/**
|
||||||
* @var ObjectProphecy
|
* @var ObjectProphecy
|
||||||
*/
|
*/
|
||||||
protected $ipResolver;
|
private $ipResolver;
|
||||||
|
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
$this->visitService = $this->prophesize(VisitService::class);
|
$this->visitService = $this->prophesize(VisitService::class);
|
||||||
$this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
|
$this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
|
||||||
$this->ipResolver->getApiLimit()->willReturn(10000000000);
|
|
||||||
|
|
||||||
$command = new ProcessVisitsCommand(
|
$command = new ProcessVisitsCommand(
|
||||||
$this->visitService->reveal(),
|
$this->visitService->reveal(),
|
||||||
@@ -51,13 +54,15 @@ class ProcessVisitsCommandTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function allReturnedVisitsIpsAreProcessed()
|
public function allReturnedVisitsIpsAreProcessed()
|
||||||
{
|
{
|
||||||
|
$shortUrl = new ShortUrl('');
|
||||||
|
|
||||||
$visits = [
|
$visits = [
|
||||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
new Visit($shortUrl, new Visitor('', '', '1.2.3.4')),
|
||||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
new Visit($shortUrl, new Visitor('', '', '4.3.2.1')),
|
||||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
new Visit($shortUrl, new Visitor('', '', '12.34.56.78')),
|
||||||
];
|
];
|
||||||
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits));
|
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits));
|
||||||
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||||
@@ -67,71 +72,39 @@ class ProcessVisitsCommandTest extends TestCase
|
|||||||
'command' => 'visit:process',
|
'command' => 'visit:process',
|
||||||
]);
|
]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
$this->assertEquals(0, \strpos($output, 'Processing IP 1.2.3.0'));
|
$this->assertContains('Processing IP 1.2.3.0', $output);
|
||||||
$this->assertGreaterThan(0, \strpos($output, 'Processing IP 4.3.2.0'));
|
$this->assertContains('Processing IP 4.3.2.0', $output);
|
||||||
$this->assertGreaterThan(0, \strpos($output, 'Processing IP 12.34.56.0'));
|
$this->assertContains('Processing IP 12.34.56.0', $output);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function localhostAddressIsIgnored()
|
public function localhostAndEmptyAddressIsIgnored()
|
||||||
{
|
{
|
||||||
|
$shortUrl = new ShortUrl('');
|
||||||
|
|
||||||
$visits = [
|
$visits = [
|
||||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
new Visit($shortUrl, new Visitor('', '', '1.2.3.4')),
|
||||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
new Visit($shortUrl, new Visitor('', '', '4.3.2.1')),
|
||||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
new Visit($shortUrl, new Visitor('', '', '12.34.56.78')),
|
||||||
(new Visit())->setRemoteAddr('127.0.0.1'),
|
new Visit($shortUrl, new Visitor('', '', '127.0.0.1')),
|
||||||
(new Visit())->setRemoteAddr('127.0.0.1'),
|
new Visit($shortUrl, new Visitor('', '', '127.0.0.1')),
|
||||||
|
new Visit($shortUrl, new Visitor('', '', '')),
|
||||||
|
new Visit($shortUrl, new Visitor('', '', null)),
|
||||||
];
|
];
|
||||||
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(\count($visits) - 2);
|
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits) - 4);
|
||||||
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||||
->shouldBeCalledTimes(\count($visits) - 2);
|
->shouldBeCalledTimes(count($visits) - 4);
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'visit:process',
|
'command' => 'visit:process',
|
||||||
]);
|
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
$this->assertGreaterThan(0, \strpos($output, 'Ignored localhost address'));
|
$this->assertContains('Ignored localhost address', $output);
|
||||||
}
|
$this->assertContains('Ignored visit with no IP address', $output);
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function sleepsEveryTimeTheApiLimitIsReached()
|
|
||||||
{
|
|
||||||
$visits = [
|
|
||||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
|
||||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
|
||||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
|
||||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
|
||||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
|
||||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
|
||||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
|
||||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
|
||||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
|
||||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
|
||||||
];
|
|
||||||
$apiLimit = 3;
|
|
||||||
|
|
||||||
$this->visitService->getUnlocatedVisits()->willReturn($visits);
|
|
||||||
$this->visitService->saveVisit(Argument::any())->will(function () {
|
|
||||||
});
|
|
||||||
|
|
||||||
$getApiLimit = $this->ipResolver->getApiLimit()->willReturn($apiLimit);
|
|
||||||
$getApiInterval = $this->ipResolver->getApiInterval()->willReturn(0);
|
|
||||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
|
||||||
->shouldBeCalledTimes(count($visits));
|
|
||||||
|
|
||||||
$this->commandTester->execute([
|
|
||||||
'command' => 'visit:process',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$getApiLimit->shouldHaveBeenCalledTimes(\count($visits));
|
|
||||||
$getApiInterval->shouldHaveBeenCalledTimes(\round(\count($visits) / $apiLimit));
|
|
||||||
$resolveIpLocation->shouldHaveBeenCalledTimes(\count($visits));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
66
module/CLI/test/Command/Visit/UpdateDbCommandTest.php
Normal file
66
module/CLI/test/Command/Visit/UpdateDbCommandTest.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Visit\UpdateDbCommand;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||||
|
use Shlinkio\Shlink\Common\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
use Zend\I18n\Translator\Translator;
|
||||||
|
|
||||||
|
class UpdateDbCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var CommandTester
|
||||||
|
*/
|
||||||
|
private $commandTester;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
private $dbUpdater;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
||||||
|
|
||||||
|
$command = new UpdateDbCommand($this->dbUpdater->reveal(), Translator::factory([]));
|
||||||
|
$app = new Application();
|
||||||
|
$app->add($command);
|
||||||
|
|
||||||
|
$this->commandTester = new CommandTester($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function successMessageIsPrintedIfEverythingWorks()
|
||||||
|
{
|
||||||
|
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->will(function () {
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->commandTester->execute([]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
$this->assertContains('GeoLite2 database properly updated', $output);
|
||||||
|
$download->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function errorMessageIsPrintedIfAnExceptionIsThrown()
|
||||||
|
{
|
||||||
|
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->willThrow(RuntimeException::class);
|
||||||
|
|
||||||
|
$this->commandTester->execute([]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
$this->assertContains('An error occurred while updating GeoLite2 database', $output);
|
||||||
|
$download->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ use Symfony\Component\Console\Application;
|
|||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
use Zend\ServiceManager\ServiceManager;
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
use function array_merge;
|
||||||
|
|
||||||
class ApplicationFactoryTest extends TestCase
|
class ApplicationFactoryTest extends TestCase
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,91 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Prophecy\Argument;
|
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
|
||||||
use Shlinkio\Shlink\CLI\Install\Plugin\ApplicationConfigCustomizer;
|
|
||||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
class ApplicationConfigCustomizerTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var ApplicationConfigCustomizer
|
|
||||||
*/
|
|
||||||
private $plugin;
|
|
||||||
/**
|
|
||||||
* @var ObjectProphecy
|
|
||||||
*/
|
|
||||||
private $io;
|
|
||||||
|
|
||||||
public function setUp()
|
|
||||||
{
|
|
||||||
$this->io = $this->prophesize(SymfonyStyle::class);
|
|
||||||
$this->io->title(Argument::any())->willReturn(null);
|
|
||||||
|
|
||||||
$this->plugin = new ApplicationConfigCustomizer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function configIsRequestedToTheUser()
|
|
||||||
{
|
|
||||||
$ask = $this->io->ask(Argument::cetera())->willReturn('the_secret');
|
|
||||||
$config = new CustomizableAppConfig();
|
|
||||||
|
|
||||||
$this->plugin->process($this->io->reveal(), $config);
|
|
||||||
|
|
||||||
$this->assertTrue($config->hasApp());
|
|
||||||
$this->assertEquals([
|
|
||||||
'SECRET' => 'the_secret',
|
|
||||||
'DISABLE_TRACK_PARAM' => 'the_secret',
|
|
||||||
], $config->getApp());
|
|
||||||
$ask->shouldHaveBeenCalledTimes(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
|
||||||
{
|
|
||||||
$ask = $this->io->ask(Argument::cetera())->willReturn('the_new_secret');
|
|
||||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
|
||||||
$config = new CustomizableAppConfig();
|
|
||||||
$config->setApp([
|
|
||||||
'SECRET' => 'foo',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->plugin->process($this->io->reveal(), $config);
|
|
||||||
|
|
||||||
$this->assertEquals([
|
|
||||||
'SECRET' => 'the_new_secret',
|
|
||||||
'DISABLE_TRACK_PARAM' => 'the_new_secret',
|
|
||||||
], $config->getApp());
|
|
||||||
$ask->shouldHaveBeenCalledTimes(2);
|
|
||||||
$confirm->shouldHaveBeenCalledTimes(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function existingValueIsKeptIfRequested()
|
|
||||||
{
|
|
||||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
|
||||||
|
|
||||||
$config = new CustomizableAppConfig();
|
|
||||||
$config->setApp([
|
|
||||||
'SECRET' => 'foo',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->plugin->process($this->io->reveal(), $config);
|
|
||||||
|
|
||||||
$this->assertEquals([
|
|
||||||
'SECRET' => 'foo',
|
|
||||||
], $config->getApp());
|
|
||||||
$confirm->shouldHaveBeenCalledTimes(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
<?php
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
|
||||||
use Prophecy\Argument;
|
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
|
||||||
use Shlinkio\Shlink\CLI\Install\Plugin\UrlShortenerConfigCustomizer;
|
|
||||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
|
||||||
|
|
||||||
class UrlShortenerConfigCustomizerTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var UrlShortenerConfigCustomizer
|
|
||||||
*/
|
|
||||||
private $plugin;
|
|
||||||
/**
|
|
||||||
* @var ObjectProphecy
|
|
||||||
*/
|
|
||||||
private $io;
|
|
||||||
|
|
||||||
public function setUp()
|
|
||||||
{
|
|
||||||
$this->io = $this->prophesize(SymfonyStyle::class);
|
|
||||||
$this->io->title(Argument::any())->willReturn(null);
|
|
||||||
$this->plugin = new UrlShortenerConfigCustomizer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function configIsRequestedToTheUser()
|
|
||||||
{
|
|
||||||
$choice = $this->io->choice(Argument::cetera())->willReturn('something');
|
|
||||||
$ask = $this->io->ask(Argument::cetera())->willReturn('something');
|
|
||||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
|
||||||
$config = new CustomizableAppConfig();
|
|
||||||
|
|
||||||
$this->plugin->process($this->io->reveal(), $config);
|
|
||||||
|
|
||||||
$this->assertTrue($config->hasUrlShortener());
|
|
||||||
$this->assertEquals([
|
|
||||||
'SCHEMA' => 'something',
|
|
||||||
'HOSTNAME' => 'something',
|
|
||||||
'CHARS' => 'something',
|
|
||||||
'VALIDATE_URL' => true,
|
|
||||||
], $config->getUrlShortener());
|
|
||||||
$ask->shouldHaveBeenCalledTimes(2);
|
|
||||||
$choice->shouldHaveBeenCalledTimes(1);
|
|
||||||
$confirm->shouldHaveBeenCalledTimes(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
|
||||||
{
|
|
||||||
$choice = $this->io->choice(Argument::cetera())->willReturn('foo');
|
|
||||||
$ask = $this->io->ask(Argument::cetera())->willReturn('foo');
|
|
||||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
|
||||||
$config = new CustomizableAppConfig();
|
|
||||||
$config->setUrlShortener([
|
|
||||||
'SCHEMA' => 'bar',
|
|
||||||
'HOSTNAME' => 'bar',
|
|
||||||
'CHARS' => 'bar',
|
|
||||||
'VALIDATE_URL' => true,
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->plugin->process($this->io->reveal(), $config);
|
|
||||||
|
|
||||||
$this->assertEquals([
|
|
||||||
'SCHEMA' => 'foo',
|
|
||||||
'HOSTNAME' => 'foo',
|
|
||||||
'CHARS' => 'foo',
|
|
||||||
'VALIDATE_URL' => false,
|
|
||||||
], $config->getUrlShortener());
|
|
||||||
$ask->shouldHaveBeenCalledTimes(2);
|
|
||||||
$choice->shouldHaveBeenCalledTimes(1);
|
|
||||||
$confirm->shouldHaveBeenCalledTimes(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function existingValueIsKeptIfRequested()
|
|
||||||
{
|
|
||||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
|
||||||
|
|
||||||
$config = new CustomizableAppConfig();
|
|
||||||
$config->setUrlShortener([
|
|
||||||
'SCHEMA' => 'foo',
|
|
||||||
'HOSTNAME' => 'foo',
|
|
||||||
'CHARS' => 'foo',
|
|
||||||
'VALIDATE_URL' => 'foo',
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->plugin->process($this->io->reveal(), $config);
|
|
||||||
|
|
||||||
$this->assertEquals([
|
|
||||||
'SCHEMA' => 'foo',
|
|
||||||
'HOSTNAME' => 'foo',
|
|
||||||
'CHARS' => 'foo',
|
|
||||||
'VALIDATE_URL' => 'foo',
|
|
||||||
], $config->getUrlShortener());
|
|
||||||
$confirm->shouldHaveBeenCalledTimes(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +1,100 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common;
|
||||||
|
|
||||||
use Doctrine\Common\Cache\Cache;
|
use Doctrine\Common\Cache\Cache;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use GeoIp2\Database\Reader;
|
||||||
|
use GuzzleHttp\Client as GuzzleClient;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Shlinkio\Shlink\Common\Factory;
|
use RKA\Middleware\IpAddress;
|
||||||
use Shlinkio\Shlink\Common\Image;
|
|
||||||
use Shlinkio\Shlink\Common\Image\ImageBuilder;
|
|
||||||
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
|
||||||
use Shlinkio\Shlink\Common\Service;
|
|
||||||
use Shlinkio\Shlink\Common\Template\Extension\TranslatorExtension;
|
|
||||||
use Symfony\Component\Filesystem\Filesystem;
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||||
|
use Zend\ServiceManager\Proxy\LazyServiceFactory;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
EntityManager::class => Factory\EntityManagerFactory::class,
|
EntityManager::class => Factory\EntityManagerFactory::class,
|
||||||
GuzzleHttp\Client::class => InvokableFactory::class,
|
GuzzleClient::class => InvokableFactory::class,
|
||||||
Cache::class => Factory\CacheFactory::class,
|
Cache::class => Factory\CacheFactory::class,
|
||||||
'Logger_Shlink' => Factory\LoggerFactory::class,
|
'Logger_Shlink' => Factory\LoggerFactory::class,
|
||||||
Filesystem::class => InvokableFactory::class,
|
Filesystem::class => InvokableFactory::class,
|
||||||
|
Reader::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Translator::class => Factory\TranslatorFactory::class,
|
Translator::class => Factory\TranslatorFactory::class,
|
||||||
TranslatorExtension::class => ConfigAbstractFactory::class,
|
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
|
||||||
LocaleMiddleware::class => ConfigAbstractFactory::class,
|
|
||||||
|
Middleware\LocaleMiddleware::class => ConfigAbstractFactory::class,
|
||||||
|
IpAddress::class => Middleware\IpAddressMiddlewareFactory::class,
|
||||||
|
|
||||||
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
||||||
|
|
||||||
Service\IpApiLocationResolver::class => ConfigAbstractFactory::class,
|
IpGeolocation\IpApiLocationResolver::class => ConfigAbstractFactory::class,
|
||||||
|
IpGeolocation\GeoLite2LocationResolver::class => ConfigAbstractFactory::class,
|
||||||
|
IpGeolocation\EmptyIpLocationResolver::class => InvokableFactory::class,
|
||||||
|
IpGeolocation\ChainIpLocationResolver::class => ConfigAbstractFactory::class,
|
||||||
|
IpGeolocation\GeoLite2\GeoLite2Options::class => ConfigAbstractFactory::class,
|
||||||
|
IpGeolocation\GeoLite2\DbUpdater::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
|
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
|
||||||
],
|
],
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
'em' => EntityManager::class,
|
'em' => EntityManager::class,
|
||||||
'httpClient' => GuzzleHttp\Client::class,
|
'httpClient' => GuzzleClient::class,
|
||||||
'translator' => Translator::class,
|
'translator' => Translator::class,
|
||||||
|
|
||||||
'logger' => LoggerInterface::class,
|
'logger' => LoggerInterface::class,
|
||||||
Logger::class => 'Logger_Shlink',
|
Logger::class => 'Logger_Shlink',
|
||||||
LoggerInterface::class => 'Logger_Shlink',
|
LoggerInterface::class => 'Logger_Shlink',
|
||||||
|
|
||||||
|
IpGeolocation\IpLocationResolverInterface::class => IpGeolocation\ChainIpLocationResolver::class,
|
||||||
],
|
],
|
||||||
'abstract_factories' => [
|
'abstract_factories' => [
|
||||||
Factory\DottedAccessConfigAbstractFactory::class,
|
Factory\DottedAccessConfigAbstractFactory::class,
|
||||||
],
|
],
|
||||||
|
'delegators' => [
|
||||||
|
// The GeoLite2 db reader has to be lazy so that it does not try to load the DB file at app bootstrapping.
|
||||||
|
// By doing so, it would fail the first time shlink tries to download it.
|
||||||
|
Reader::class => [
|
||||||
|
LazyServiceFactory::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'lazy_services' => [
|
||||||
|
'class_map' => [
|
||||||
|
Reader::class => Reader::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
TranslatorExtension::class => ['translator'],
|
Reader::class => ['config.geolite2.db_location'],
|
||||||
LocaleMiddleware::class => ['translator'],
|
|
||||||
Service\IpApiLocationResolver::class => ['httpClient'],
|
Template\Extension\TranslatorExtension::class => ['translator'],
|
||||||
|
Middleware\LocaleMiddleware::class => ['translator'],
|
||||||
|
|
||||||
|
IpGeolocation\IpApiLocationResolver::class => ['httpClient'],
|
||||||
|
IpGeolocation\GeoLite2LocationResolver::class => [Reader::class],
|
||||||
|
IpGeolocation\ChainIpLocationResolver::class => [
|
||||||
|
IpGeolocation\GeoLite2LocationResolver::class,
|
||||||
|
IpGeolocation\IpApiLocationResolver::class,
|
||||||
|
IpGeolocation\EmptyIpLocationResolver::class,
|
||||||
|
],
|
||||||
|
IpGeolocation\GeoLite2\GeoLite2Options::class => ['config.geolite2'],
|
||||||
|
IpGeolocation\GeoLite2\DbUpdater::class => [
|
||||||
|
GuzzleClient::class,
|
||||||
|
Filesystem::class,
|
||||||
|
IpGeolocation\GeoLite2\GeoLite2Options::class,
|
||||||
|
],
|
||||||
|
|
||||||
Service\PreviewGenerator::class => [
|
Service\PreviewGenerator::class => [
|
||||||
ImageBuilder::class,
|
Image\ImageBuilder::class,
|
||||||
Filesystem::class,
|
Filesystem::class,
|
||||||
'config.preview_generation.files_location',
|
'config.preview_generation.files_location',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Common;
|
namespace Shlinkio\Shlink\Common;
|
||||||
|
|
||||||
|
use const JSON_ERROR_NONE;
|
||||||
use function getenv;
|
use function getenv;
|
||||||
|
use function json_decode as spl_json_decode;
|
||||||
|
use function json_last_error;
|
||||||
|
use function json_last_error_msg;
|
||||||
|
use function sprintf;
|
||||||
use function strtolower;
|
use function strtolower;
|
||||||
use function trim;
|
use function trim;
|
||||||
|
|
||||||
@@ -40,3 +45,16 @@ function env($key, $default = null)
|
|||||||
|
|
||||||
return trim($value);
|
return trim($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws Exception\InvalidArgumentException
|
||||||
|
*/
|
||||||
|
function json_decode(string $json, int $depth = 512, int $options = 0): array
|
||||||
|
{
|
||||||
|
$data = spl_json_decode($json, true, $depth, $options);
|
||||||
|
if (JSON_ERROR_NONE !== json_last_error()) {
|
||||||
|
throw new Exception\InvalidArgumentException(sprintf('Error decoding JSON: %s', json_last_error_msg()));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|||||||
67
module/Common/src/Collection/PathCollection.php
Normal file
67
module/Common/src/Collection/PathCollection.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\Collection;
|
||||||
|
|
||||||
|
use function array_key_exists;
|
||||||
|
use function array_shift;
|
||||||
|
use function is_array;
|
||||||
|
|
||||||
|
final class PathCollection
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $array;
|
||||||
|
|
||||||
|
public function __construct(array $array)
|
||||||
|
{
|
||||||
|
$this->array = $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pathExists(array $path): bool
|
||||||
|
{
|
||||||
|
return $this->checkPathExists($path, $this->array);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkPathExists(array $path, array $array): bool
|
||||||
|
{
|
||||||
|
// As soon as a step is not found, the path does not exist
|
||||||
|
$step = array_shift($path);
|
||||||
|
if (! array_key_exists($step, $array)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Once the path is empty, we have found all the parts in the path
|
||||||
|
if (empty($path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If current value is not an array, then we have not found the path
|
||||||
|
$newArray = $array[$step];
|
||||||
|
if (! is_array($newArray)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->checkPathExists($path, $newArray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getValueInPath(array $path)
|
||||||
|
{
|
||||||
|
$array = $this->array;
|
||||||
|
|
||||||
|
do {
|
||||||
|
$step = array_shift($path);
|
||||||
|
if (! is_array($array) || ! array_key_exists($step, $array)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$array = $array[$step];
|
||||||
|
} while (! empty($path));
|
||||||
|
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Exception;
|
namespace Shlinkio\Shlink\Common\Exception;
|
||||||
|
|
||||||
interface ExceptionInterface extends \Throwable
|
use Throwable;
|
||||||
|
|
||||||
|
interface ExceptionInterface extends Throwable
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Exception;
|
namespace Shlinkio\Shlink\Common\Exception;
|
||||||
|
|
||||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
use InvalidArgumentException as SplInvalidArgumentException;
|
||||||
|
|
||||||
|
class InvalidArgumentException extends SplInvalidArgumentException implements ExceptionInterface
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Exception;
|
namespace Shlinkio\Shlink\Common\Exception;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class PreviewGenerationException extends RuntimeException
|
class PreviewGenerationException extends RuntimeException
|
||||||
{
|
{
|
||||||
public static function fromImageError($error)
|
public static function fromImageError($error)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Exception;
|
namespace Shlinkio\Shlink\Common\Exception;
|
||||||
|
|
||||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
use RuntimeException as SplRuntimeException;
|
||||||
|
|
||||||
|
class RuntimeException extends SplRuntimeException implements ExceptionInterface
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Exception;
|
namespace Shlinkio\Shlink\Common\Exception;
|
||||||
|
|
||||||
|
use Throwable;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class WrongIpException extends RuntimeException
|
class WrongIpException extends RuntimeException
|
||||||
{
|
{
|
||||||
public static function fromIpAddress($ipAddress, \Throwable $prev = null): self
|
public static function fromIpAddress($ipAddress, Throwable $prev = null): self
|
||||||
{
|
{
|
||||||
return new self(\sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
return new self(sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,15 +6,17 @@ namespace Shlinkio\Shlink\Common\Factory;
|
|||||||
use Doctrine\Common\Cache;
|
use Doctrine\Common\Cache;
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
use Interop\Container\Exception\ContainerException;
|
use Interop\Container\Exception\ContainerException;
|
||||||
use Shlinkio\Shlink\Common;
|
use Memcached;
|
||||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||||
|
use function Functional\contains;
|
||||||
|
use function Shlinkio\Shlink\Common\env;
|
||||||
|
|
||||||
class CacheFactory implements FactoryInterface
|
class CacheFactory implements FactoryInterface
|
||||||
{
|
{
|
||||||
const VALID_CACHE_ADAPTERS = [
|
private const VALID_CACHE_ADAPTERS = [
|
||||||
Cache\ApcuCache::class,
|
Cache\ApcuCache::class,
|
||||||
Cache\ArrayCache::class,
|
Cache\ArrayCache::class,
|
||||||
Cache\FilesystemCache::class,
|
Cache\FilesystemCache::class,
|
||||||
@@ -51,14 +53,12 @@ class CacheFactory implements FactoryInterface
|
|||||||
{
|
{
|
||||||
// Try to get the adapter from config
|
// Try to get the adapter from config
|
||||||
$config = $container->get('config');
|
$config = $container->get('config');
|
||||||
if (isset($config['cache'], $config['cache']['adapter'])
|
if (isset($config['cache']['adapter']) && contains(self::VALID_CACHE_ADAPTERS, $config['cache']['adapter'])) {
|
||||||
&& in_array($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS)
|
|
||||||
) {
|
|
||||||
return $this->resolveCacheAdapter($config['cache']);
|
return $this->resolveCacheAdapter($config['cache']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the adapter has not been set in config, create one based on environment
|
// If the adapter has not been set in config, create one based on environment
|
||||||
return Common\env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
return env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,8 +75,8 @@ class CacheFactory implements FactoryInterface
|
|||||||
case Cache\PhpFileCache::class:
|
case Cache\PhpFileCache::class:
|
||||||
return new $cacheConfig['adapter']($cacheConfig['options']['dir']);
|
return new $cacheConfig['adapter']($cacheConfig['options']['dir']);
|
||||||
case Cache\MemcachedCache::class:
|
case Cache\MemcachedCache::class:
|
||||||
$memcached = new \Memcached();
|
$memcached = new Memcached();
|
||||||
$servers = isset($cacheConfig['options']['servers']) ? $cacheConfig['options']['servers'] : [];
|
$servers = $cacheConfig['options']['servers'] ?? [];
|
||||||
|
|
||||||
foreach ($servers as $server) {
|
foreach ($servers as $server) {
|
||||||
if (! isset($server['host'])) {
|
if (! isset($server['host'])) {
|
||||||
|
|||||||
@@ -3,12 +3,18 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Factory;
|
namespace Shlinkio\Shlink\Common\Factory;
|
||||||
|
|
||||||
|
use ArrayAccess;
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
use Interop\Container\Exception\ContainerException;
|
use Interop\Container\Exception\ContainerException;
|
||||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
|
use Zend\ServiceManager\Factory\AbstractFactoryInterface;
|
||||||
|
use function array_shift;
|
||||||
|
use function explode;
|
||||||
|
use function is_array;
|
||||||
|
use function sprintf;
|
||||||
|
use function substr_count;
|
||||||
|
|
||||||
class DottedAccessConfigAbstractFactory implements AbstractFactoryInterface
|
class DottedAccessConfigAbstractFactory implements AbstractFactoryInterface
|
||||||
{
|
{
|
||||||
@@ -72,7 +78,7 @@ class DottedAccessConfigAbstractFactory implements AbstractFactoryInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
$value = $array[$key];
|
$value = $array[$key];
|
||||||
if (! empty($keys) && (is_array($value) || $value instanceof \ArrayAccess)) {
|
if (! empty($keys) && (is_array($value) || $value instanceof ArrayAccess)) {
|
||||||
$value = $this->readKeysFromArray($keys, $value);
|
$value = $this->readKeysFromArray($keys, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,13 @@ namespace Shlinkio\Shlink\Common\Factory;
|
|||||||
|
|
||||||
use Doctrine\Common\Cache\ArrayCache;
|
use Doctrine\Common\Cache\ArrayCache;
|
||||||
use Doctrine\Common\Cache\Cache;
|
use Doctrine\Common\Cache\Cache;
|
||||||
|
use Doctrine\DBAL\DBALException;
|
||||||
|
use Doctrine\DBAL\Types\Type;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use Doctrine\ORM\ORMException;
|
||||||
use Doctrine\ORM\Tools\Setup;
|
use Doctrine\ORM\Tools\Setup;
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
use Interop\Container\Exception\ContainerException;
|
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||||
@@ -16,15 +19,10 @@ use Zend\ServiceManager\Factory\FactoryInterface;
|
|||||||
class EntityManagerFactory implements FactoryInterface
|
class EntityManagerFactory implements FactoryInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Create an object
|
|
||||||
*
|
|
||||||
* @param ContainerInterface $container
|
|
||||||
* @param string $requestedName
|
|
||||||
* @param null|array $options
|
|
||||||
* @return object
|
|
||||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||||
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
||||||
* @throws ContainerException if any other error occurs
|
* @throws ORMException
|
||||||
|
* @throws DBALException
|
||||||
*/
|
*/
|
||||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||||
{
|
{
|
||||||
@@ -35,6 +33,10 @@ class EntityManagerFactory implements FactoryInterface
|
|||||||
$connectionConfig = $emConfig['connection'] ?? [];
|
$connectionConfig = $emConfig['connection'] ?? [];
|
||||||
$ormConfig = $emConfig['orm'] ?? [];
|
$ormConfig = $emConfig['orm'] ?? [];
|
||||||
|
|
||||||
|
if (! Type::hasType(ChronosDateTimeType::CHRONOS_DATETIME)) {
|
||||||
|
Type::addType(ChronosDateTimeType::CHRONOS_DATETIME, ChronosDateTimeType::class);
|
||||||
|
}
|
||||||
|
|
||||||
return EntityManager::create($connectionConfig, Setup::createAnnotationMetadataConfiguration(
|
return EntityManager::create($connectionConfig, Setup::createAnnotationMetadataConfiguration(
|
||||||
$ormConfig['entities_paths'] ?? [],
|
$ormConfig['entities_paths'] ?? [],
|
||||||
$isDevMode,
|
$isDevMode,
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use Interop\Container\Exception\ContainerException;
|
|||||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||||
|
use function count;
|
||||||
|
use function explode;
|
||||||
|
|
||||||
class LoggerFactory implements FactoryInterface
|
class LoggerFactory implements FactoryInterface
|
||||||
{
|
{
|
||||||
|
|||||||
38
module/Common/src/IpGeolocation/ChainIpLocationResolver.php
Normal file
38
module/Common/src/IpGeolocation/ChainIpLocationResolver.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\IpGeolocation;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
|
|
||||||
|
class ChainIpLocationResolver implements IpLocationResolverInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var IpLocationResolverInterface[]
|
||||||
|
*/
|
||||||
|
private $resolvers;
|
||||||
|
|
||||||
|
public function __construct(IpLocationResolverInterface ...$resolvers)
|
||||||
|
{
|
||||||
|
$this->resolvers = $resolvers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws WrongIpException
|
||||||
|
*/
|
||||||
|
public function resolveIpLocation(string $ipAddress): array
|
||||||
|
{
|
||||||
|
$error = null;
|
||||||
|
|
||||||
|
foreach ($this->resolvers as $resolver) {
|
||||||
|
try {
|
||||||
|
return $resolver->resolveIpLocation($ipAddress);
|
||||||
|
} catch (WrongIpException $e) {
|
||||||
|
$error = $e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this instruction is reached, it means no resolver was capable of resolving the address
|
||||||
|
throw WrongIpException::fromIpAddress($ipAddress, $error);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
module/Common/src/IpGeolocation/EmptyIpLocationResolver.php
Normal file
25
module/Common/src/IpGeolocation/EmptyIpLocationResolver.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\IpGeolocation;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
|
|
||||||
|
class EmptyIpLocationResolver implements IpLocationResolverInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws WrongIpException
|
||||||
|
*/
|
||||||
|
public function resolveIpLocation(string $ipAddress): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'country_code' => '',
|
||||||
|
'country_name' => '',
|
||||||
|
'region_name' => '',
|
||||||
|
'city' => '',
|
||||||
|
'latitude' => '',
|
||||||
|
'longitude' => '',
|
||||||
|
'time_zone' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
106
module/Common/src/IpGeolocation/GeoLite2/DbUpdater.php
Normal file
106
module/Common/src/IpGeolocation/GeoLite2/DbUpdater.php
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\IpGeolocation\GeoLite2;
|
||||||
|
|
||||||
|
use Fig\Http\Message\RequestMethodInterface as RequestMethod;
|
||||||
|
use GuzzleHttp\ClientInterface;
|
||||||
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use GuzzleHttp\RequestOptions;
|
||||||
|
use PharData;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||||
|
use Symfony\Component\Filesystem\Exception as FilesystemException;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
use Throwable;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
class DbUpdater implements DbUpdaterInterface
|
||||||
|
{
|
||||||
|
private const DB_COMPRESSED_FILE = 'GeoLite2-City.tar.gz';
|
||||||
|
private const DB_DECOMPRESSED_FILE = 'GeoLite2-City.mmdb';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ClientInterface
|
||||||
|
*/
|
||||||
|
private $httpClient;
|
||||||
|
/**
|
||||||
|
* @var Filesystem
|
||||||
|
*/
|
||||||
|
private $filesystem;
|
||||||
|
/**
|
||||||
|
* @var GeoLite2Options
|
||||||
|
*/
|
||||||
|
private $options;
|
||||||
|
|
||||||
|
public function __construct(ClientInterface $httpClient, Filesystem $filesystem, GeoLite2Options $options)
|
||||||
|
{
|
||||||
|
$this->httpClient = $httpClient;
|
||||||
|
$this->filesystem = $filesystem;
|
||||||
|
$this->options = $options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException
|
||||||
|
*/
|
||||||
|
public function downloadFreshCopy(callable $handleProgress = null): void
|
||||||
|
{
|
||||||
|
$tempDir = $this->options->getTempDir();
|
||||||
|
$compressedFile = sprintf('%s/%s', $tempDir, self::DB_COMPRESSED_FILE);
|
||||||
|
|
||||||
|
$this->downloadDbFile($compressedFile, $handleProgress);
|
||||||
|
$tempFullPath = $this->extractDbFile($compressedFile, $tempDir);
|
||||||
|
$this->copyNewDbFile($tempFullPath);
|
||||||
|
$this->deleteTempFiles([$compressedFile, $tempFullPath]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function downloadDbFile(string $dest, callable $handleProgress = null): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->httpClient->request(RequestMethod::METHOD_GET, $this->options->getDownloadFrom(), [
|
||||||
|
RequestOptions::SINK => $dest,
|
||||||
|
RequestOptions::PROGRESS => $handleProgress,
|
||||||
|
]);
|
||||||
|
} catch (Throwable | GuzzleException $e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
'An error occurred while trying to download a fresh copy of the GeoLite2 database',
|
||||||
|
0,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function extractDbFile(string $compressedFile, string $tempDir): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$phar = new PharData($compressedFile);
|
||||||
|
$internalPathToDb = sprintf('%s/%s', $phar->getBasename(), self::DB_DECOMPRESSED_FILE);
|
||||||
|
$phar->extractTo($tempDir, $internalPathToDb, true);
|
||||||
|
|
||||||
|
return sprintf('%s/%s', $tempDir, $internalPathToDb);
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
sprintf('An error occurred while trying to extract the GeoLite2 database from %s', $compressedFile),
|
||||||
|
0,
|
||||||
|
$e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function copyNewDbFile(string $from): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->filesystem->copy($from, $this->options->getDbLocation(), true);
|
||||||
|
} catch (FilesystemException\FileNotFoundException | FilesystemException\IOException $e) {
|
||||||
|
throw new RuntimeException('An error occurred while trying to copy GeoLite2 db file to destination', 0, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function deleteTempFiles(array $files): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->filesystem->remove($files);
|
||||||
|
} catch (FilesystemException\IOException $e) {
|
||||||
|
// Ignore any error produced when trying to delete temp files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\IpGeolocation\GeoLite2;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||||
|
|
||||||
|
interface DbUpdaterInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws RuntimeException
|
||||||
|
*/
|
||||||
|
public function downloadFreshCopy(callable $handleProgress = null): void;
|
||||||
|
}
|
||||||
46
module/Common/src/IpGeolocation/GeoLite2/GeoLite2Options.php
Normal file
46
module/Common/src/IpGeolocation/GeoLite2/GeoLite2Options.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\IpGeolocation\GeoLite2;
|
||||||
|
|
||||||
|
use Zend\Stdlib\AbstractOptions;
|
||||||
|
|
||||||
|
class GeoLite2Options extends AbstractOptions
|
||||||
|
{
|
||||||
|
private $dbLocation = '';
|
||||||
|
private $tempDir = '';
|
||||||
|
private $downloadFrom = 'http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.tar.gz';
|
||||||
|
|
||||||
|
public function getDbLocation(): string
|
||||||
|
{
|
||||||
|
return $this->dbLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setDbLocation(string $dbLocation): self
|
||||||
|
{
|
||||||
|
$this->dbLocation = $dbLocation;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTempDir(): string
|
||||||
|
{
|
||||||
|
return $this->tempDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setTempDir(string $tempDir): self
|
||||||
|
{
|
||||||
|
$this->tempDir = $tempDir;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDownloadFrom(): string
|
||||||
|
{
|
||||||
|
return $this->downloadFrom;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function setDownloadFrom(string $downloadFrom): self
|
||||||
|
{
|
||||||
|
$this->downloadFrom = $downloadFrom;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
56
module/Common/src/IpGeolocation/GeoLite2LocationResolver.php
Normal file
56
module/Common/src/IpGeolocation/GeoLite2LocationResolver.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\IpGeolocation;
|
||||||
|
|
||||||
|
use GeoIp2\Database\Reader;
|
||||||
|
use GeoIp2\Exception\AddressNotFoundException;
|
||||||
|
use GeoIp2\Model\City;
|
||||||
|
use GeoIp2\Record\Subdivision;
|
||||||
|
use MaxMind\Db\Reader\InvalidDatabaseException;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
|
use function Functional\first;
|
||||||
|
|
||||||
|
class GeoLite2LocationResolver implements IpLocationResolverInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Reader
|
||||||
|
*/
|
||||||
|
private $geoLiteDbReader;
|
||||||
|
|
||||||
|
public function __construct(Reader $geoLiteDbReader)
|
||||||
|
{
|
||||||
|
$this->geoLiteDbReader = $geoLiteDbReader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws WrongIpException
|
||||||
|
*/
|
||||||
|
public function resolveIpLocation(string $ipAddress): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$city = $this->geoLiteDbReader->city($ipAddress);
|
||||||
|
return $this->mapFields($city);
|
||||||
|
} catch (AddressNotFoundException $e) {
|
||||||
|
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||||
|
} catch (InvalidDatabaseException $e) {
|
||||||
|
throw new WrongIpException('Provided GeoLite2 db file is invalid', 0, $e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function mapFields(City $city): array
|
||||||
|
{
|
||||||
|
/** @var Subdivision $region */
|
||||||
|
$region = first($city->subdivisions);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'country_code' => $city->country->isoCode ?? '',
|
||||||
|
'country_name' => $city->country->name ?? '',
|
||||||
|
'region_name' => $region->name ?? '',
|
||||||
|
'city' => $city->city->name ?? '',
|
||||||
|
'latitude' => $city->location->latitude ?? '',
|
||||||
|
'longitude' => $city->location->longitude ?? '',
|
||||||
|
'time_zone' => $city->location->timeZone ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\Common\Service;
|
namespace Shlinkio\Shlink\Common\IpGeolocation;
|
||||||
|
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
|
use function Shlinkio\Shlink\Common\json_decode;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
class IpApiLocationResolver implements IpLocationResolverInterface
|
class IpApiLocationResolver implements IpLocationResolverInterface
|
||||||
{
|
{
|
||||||
@@ -22,17 +25,17 @@ class IpApiLocationResolver implements IpLocationResolverInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $ipAddress
|
|
||||||
* @return array
|
|
||||||
* @throws WrongIpException
|
* @throws WrongIpException
|
||||||
*/
|
*/
|
||||||
public function resolveIpLocation(string $ipAddress): array
|
public function resolveIpLocation(string $ipAddress): array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$response = $this->httpClient->get(\sprintf(self::SERVICE_PATTERN, $ipAddress));
|
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||||
return $this->mapFields(\json_decode((string) $response->getBody(), true));
|
return $this->mapFields(json_decode((string) $response->getBody()));
|
||||||
} catch (GuzzleException $e) {
|
} catch (GuzzleException $e) {
|
||||||
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||||
|
} catch (InvalidArgumentException $e) {
|
||||||
|
throw new WrongIpException('IP-API returned invalid body while locating IP address', 0, $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,24 +51,4 @@ class IpApiLocationResolver implements IpLocationResolverInterface
|
|||||||
'time_zone' => $entry['timezone'] ?? '',
|
'time_zone' => $entry['timezone'] ?? '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the interval in seconds that needs to be waited when the API limit is reached
|
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getApiInterval(): int
|
|
||||||
{
|
|
||||||
return 65; // ip-api interval is 1 minute. Return 5 extra seconds just in case
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the limit of requests that can be performed to the API in a specific interval, or null if no limit exists
|
|
||||||
*
|
|
||||||
* @return int|null
|
|
||||||
*/
|
|
||||||
public function getApiLimit(): ?int
|
|
||||||
{
|
|
||||||
return 145; // ip-api limit is 150 requests per minute. Leave 5 less requests just in case
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\IpGeolocation;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
|
|
||||||
|
interface IpLocationResolverInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @throws WrongIpException
|
||||||
|
*/
|
||||||
|
public function resolveIpLocation(string $ipAddress): array;
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Common\Logger\Processor;
|
||||||
|
|
||||||
|
use const PHP_EOL;
|
||||||
|
use function str_replace;
|
||||||
|
use function strpos;
|
||||||
|
|
||||||
|
final class ExceptionWithNewLineProcessor
|
||||||
|
{
|
||||||
|
private const EXCEPTION_PLACEHOLDER = '{e}';
|
||||||
|
|
||||||
|
public function __invoke(array $record)
|
||||||
|
{
|
||||||
|
$message = $record['message'];
|
||||||
|
$messageHasExceptionPlaceholder = strpos($message, self::EXCEPTION_PLACEHOLDER) !== false;
|
||||||
|
|
||||||
|
if ($messageHasExceptionPlaceholder) {
|
||||||
|
$record['message'] = str_replace(
|
||||||
|
self::EXCEPTION_PLACEHOLDER,
|
||||||
|
PHP_EOL . self::EXCEPTION_PLACEHOLDER,
|
||||||
|
$message
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $record;
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user