mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-05 23:03:11 +08:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ba4d8e947 | ||
|
|
3faf6e967f | ||
|
|
a7a5667301 | ||
|
|
d4924897b2 | ||
|
|
f2d39ca55a | ||
|
|
743d052f55 | ||
|
|
17dbab5ee8 | ||
|
|
8cb5e07c7b | ||
|
|
e9972783d2 | ||
|
|
84f6080a38 | ||
|
|
1b5c1e4e52 | ||
|
|
d7e89ebdae | ||
|
|
aa413dab6d | ||
|
|
b876870bd8 | ||
|
|
05e56cc845 | ||
|
|
d6c158ce98 | ||
|
|
1d4ef4e9a4 | ||
|
|
4d2684be52 | ||
|
|
6947805b5c | ||
|
|
d0e0aea0f1 | ||
|
|
b0f250ed8a | ||
|
|
45254606d4 | ||
|
|
03ee46d903 | ||
|
|
c4afc7a923 | ||
|
|
afa2a5b0f0 | ||
|
|
b40057d423 | ||
|
|
282ffef200 | ||
|
|
22b02de405 | ||
|
|
f0330e9ae3 | ||
|
|
0c26490e3f | ||
|
|
cfaecd93e4 | ||
|
|
ccbc6c7a75 | ||
|
|
2fc2ad98aa | ||
|
|
16590b2dbb | ||
|
|
f40349479e | ||
|
|
9f60c8dffe | ||
|
|
5abd9d1a40 | ||
|
|
0ae5a53d86 | ||
|
|
15a70d0157 | ||
|
|
ededb68ef1 | ||
|
|
09add5fbff | ||
|
|
e30f49a791 | ||
|
|
64737b741b | ||
|
|
d4d65bdf37 | ||
|
|
90732a4fad | ||
|
|
c5015f5828 | ||
|
|
aa77c944d8 | ||
|
|
b8faa6714a | ||
|
|
f48f98f4d7 | ||
|
|
79b2a0839f | ||
|
|
6094d17718 | ||
|
|
d2ed7d6417 | ||
|
|
a705ef21a9 | ||
|
|
67e465c479 | ||
|
|
ed3883b52c | ||
|
|
71ea0bcb5e | ||
|
|
dd2cffeee9 | ||
|
|
1ceabf3bc3 | ||
|
|
17fcd637f2 | ||
|
|
d44bc4b182 | ||
|
|
4760406221 | ||
|
|
0aae0d888c | ||
|
|
1bc01057f3 | ||
|
|
c1906606c6 | ||
|
|
1363194909 | ||
|
|
d945e0c31b |
@@ -18,6 +18,7 @@ matrix:
|
||||
before_install:
|
||||
- echo 'extension = memcached.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
|
||||
- yes | pecl install swoole
|
||||
- phpenv config-rm xdebug.ini || return 0
|
||||
|
||||
install:
|
||||
|
||||
58
CHANGELOG.md
58
CHANGELOG.md
@@ -4,6 +4,64 @@ 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.15.0 - 2018-12-02
|
||||
|
||||
#### Added
|
||||
|
||||
* [#208](https://github.com/shlinkio/shlink/issues/208) Added initial support to run shlink using [swoole](https://www.swoole.co.uk/), a non-blocking IO server which improves the performance of shlink from 4 to 10 times.
|
||||
|
||||
Run shlink with `./vendor/bin/zend-expressive-swoole start` to start-up the service, which will be exposed in port `8080`.
|
||||
|
||||
Adding the `-d` flag, it will be started as a background service. Then you can use the `./vendor/bin/zend-expressive-swoole stop` command in order to stop it.
|
||||
|
||||
* [#266](https://github.com/shlinkio/shlink/issues/266) Added pagination to `GET /short-urls/{shortCode}/visits` endpoint.
|
||||
|
||||
In order to make it backwards compatible, it keeps returning all visits by default, but it now allows to provide the `page` and `itemsPerPage` query parameters in order to configure the number of items to get.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#267](https://github.com/shlinkio/shlink/issues/267) API responses and the CLI interface is no longer translated and uses english always. Only not found error templates are still translated.
|
||||
* [#289](https://github.com/shlinkio/shlink/issues/289) Extracted coding standard rules to a separated package.
|
||||
* [#273](https://github.com/shlinkio/shlink/issues/273) Improved code coverage in repository classes.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#278](https://github.com/shlinkio/shlink/pull/278) Added missing `X-Api-Key` header to the list of valid cross domain headers.
|
||||
* [#295](https://github.com/shlinkio/shlink/pull/295) Fixed custom slugs so that they are case sensitive and do not try to lowercase provided values.
|
||||
|
||||
|
||||
## 1.14.1 - 2018-11-17
|
||||
|
||||
#### Added
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#260](https://github.com/shlinkio/shlink/issues/260) Increased mutation score to 65%.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#271](https://github.com/shlinkio/shlink/issues/271) Fixed memory leak produced when processing high amounts of visits at the same time.
|
||||
* [#272](https://github.com/shlinkio/shlink/issues/272) Fixed errors produced when trying to process visits multiple times in parallel, by using a lock which ensures only one instance is run at a time.
|
||||
|
||||
|
||||
## 1.14.0 - 2018-11-16
|
||||
|
||||
#### Added
|
||||
|
||||
155
README.md
155
README.md
@@ -44,52 +44,132 @@ Despite how you built the project, you are going to need to install it now, by f
|
||||
* 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.
|
||||
* 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.
|
||||
* Expose shlink to the web, either by using a traditional web server + fast CGI approach, or by using a [swoole](https://www.swoole.co.uk/) non-blocking server.
|
||||
|
||||
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.
|
||||
* **Using a web server:**
|
||||
|
||||
*Nginx:*
|
||||
For example, assuming your domain is doma.in and shlink is in the `/path/to/shlink` folder, these would be the basic configurations for Nginx and Apache.
|
||||
|
||||
```nginx
|
||||
server {
|
||||
server_name doma.in;
|
||||
listen 80;
|
||||
root /path/to/shlink/public;
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
*Nginx:*
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
```nginx
|
||||
server {
|
||||
server_name doma.in;
|
||||
listen 80;
|
||||
root /path/to/shlink/public;
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*Apache:*
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName doma.in
|
||||
DocumentRoot "/path/to/shlink/public"
|
||||
|
||||
<Directory "/path/to/shlink/public">
|
||||
Options FollowSymLinks Includes ExecCGI
|
||||
AllowOverride all
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
```
|
||||
|
||||
* **Using swoole:**
|
||||
|
||||
**Important!** Swoole support is still experimental. Use it with care, and report any found issue.
|
||||
|
||||
First you need to install the swoole PHP extension with [pecl](https://pecl.php.net/package/swoole), `pecl install swoole`.
|
||||
|
||||
Once installed, it's actually pretty easy to get shlink up and running with swoole. Just run `./vendor/bin/zend-expressive-swoole start -d` and you will get shlink running on port 8080.
|
||||
|
||||
However, by doing it this way, you are loosing all the access logs, and the service won't be automatically run if the server has to be restarted.
|
||||
|
||||
For that reason, you should create a daemon script, in `/etc/init.d/shlink_swoole`, like this one, replacing `/path/to/shlink` by the path to your shlink installation:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
### BEGIN INIT INFO
|
||||
# Provides: shlink_swoole
|
||||
# Required-Start: $local_fs $network $named $time $syslog
|
||||
# Required-Stop: $local_fs $network $named $time $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Description: Shlink non-blocking server with swoole
|
||||
### END INIT INFO
|
||||
|
||||
SCRIPT=/path/to/shlink/vendor/bin/zend-expressive-swoole\ start
|
||||
RUNAS=root
|
||||
|
||||
PIDFILE=/var/run/shlink_swoole.pid
|
||||
LOGDIR=/var/log/shlink
|
||||
LOGFILE=${LOGDIR}/shlink_swoole.log
|
||||
|
||||
start() {
|
||||
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE"); then
|
||||
echo 'Shlink with swoole already running' >&2
|
||||
return 1
|
||||
fi
|
||||
echo 'Starting shlink with swoole' >&2
|
||||
mkdir -p "$LOGDIR"
|
||||
touch "$LOGFILE"
|
||||
local CMD="$SCRIPT &> \"$LOGFILE\" & echo \$!"
|
||||
su -c "$CMD" $RUNAS > "$PIDFILE"
|
||||
echo 'Shlink started' >&2
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
stop() {
|
||||
if [[ ! -f "$PIDFILE" ]] || ! kill -0 $(cat "$PIDFILE"); then
|
||||
echo 'Shlink with swoole not running' >&2
|
||||
return 1
|
||||
fi
|
||||
echo 'Stopping shlink with swoole' >&2
|
||||
kill -15 $(cat "$PIDFILE") && rm -f "$PIDFILE"
|
||||
echo 'Shlink stopped' >&2
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
stop
|
||||
start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
esac
|
||||
```
|
||||
|
||||
*Apache:*
|
||||
Then run these commands to enable the service and start it:
|
||||
|
||||
```apache
|
||||
<VirtualHost *:80>
|
||||
ServerName doma.in
|
||||
DocumentRoot "/path/to/shlink/public"
|
||||
* `sudo chmod +x /etc/init.d/shlink_swoole`
|
||||
* `sudo update-rc.d shlink_swoole defaults`
|
||||
* `sudo update-rc.d shlink_swoole enable`
|
||||
* `/etc/init.d/shlink_swoole start`
|
||||
|
||||
<Directory "/path/to/shlink/public">
|
||||
Options FollowSymLinks Includes ExecCGI
|
||||
AllowOverride all
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
```
|
||||
Now again, you can access shlink on port 8080, but this time the service will be automatically run at system start-up, and all access logs will be written in `/var/log/shlink/shlink_swoole.log` (you will probably want to [rotate those logs](https://www.digitalocean.com/community/tutorials/how-to-manage-logfiles-with-logrotate-on-ubuntu-16-04). You can find an example logrotate config file [here](data/infra/examples/shlink-daemon-logrotate.conf)).
|
||||
|
||||
* Generate your first API key by running `bin/cli api-key:generate`. You will need the key in order to interact with shlink's API.
|
||||
* Finally access to [https://app.shlink.io](https://app.shlink.io) and configure your server to start creating short URLs.
|
||||
@@ -114,6 +194,10 @@ Those tasks can be performed using shlink's CLI, so it should be easy to schedul
|
||||
|
||||
Running this will improve the performance of the `doma.in/abc123/preview` URLs, which return a preview of the site.
|
||||
|
||||
*Any of those commands accept the `-q` flag, which makes it not display any output. This is recommended when configuring the commands as cron jobs.*
|
||||
|
||||
In future versions, it is planed that, when using **swoole** to serve shlink, some of these tasks are automatically run without blocking the request and also, without having to configure cron jobs. Probably resolving IP locations and generating previews.
|
||||
|
||||
## Update to new version
|
||||
|
||||
When a new Shlink version is available, you don't need to repeat the entire process yourself. Instead, follow these steps:
|
||||
@@ -191,6 +275,7 @@ Available commands:
|
||||
tag:rename Renames one existing tag.
|
||||
visit
|
||||
visit:process Processes visits where location is not set yet
|
||||
visit:update-db Updates the GeoLite2 database file used to geolocate IP addresses
|
||||
```
|
||||
|
||||
> This product includes GeoLite2 data created by MaxMind, available from [https://www.maxmind.com](https://www.maxmind.com)
|
||||
|
||||
3
bin/cli
3
bin/cli
@@ -3,8 +3,11 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Common\Exec\ExecutionContext;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/../config/container.php';
|
||||
|
||||
putenv(sprintf('CURRENT_SHLINK_CONTEXT=%s', ExecutionContext::CLI));
|
||||
$container->get(CliApp::class)->run();
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"roave/security-advisories": "dev-master",
|
||||
"symfony/console": "^4.1",
|
||||
"symfony/filesystem": "^4.1",
|
||||
"symfony/lock": "^4.1",
|
||||
"symfony/process": "^4.1",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"zendframework/zend-config": "^3.0",
|
||||
@@ -41,6 +42,7 @@
|
||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.0",
|
||||
"zendframework/zend-expressive-platesrenderer": "^2.0",
|
||||
"zendframework/zend-expressive-swoole": "^2.1",
|
||||
"zendframework/zend-i18n": "^2.7",
|
||||
"zendframework/zend-inputfilter": "^2.8",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
@@ -54,8 +56,7 @@
|
||||
"phpstan/phpstan": "^0.10.0",
|
||||
"phpunit/phpcov": "^5.0",
|
||||
"phpunit/phpunit": "^7.3",
|
||||
"slevomat/coding-standard": "^4.0",
|
||||
"squizlabs/php_codesniffer": "^3.2.3",
|
||||
"shlinkio/php-coding-standard": "~1.0.0",
|
||||
"symfony/dotenv": "^4.0",
|
||||
"symfony/var-dumper": "^4.0",
|
||||
"zendframework/zend-component-installer": "^2.1",
|
||||
@@ -118,13 +119,17 @@
|
||||
|
||||
"test:pretty": [
|
||||
"@test",
|
||||
"phpcov merge build --html build/html"
|
||||
"phpdbg -qrr vendor/bin/phpcov merge build --html build/html"
|
||||
],
|
||||
"test:unit:pretty": "phpdbg -qrr vendor/bin/phpunit --coverage-html build/coverage --order-by=random",
|
||||
|
||||
"infect": "infection --threads=4 --min-msi=60 --log-verbosity=2 --only-covered",
|
||||
"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"
|
||||
"infect": "infection --threads=4 --min-msi=65 --log-verbosity=2 --only-covered",
|
||||
"infect:ci": "infection --threads=4 --min-msi=65 --log-verbosity=2 --only-covered --coverage=build",
|
||||
"infect:show": "infection --threads=4 --min-msi=65 --log-verbosity=2 --only-covered --show-mutations",
|
||||
"infect:test": [
|
||||
"@test:unit:ci",
|
||||
"@infect:ci"
|
||||
]
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"check": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"test\" and \"infect\"</>",
|
||||
|
||||
@@ -4,18 +4,13 @@ declare(strict_types=1);
|
||||
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
|
||||
use Zend\Expressive;
|
||||
use Zend\Expressive\Container;
|
||||
use Zend\Expressive\Helper;
|
||||
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
|
||||
|
||||
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
|
||||
Helper\ServerUrlHelper::class => InvokableFactory::class,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Common;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
@@ -10,9 +10,9 @@ return [
|
||||
'proxies_dir' => 'data/proxies',
|
||||
],
|
||||
'connection' => [
|
||||
'user' => Common\env('DB_USER'),
|
||||
'password' => Common\env('DB_PASSWORD'),
|
||||
'dbname' => Common\env('DB_NAME', 'shlink'),
|
||||
'user' => env('DB_USER'),
|
||||
'password' => env('DB_PASSWORD'),
|
||||
'dbname' => env('DB_NAME', 'shlink'),
|
||||
'charset' => 'utf8',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\ConfigAggregator\ConfigAggregator;
|
||||
|
||||
return [
|
||||
|
||||
'debug' => true,
|
||||
'config_cache_enabled' => false,
|
||||
ConfigAggregator::ENABLE_CACHE => false,
|
||||
|
||||
];
|
||||
|
||||
25
config/autoload/locks.global.php
Normal file
25
config/autoload/locks.global.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Symfony\Component\Lock;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
|
||||
return [
|
||||
|
||||
'locks' => [
|
||||
'locks_dir' => __DIR__ . '/../../data/locks',
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Lock\Store\FlockStore::class => ConfigAbstractFactory::class,
|
||||
Lock\Factory::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Lock\Store\FlockStore::class => ['config.locks.locks_dir'],
|
||||
Lock\Factory::class => [Lock\Store\FlockStore::class],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -4,8 +4,10 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
use Monolog\Processor;
|
||||
use Zend\Expressive\Swoole\Log\AccessLogInterface;
|
||||
use const PHP_EOL;
|
||||
|
||||
return [
|
||||
@@ -19,13 +21,19 @@ return [
|
||||
],
|
||||
|
||||
'handlers' => [
|
||||
'rotating_file_handler' => [
|
||||
'shlink_rotating_handler' => [
|
||||
'class' => RotatingFileHandler::class,
|
||||
'level' => Logger::INFO,
|
||||
'filename' => 'data/log/shlink_log.log',
|
||||
'max_files' => 30,
|
||||
'formatter' => 'dashed',
|
||||
],
|
||||
'swoole_access_handler' => [
|
||||
'class' => StreamHandler::class,
|
||||
'level' => Logger::INFO,
|
||||
'stream' => 'php://stdout',
|
||||
'formatter' => 'dashed',
|
||||
],
|
||||
],
|
||||
|
||||
'processors' => [
|
||||
@@ -39,9 +47,30 @@ return [
|
||||
|
||||
'loggers' => [
|
||||
'Shlink' => [
|
||||
'handlers' => ['rotating_file_handler'],
|
||||
'handlers' => ['shlink_rotating_handler'],
|
||||
'processors' => ['exception_with_new_line', 'psr3'],
|
||||
],
|
||||
'Swoole' => [
|
||||
'handlers' => ['swoole_access_handler'],
|
||||
'processors' => ['psr3'],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
'Logger_Shlink' => Common\Factory\LoggerFactory::class,
|
||||
'Logger_Swoole' => Common\Factory\LoggerFactory::class,
|
||||
|
||||
AccessLogInterface::class => Common\Logger\Swoole\AccessLogFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
'logger_name' => 'Logger_Swoole',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Monolog\Logger;
|
||||
|
||||
return [
|
||||
|
||||
'logger' => [
|
||||
'handlers' => [
|
||||
'rotating_file_handler' => [
|
||||
'shlink_rotating_handler' => [
|
||||
'level' => Logger::DEBUG,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -10,11 +10,18 @@ return [
|
||||
|
||||
'middleware_pipeline' => [
|
||||
'pre-routing' => [
|
||||
'middleware' => [
|
||||
ErrorHandler::class,
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
Common\Middleware\LocaleMiddleware::class,
|
||||
],
|
||||
'middleware' => (function () {
|
||||
$middleware = [
|
||||
ErrorHandler::class,
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
];
|
||||
|
||||
if (Common\Exec\ExecutionContext::currentContextIsSwoole()) {
|
||||
$middleware[] = Common\Middleware\CloseDbConnectionMiddleware::class;
|
||||
}
|
||||
|
||||
return $middleware;
|
||||
})(),
|
||||
'priority' => 12,
|
||||
],
|
||||
'pre-routing-rest' => [
|
||||
@@ -47,6 +54,9 @@ return [
|
||||
'post-routing' => [
|
||||
'middleware' => [
|
||||
Expressive\Router\Middleware\DispatchMiddleware::class,
|
||||
|
||||
// Only if a not found error is triggered, set-up the locale to be used
|
||||
Common\Middleware\LocaleMiddleware::class,
|
||||
Core\Response\NotFoundHandler::class,
|
||||
],
|
||||
'priority' => 1,
|
||||
|
||||
23
config/autoload/slugify.global.php
Normal file
23
config/autoload/slugify.global.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Cocur\Slugify\Slugify;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
|
||||
return [
|
||||
|
||||
'slugify_options' => [
|
||||
'lowercase' => false,
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Slugify::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Slugify::class => ['config.slugify_options'],
|
||||
],
|
||||
|
||||
];
|
||||
18
config/autoload/swoole.global.php
Normal file
18
config/autoload/swoole.global.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'zend-expressive-swoole' => [
|
||||
'enable_coroutine' => true,
|
||||
|
||||
'swoole-http-server' => [
|
||||
'host' => '0.0.0.0',
|
||||
'process-name' => 'shlink',
|
||||
'static-files' => [
|
||||
'enable' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Zend\ConfigAggregator\ConfigAggregator;
|
||||
|
||||
return [
|
||||
|
||||
'debug' => false,
|
||||
'config_cache_enabled' => true,
|
||||
ConfigAggregator::ENABLE_CACHE => true,
|
||||
|
||||
];
|
||||
|
||||
@@ -6,17 +6,13 @@ namespace Shlinkio\Shlink;
|
||||
use Acelaya\ExpressiveErrorHandler;
|
||||
use Zend\ConfigAggregator;
|
||||
use Zend\Expressive;
|
||||
use function class_exists;
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator([
|
||||
Expressive\ConfigProvider::class,
|
||||
Expressive\Router\ConfigProvider::class,
|
||||
Expressive\Router\FastRouteRouter\ConfigProvider::class,
|
||||
Expressive\Plates\ConfigProvider::class,
|
||||
Expressive\Helper\ConfigProvider::class,
|
||||
class_exists(Expressive\Swoole\ConfigProvider::class)
|
||||
? Expressive\Swoole\ConfigProvider::class
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
Expressive\Swoole\ConfigProvider::class,
|
||||
ExpressiveErrorHandler\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
|
||||
11
data/infra/examples/apache-vhost.conf
Normal file
11
data/infra/examples/apache-vhost.conf
Normal file
@@ -0,0 +1,11 @@
|
||||
<VirtualHost *:80>
|
||||
ServerName doma.in
|
||||
DocumentRoot "/path/to/shlink/public"
|
||||
|
||||
<Directory "/path/to/shlink/public">
|
||||
Options FollowSymLinks Includes ExecCGI
|
||||
AllowOverride all
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
22
data/infra/examples/nginx-vhost.conf
Normal file
22
data/infra/examples/nginx-vhost.conf
Normal file
@@ -0,0 +1,22 @@
|
||||
server {
|
||||
server_name doma.in;
|
||||
listen 80;
|
||||
root /path/to/shlink/public;
|
||||
index index.php;
|
||||
charset utf-8;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php$is_args$args;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php7.1-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
|
||||
location ~ /\.ht {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
13
data/infra/examples/shlink-daemon-logrotate.conf
Normal file
13
data/infra/examples/shlink-daemon-logrotate.conf
Normal file
@@ -0,0 +1,13 @@
|
||||
/var/log/shlink/shlink_swoole.log {
|
||||
su root root
|
||||
daily
|
||||
missingok
|
||||
rotate 120
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 0640 root root
|
||||
postrotate
|
||||
/etc/init.d/shlink_swoole restart
|
||||
endscript
|
||||
}
|
||||
54
data/infra/examples/shlink-daemon.sh
Normal file
54
data/infra/examples/shlink-daemon.sh
Normal file
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
### BEGIN INIT INFO
|
||||
# Provides: shlink_swoole
|
||||
# Required-Start: $local_fs $network $named $time $syslog
|
||||
# Required-Stop: $local_fs $network $named $time $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Description: Shlink non-blocking server with swoole
|
||||
### END INIT INFO
|
||||
|
||||
SCRIPT=/path/to/shlink/vendor/bin/zend-expressive-swoole\ start
|
||||
RUNAS=root
|
||||
|
||||
PIDFILE=/var/run/shlink_swoole.pid
|
||||
LOGDIR=/var/log/shlink
|
||||
LOGFILE=${LOGDIR}/shlink_swoole.log
|
||||
|
||||
start() {
|
||||
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE"); then
|
||||
echo 'Shlink with swoole already running' >&2
|
||||
return 1
|
||||
fi
|
||||
echo 'Starting shlink with swoole' >&2
|
||||
mkdir -p "$LOGDIR"
|
||||
touch "$LOGFILE"
|
||||
local CMD="$SCRIPT &> \"$LOGFILE\" & echo \$!"
|
||||
su -c "$CMD" $RUNAS > "$PIDFILE"
|
||||
echo 'Shlink started' >&2
|
||||
}
|
||||
|
||||
stop() {
|
||||
if [[ ! -f "$PIDFILE" ]] || ! kill -0 $(cat "$PIDFILE"); then
|
||||
echo 'Shlink with swoole not running' >&2
|
||||
return 1
|
||||
fi
|
||||
echo 'Stopping shlink with swoole' >&2
|
||||
kill -15 $(cat "$PIDFILE") && rm -f "$PIDFILE"
|
||||
echo 'Shlink stopped' >&2
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
stop
|
||||
start
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart}"
|
||||
esac
|
||||
98
data/infra/swoole.Dockerfile
Normal file
98
data/infra/swoole.Dockerfile
Normal file
@@ -0,0 +1,98 @@
|
||||
FROM php:7.1.22-cli-alpine3.7
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
RUN apk update
|
||||
|
||||
# Install common php extensions
|
||||
RUN docker-php-ext-install pdo_mysql
|
||||
RUN docker-php-ext-install iconv
|
||||
RUN docker-php-ext-install mbstring
|
||||
RUN docker-php-ext-install calendar
|
||||
|
||||
RUN apk add --no-cache --virtual sqlite-libs
|
||||
RUN apk add --no-cache --virtual sqlite-dev
|
||||
RUN docker-php-ext-install pdo_sqlite
|
||||
|
||||
RUN apk add --no-cache --virtual icu-dev
|
||||
RUN docker-php-ext-install intl
|
||||
|
||||
RUN apk add --no-cache --virtual zlib-dev
|
||||
RUN docker-php-ext-install zip
|
||||
|
||||
RUN apk add --no-cache --virtual libmcrypt-dev
|
||||
RUN docker-php-ext-install mcrypt
|
||||
|
||||
RUN apk add --no-cache --virtual libpng-dev
|
||||
RUN docker-php-ext-install gd
|
||||
|
||||
# Install redis extension
|
||||
ADD https://github.com/phpredis/phpredis/archive/3.1.4.tar.gz /tmp/phpredis.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/redis\
|
||||
&& tar xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1
|
||||
# configure and install
|
||||
RUN docker-php-ext-configure redis\
|
||||
&& docker-php-ext-install redis
|
||||
# cleanup
|
||||
RUN rm /tmp/phpredis.tar.gz
|
||||
|
||||
# Install memcached extension
|
||||
RUN apk add --no-cache --virtual cyrus-sasl-dev
|
||||
RUN apk add --no-cache --virtual libmemcached-dev
|
||||
ADD https://github.com/php-memcached-dev/php-memcached/archive/php7.tar.gz /tmp/memcached.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/memcached\
|
||||
&& tar xf /tmp/memcached.tar.gz -C /usr/src/php/ext/memcached --strip-components=1
|
||||
# configure and install
|
||||
RUN docker-php-ext-configure memcached\
|
||||
&& docker-php-ext-install memcached
|
||||
# cleanup
|
||||
RUN rm /tmp/memcached.tar.gz
|
||||
|
||||
# Install APCu extension
|
||||
ADD https://pecl.php.net/get/apcu-5.1.3.tgz /tmp/apcu.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu\
|
||||
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1
|
||||
# configure and install
|
||||
RUN docker-php-ext-configure apcu\
|
||||
&& docker-php-ext-install apcu
|
||||
# cleanup
|
||||
RUN rm /tmp/apcu.tar.gz
|
||||
|
||||
# Install APCu-BC extension
|
||||
ADD https://pecl.php.net/get/apcu_bc-1.0.3.tgz /tmp/apcu_bc.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu-bc\
|
||||
&& tar xf /tmp/apcu_bc.tar.gz -C /usr/src/php/ext/apcu-bc --strip-components=1
|
||||
# configure and install
|
||||
RUN docker-php-ext-configure apcu-bc\
|
||||
&& docker-php-ext-install apcu-bc
|
||||
# cleanup
|
||||
RUN rm /tmp/apcu_bc.tar.gz
|
||||
|
||||
# Load APCU.ini before APC.ini
|
||||
RUN rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
|
||||
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||
|
||||
# Install swoole
|
||||
# First line fixes an error when installing pecl extensions. Found in https://github.com/docker-library/php/issues/233
|
||||
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS && \
|
||||
pecl install swoole && \
|
||||
docker-php-ext-enable swoole && \
|
||||
apk del .phpize-deps
|
||||
|
||||
# Install composer
|
||||
RUN php -r "readfile('https://getcomposer.org/installer');" | php
|
||||
RUN chmod +x composer.phar
|
||||
RUN mv composer.phar /usr/local/bin/composer
|
||||
|
||||
# Make home directory writable by anyone
|
||||
RUN chmod 777 /home
|
||||
|
||||
VOLUME /home/shlink
|
||||
WORKDIR /home/shlink
|
||||
|
||||
# Expose swoole port
|
||||
EXPOSE 8080
|
||||
|
||||
CMD /usr/local/bin/composer update && \
|
||||
# When restarting the container, swoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php ./vendor/bin/zend-expressive-swoole start; do sleep 1 ; done
|
||||
2
data/locks/.gitignore
vendored
Normal file
2
data/locks/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -6,3 +6,9 @@ services:
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_swoole:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
@@ -26,6 +26,18 @@ services:
|
||||
links:
|
||||
- shlink_db
|
||||
|
||||
shlink_swoole:
|
||||
container_name: shlink_swoole
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/swoole.Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- ./:/home/shlink
|
||||
links:
|
||||
- shlink_db
|
||||
|
||||
shlink_db:
|
||||
container_name: shlink_db
|
||||
build:
|
||||
|
||||
@@ -33,6 +33,24 @@
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"description": "The page to display. Defaults to 1",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "itemsPerPage",
|
||||
"in": "query",
|
||||
"description": "The amount of items to return on every page. Defaults to all the items",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
@@ -59,6 +77,9 @@
|
||||
"items": {
|
||||
"$ref": "../definitions/Visit.json"
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"$ref": "../definitions/Pagination.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,7 +117,14 @@
|
||||
"userAgent": "some_web_crawler/1.4",
|
||||
"visitLocation": null
|
||||
}
|
||||
]
|
||||
],
|
||||
"pagination": {
|
||||
"currentPage": 5,
|
||||
"pagesCount": 12,
|
||||
"itemsPerPage": 10,
|
||||
"itemsInCurrentPage": 10,
|
||||
"totalItems": 115
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
indocker
9
indocker
@@ -1,2 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
docker exec -it shlink_php /bin/sh -c "cd /home/shlink/www && $*"
|
||||
|
||||
# Run docker containers if they are not up yet
|
||||
if [[ $(docker ps | grep shlink_swoole) ]]; then :
|
||||
else
|
||||
docker-compose up -d
|
||||
fi
|
||||
|
||||
docker exec -it shlink_swoole /bin/sh -c "$*"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"module/*/src"
|
||||
]
|
||||
},
|
||||
"timeout": 10,
|
||||
"timeout": 5,
|
||||
"logs": {
|
||||
"text": "build/infection/infection-log.txt",
|
||||
"summary": "build/infection/summary-log.txt",
|
||||
|
||||
@@ -3,12 +3,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
'cli' => [
|
||||
'locale' => env('CLI_LOCALE', 'en'),
|
||||
'commands' => [
|
||||
Command\ShortUrl\GenerateShortUrlCommand::NAME => Command\ShortUrl\GenerateShortUrlCommand::class,
|
||||
Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,
|
||||
|
||||
@@ -9,8 +9,9 @@ use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Symfony\Component\Lock;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
@@ -28,8 +29,8 @@ return [
|
||||
Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Visit\UpdateDbCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Config\GenerateCharsetCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Config\GenerateSecretCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Config\GenerateCharsetCommand::class => InvokableFactory::class,
|
||||
Command\Config\GenerateSecretCommand::class => InvokableFactory::class,
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
|
||||
@@ -43,46 +44,28 @@ return [
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [
|
||||
Service\UrlShortener::class,
|
||||
'translator',
|
||||
'config.url_shortener.domain',
|
||||
],
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'],
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => [
|
||||
Service\ShortUrlService::class,
|
||||
'translator',
|
||||
'config.url_shortener.domain',
|
||||
],
|
||||
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'],
|
||||
Command\ShortUrl\GeneratePreviewCommand::class => [
|
||||
Service\ShortUrlService::class,
|
||||
PreviewGenerator::class,
|
||||
'translator',
|
||||
],
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => [
|
||||
Service\ShortUrl\DeleteShortUrlService::class,
|
||||
'translator',
|
||||
],
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [Service\UrlShortener::class, 'config.url_shortener.domain'],
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'],
|
||||
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class],
|
||||
Command\ShortUrl\GeneratePreviewCommand::class => [Service\ShortUrlService::class, PreviewGenerator::class],
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::class => [
|
||||
Service\VisitService::class,
|
||||
IpLocationResolverInterface::class,
|
||||
'translator',
|
||||
Lock\Factory::class,
|
||||
],
|
||||
Command\Visit\UpdateDbCommand::class => [DbUpdater::class, 'translator'],
|
||||
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
|
||||
|
||||
Command\Config\GenerateCharsetCommand::class => ['translator'],
|
||||
Command\Config\GenerateSecretCommand::class => ['translator'],
|
||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\ListKeysCommand::class => [ApiKeyService::class],
|
||||
|
||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, 'translator'],
|
||||
Command\Api\DisableKeyCommand::class => [ApiKeyService::class, 'translator'],
|
||||
Command\Api\ListKeysCommand::class => [ApiKeyService::class, 'translator'],
|
||||
|
||||
Command\Tag\ListTagsCommand::class => [Service\Tag\TagService::class, Translator::class],
|
||||
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class, Translator::class],
|
||||
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class, Translator::class],
|
||||
Command\Tag\DeleteTagsCommand::class => [Service\Tag\TagService::class, Translator::class],
|
||||
Command\Tag\ListTagsCommand::class => [Service\Tag\TagService::class],
|
||||
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class],
|
||||
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class],
|
||||
Command\Tag\DeleteTagsCommand::class => [Service\Tag\TagService::class],
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'translator' => [
|
||||
'translation_file_patterns' => [
|
||||
[
|
||||
'type' => 'gettext',
|
||||
'base_dir' => __DIR__ . '/../lang',
|
||||
'pattern' => '%s.mo',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
Binary file not shown.
@@ -1,399 +0,0 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2018-11-12 21:01+0100\n"
|
||||
"PO-Revision-Date: 2018-11-12 21:03+0100\n"
|
||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 2.0.6\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"X-Poedit-KeywordsList: translate;translatePlural\n"
|
||||
"X-Poedit-SearchPath-0: src\n"
|
||||
"X-Poedit-SearchPath-1: config\n"
|
||||
|
||||
msgid "Disables an API key."
|
||||
msgstr "Desahbilita una clave de API."
|
||||
|
||||
msgid "The API key to disable"
|
||||
msgstr "La clave de API a deshabilitar"
|
||||
|
||||
#, php-format
|
||||
msgid "API key \"%s\" properly disabled"
|
||||
msgstr "Clave de API \"%s\" deshabilitada correctamente"
|
||||
|
||||
#, php-format
|
||||
msgid "API key \"%s\" does not exist."
|
||||
msgstr "La clave de API \"%s\" no existe."
|
||||
|
||||
msgid "Generates a new valid API key."
|
||||
msgstr "Genera una nueva clave de API válida."
|
||||
|
||||
msgid "The date in which the API key should expire. Use any valid PHP format."
|
||||
msgstr ""
|
||||
"La fecha en la que la clave de API debe expirar. Utiliza cualquier valor "
|
||||
"válido en PHP."
|
||||
|
||||
#, php-format
|
||||
msgid "Generated API key: \"%s\""
|
||||
msgstr "Generada clave de API. \"%s\""
|
||||
|
||||
msgid "Lists all the available API keys."
|
||||
msgstr "Lista todas las claves de API disponibles."
|
||||
|
||||
msgid "Tells if only enabled API keys should be returned."
|
||||
msgstr "Define si sólo las claves de API habilitadas deben ser devueltas."
|
||||
|
||||
msgid "Key"
|
||||
msgstr "Clave"
|
||||
|
||||
msgid "Is enabled"
|
||||
msgstr "Está habilitada"
|
||||
|
||||
msgid "Expiration date"
|
||||
msgstr "Fecha de caducidad"
|
||||
|
||||
#, php-format
|
||||
msgid ""
|
||||
"Generates a character set sample just by shuffling the default one, \"%s\". "
|
||||
"Then it can be set in the SHORTCODE_CHARS environment variable"
|
||||
msgstr ""
|
||||
"Genera un grupo de caracteres simplemente mexclando el grupo por defecto \"%s"
|
||||
"\". Después puede ser utilizado en la variable de entrono SHORTCODE_CHARS"
|
||||
|
||||
#, php-format
|
||||
msgid "Character set: \"%s\""
|
||||
msgstr "Grupo de caracteres: \"%s\""
|
||||
|
||||
msgid ""
|
||||
"Generates a random secret string that can be used for JWT token encryption"
|
||||
msgstr ""
|
||||
"Genera una cadena de caracteres aleatoria que puede ser usada para cifrar "
|
||||
"tokens JWT"
|
||||
|
||||
#, php-format
|
||||
msgid "Secret key: \"%s\""
|
||||
msgstr "Clave secreta: \"%s\""
|
||||
|
||||
msgid "Deletes a short URL"
|
||||
msgstr "Elimina una URL"
|
||||
|
||||
msgid "The short code for the short URL to be deleted"
|
||||
msgstr "El código corto de la URL corta a eliminar"
|
||||
|
||||
msgid ""
|
||||
"Ignores the safety visits threshold check, which could make short URLs with "
|
||||
"many visits to be accidentally deleted"
|
||||
msgstr ""
|
||||
"Ignora el límite de seguridad de visitas, pudiendo resultar en el borrado "
|
||||
"accidental de URLs con muchas visitas"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided short code \"%s\" could not be found."
|
||||
msgstr "El código corto proporcionado \"%s\" no ha podido ser encontrado."
|
||||
|
||||
#, php-format
|
||||
msgid ""
|
||||
"It was not possible to delete the short URL with short code \"%s\" because "
|
||||
"it has more than %s visits."
|
||||
msgstr ""
|
||||
"No se pudo eliminar la URL acortada con código corto \"%s\" porque tiene más "
|
||||
"de %s visitas."
|
||||
|
||||
msgid "Do you want to delete it anyway?"
|
||||
msgstr "¿Aún así quieres eliminarla?"
|
||||
|
||||
msgid "Short URL was not deleted."
|
||||
msgstr "La URL corta no ha sido eliminada."
|
||||
|
||||
#, php-format
|
||||
msgid "Short URL with short code \"%s\" successfully deleted."
|
||||
msgstr "La URL acortada con el código corto \"%s\" eliminada correctamente."
|
||||
|
||||
msgid ""
|
||||
"Processes and generates the previews for every URL, improving performance "
|
||||
"for later web requests."
|
||||
msgstr ""
|
||||
"Procesa y genera las vistas previas para cada URL, mejorando el rendimiento "
|
||||
"para peticiones web posteriores."
|
||||
|
||||
msgid "Finished processing all URLs"
|
||||
msgstr "Finalizado el procesado de todas las URLs"
|
||||
|
||||
#, php-format
|
||||
msgid "Processing URL %s..."
|
||||
msgstr "Procesando URL %s..."
|
||||
|
||||
msgid " <info>Success!</info>"
|
||||
msgstr " <info>¡Correcto!</info>"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
msgid "Generates a short URL for provided long URL and returns it"
|
||||
msgstr "Genera una URL corta para la URL larga proporcionada y la devuelve"
|
||||
|
||||
msgid "The long URL to parse"
|
||||
msgstr "La URL larga a procesar"
|
||||
|
||||
msgid "Tags to apply to the new short URL"
|
||||
msgstr "Etiquetas a aplicar a la nueva URL acortada"
|
||||
|
||||
msgid ""
|
||||
"The date from which this short URL will be valid. If someone tries to access "
|
||||
"it before this date, it will not be found."
|
||||
msgstr ""
|
||||
"La fecha desde la cual será válida esta URL acortada. Si alguien intenta "
|
||||
"acceder a ella antes de esta fecha, no será encontrada."
|
||||
|
||||
msgid ""
|
||||
"The date until which this short URL will be valid. If someone tries to "
|
||||
"access it after this date, it will not be found."
|
||||
msgstr ""
|
||||
"La fecha hasta la cual será válida está URL acortada. Si alguien intenta "
|
||||
"acceder a ella después de esta fecha, no será encontrada."
|
||||
|
||||
msgid "If provided, this slug will be used instead of generating a short code"
|
||||
msgstr ""
|
||||
"Si se proporciona, este slug será usado en vez de generar un código corto"
|
||||
|
||||
msgid "This will limit the number of visits for this short URL."
|
||||
msgstr "Esto limitará el número de visitas a esta URL acortada."
|
||||
|
||||
#, fuzzy
|
||||
#| msgid "A long URL was not provided. Which URL do you want to shorten?:"
|
||||
msgid "A long URL was not provided. Which URL do you want to be shortened?"
|
||||
msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?"
|
||||
|
||||
msgid "A URL was not provided!"
|
||||
msgstr "¡No se ha proporcionado una URL!"
|
||||
|
||||
msgid "Processed long URL:"
|
||||
msgstr "URL larga procesada:"
|
||||
|
||||
msgid "Generated short URL:"
|
||||
msgstr "URL corta generada:"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided URL \"%s\" is invalid. Try with a different one."
|
||||
msgstr "La URL proporcionada \"%s\" e inválida. Prueba con una diferente."
|
||||
|
||||
#, php-format
|
||||
msgid ""
|
||||
"Provided slug \"%s\" is already in use by another URL. Try with a different "
|
||||
"one."
|
||||
msgstr ""
|
||||
"El slug proporcionado \"%s\" ya está siendo usado para otra URL. Prueba con "
|
||||
"uno diferente."
|
||||
|
||||
msgid "Returns the detailed visits information for provided short code"
|
||||
msgstr ""
|
||||
"Devuelve la información detallada de visitas para el código corto "
|
||||
"proporcionado"
|
||||
|
||||
msgid "The short code which visits we want to get"
|
||||
msgstr "El código corto del cual queremos obtener las visitas"
|
||||
|
||||
msgid "Allows to filter visits, returning only those older than start date"
|
||||
msgstr ""
|
||||
"Permite filtrar las visitas, devolviendo sólo aquellas más antiguas que "
|
||||
"startDate"
|
||||
|
||||
msgid "Allows to filter visits, returning only those newer than end date"
|
||||
msgstr ""
|
||||
"Permite filtrar las visitas, devolviendo sólo aquellas más nuevas que endDate"
|
||||
|
||||
msgid "A short code was not provided. Which short code do you want to use?"
|
||||
msgstr "No se proporcionó un código corto. ¿Qué código corto deseas usar?"
|
||||
|
||||
msgid "Referer"
|
||||
msgstr "Origen"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Fecha"
|
||||
|
||||
msgid "User agent"
|
||||
msgstr "Agente de usuario"
|
||||
|
||||
msgid "Country"
|
||||
msgstr "País"
|
||||
|
||||
msgid "List all short URLs"
|
||||
msgstr "Listar todas las URLs cortas"
|
||||
|
||||
#, php-format
|
||||
msgid "The first page to list (%s items per page)"
|
||||
msgstr "La primera página a listar (%s elementos por página)"
|
||||
|
||||
msgid ""
|
||||
"A query used to filter results by searching for it on the longUrl and "
|
||||
"shortCode fields"
|
||||
msgstr ""
|
||||
"Una consulta usada para filtrar el resultado buscándola en los campos "
|
||||
"longUrl y shortCode"
|
||||
|
||||
msgid "A comma-separated list of tags to filter results"
|
||||
msgstr "Una lista de etiquetas separadas por coma para filtrar el resultado"
|
||||
|
||||
msgid ""
|
||||
"The field from which we want to order by. Pass ASC or DESC separated by a "
|
||||
"comma"
|
||||
msgstr ""
|
||||
"El campo por el cual queremos ordernar. Pasa ASC o DESC separado por una coma"
|
||||
|
||||
msgid "Whether to display the tags or not"
|
||||
msgstr "Si se desea mostrar las etiquetas o no"
|
||||
|
||||
msgid "Short code"
|
||||
msgstr "Código corto"
|
||||
|
||||
msgid "Short URL"
|
||||
msgstr "URL corta"
|
||||
|
||||
msgid "Long URL"
|
||||
msgstr "URL larga"
|
||||
|
||||
msgid "Date created"
|
||||
msgstr "Fecha de creación"
|
||||
|
||||
msgid "Visits count"
|
||||
msgstr "Número de visitas"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
msgid "Short URLs properly listed"
|
||||
msgstr "URLs cortas listadas correctamente"
|
||||
|
||||
msgid "Continue with page"
|
||||
msgstr "Continuar con la página"
|
||||
|
||||
msgid "Returns the long URL behind a short code"
|
||||
msgstr "Devuelve la URL larga detrás de un código corto"
|
||||
|
||||
msgid "The short code to parse"
|
||||
msgstr "El código corto a convertir"
|
||||
|
||||
msgid "A short code was not provided. Which short code do you want to parse?"
|
||||
msgstr ""
|
||||
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?"
|
||||
|
||||
msgid "Long URL:"
|
||||
msgstr "URL larga:"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided short code \"%s\" has an invalid format."
|
||||
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
|
||||
|
||||
msgid "Creates one or more tags."
|
||||
msgstr "Crea una o más etiquetas."
|
||||
|
||||
msgid "The name of the tags to create"
|
||||
msgstr "El nombre de las etiquetas a crear"
|
||||
|
||||
msgid "You have to provide at least one tag name"
|
||||
msgstr "Debes proporcionar al menos un nombre de etiqueta"
|
||||
|
||||
msgid "Tags properly created"
|
||||
msgstr "Etiquetas correctamente creadas"
|
||||
|
||||
msgid "Deletes one or more tags."
|
||||
msgstr "Elimina una o más etiquetas."
|
||||
|
||||
msgid "The name of the tags to delete"
|
||||
msgstr "El nombre de las etiquetas a eliminar"
|
||||
|
||||
msgid "Tags properly deleted"
|
||||
msgstr "Etiquetas correctamente eliminadas"
|
||||
|
||||
msgid "Lists existing tags."
|
||||
msgstr "Lista las etiquetas existentes."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
msgid "No tags yet"
|
||||
msgstr "Aún no hay etiquetas"
|
||||
|
||||
msgid "Renames one existing tag."
|
||||
msgstr "Renombra una etiqueta existente."
|
||||
|
||||
msgid "Current name of the tag."
|
||||
msgstr "Nombre actual de la etiqueta."
|
||||
|
||||
msgid "New name of the tag."
|
||||
msgstr "Nuevo nombre de la etiqueta."
|
||||
|
||||
msgid "Tag properly renamed."
|
||||
msgstr "Etiqueta correctamente renombrada."
|
||||
|
||||
#, php-format
|
||||
msgid "A tag with name \"%s\" was not found"
|
||||
msgstr "Una etiqueta con nombre \"%s\" no ha sido encontrada"
|
||||
|
||||
msgid "Processes visits where location is not set yet"
|
||||
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"
|
||||
msgstr "Procesando IP"
|
||||
|
||||
msgid "Ignored localhost address"
|
||||
msgstr "Ignorada IP de localhost"
|
||||
|
||||
#, php-format
|
||||
msgid "Address located at \"%s\""
|
||||
msgstr "Dirección localizada en \"%s\""
|
||||
|
||||
msgid "An error occurred while locating IP. Skipped"
|
||||
msgstr "Se produjo un error al localizar la IP. Ignorado"
|
||||
|
||||
msgid "Finished processing all 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"
|
||||
#~ msgstr "Dirección remota"
|
||||
|
||||
#~ msgid "Original URL"
|
||||
#~ msgstr "URL original"
|
||||
|
||||
#~ msgid "You have reached last page"
|
||||
#~ msgstr "Has alcanzado la última página"
|
||||
|
||||
#~ msgid "No URL found for short code \"%s\""
|
||||
#~ msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
|
||||
|
||||
#~ msgid "Created tags"
|
||||
#~ msgstr "Etiquetas creadas"
|
||||
|
||||
#~ msgid "Deleted tags"
|
||||
#~ msgstr "Etiquetas eliminadas"
|
||||
@@ -10,34 +10,26 @@ use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
|
||||
class DisableKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:disable';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
/** @var ApiKeyServiceInterface */
|
||||
private $apiKeyService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Disables an API key.'))
|
||||
->addArgument('apiKey', InputArgument::REQUIRED, $this->translator->translate('The API key to disable'));
|
||||
->setDescription('Disables an API key.')
|
||||
->addArgument('apiKey', InputArgument::REQUIRED, 'The API key to disable');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
@@ -47,9 +39,9 @@ class DisableKeyCommand extends Command
|
||||
|
||||
try {
|
||||
$this->apiKeyService->disable($apiKey);
|
||||
$io->success(sprintf($this->translator->translate('API key "%s" properly disabled'), $apiKey));
|
||||
$io->success(sprintf('API key "%s" properly disabled', $apiKey));
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$io->error(sprintf($this->translator->translate('API key "%s" does not exist.'), $apiKey));
|
||||
$io->error(sprintf('API key "%s" does not exist.', $apiKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,39 +10,32 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
|
||||
class GenerateKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:generate';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
/** @var ApiKeyServiceInterface */
|
||||
private $apiKeyService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Generates a new valid API key.'))
|
||||
->addOption(
|
||||
'expirationDate',
|
||||
'e',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('The date in which the API key should expire. Use any valid PHP format.')
|
||||
);
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Generates a new valid API key.')
|
||||
->addOption(
|
||||
'expirationDate',
|
||||
'e',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
'The date in which the API key should expire. Use any valid PHP format.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
@@ -50,8 +43,6 @@ class GenerateKeyCommand extends Command
|
||||
$expirationDate = $input->getOption('expirationDate');
|
||||
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? Chronos::parse($expirationDate) : null);
|
||||
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey)
|
||||
);
|
||||
(new SymfonyStyle($input, $output))->success(sprintf('Generated API key: "%s"', $apiKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function sprintf;
|
||||
@@ -23,32 +22,26 @@ class ListKeysCommand extends Command
|
||||
|
||||
public const NAME = 'api-key:list';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
/** @var ApiKeyServiceInterface */
|
||||
private $apiKeyService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Lists all the available API keys.'))
|
||||
->addOption(
|
||||
'enabledOnly',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate('Tells if only enabled API keys should be returned.')
|
||||
);
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Lists all the available API keys.')
|
||||
->addOption(
|
||||
'enabledOnly',
|
||||
'e',
|
||||
InputOption::VALUE_NONE,
|
||||
'Tells if only enabled API keys should be returned.'
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
@@ -57,12 +50,11 @@ class ListKeysCommand extends Command
|
||||
$enabledOnly = $input->getOption('enabledOnly');
|
||||
|
||||
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
|
||||
$key = (string) $apiKey;
|
||||
$expiration = $apiKey->getExpirationDate();
|
||||
$messagePattern = $this->determineMessagePattern($apiKey);
|
||||
|
||||
// Set columns for this row
|
||||
$rowData = [sprintf($messagePattern, $key)];
|
||||
$rowData = [sprintf($messagePattern, $apiKey)];
|
||||
if (! $enabledOnly) {
|
||||
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
|
||||
}
|
||||
@@ -71,9 +63,9 @@ class ListKeysCommand extends Command
|
||||
}, $this->apiKeyService->listKeys($enabledOnly));
|
||||
|
||||
$io->table(array_filter([
|
||||
$this->translator->translate('Key'),
|
||||
! $enabledOnly ? $this->translator->translate('Is enabled') : null,
|
||||
$this->translator->translate('Expiration date'),
|
||||
'Key',
|
||||
! $enabledOnly ? 'Is enabled' : null,
|
||||
'Expiration date',
|
||||
]), $rows);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
use function str_shuffle;
|
||||
|
||||
@@ -16,31 +15,20 @@ class GenerateCharsetCommand extends Command
|
||||
{
|
||||
public const NAME = 'config:generate-charset';
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(sprintf($this->translator->translate(
|
||||
'Generates a character set sample just by shuffling the default one, "%s". '
|
||||
. 'Then it can be set in the SHORTCODE_CHARS environment variable'
|
||||
), UrlShortener::DEFAULT_CHARS));
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription(sprintf(
|
||||
'Generates a character set sample just by shuffling the default one, "%s". '
|
||||
. 'Then it can be set in the SHORTCODE_CHARS environment variable',
|
||||
UrlShortener::DEFAULT_CHARS
|
||||
));
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Character set: "%s"'), $charSet)
|
||||
);
|
||||
(new SymfonyStyle($input, $output))->success(sprintf('Character set: "%s"', $charSet));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
|
||||
class GenerateSecretCommand extends Command
|
||||
@@ -17,30 +16,16 @@ class GenerateSecretCommand extends Command
|
||||
|
||||
public const NAME = 'config:generate-secret';
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate(
|
||||
'Generates a random secret string that can be used for JWT token encryption'
|
||||
));
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('[DEPRECATED] Generates a random secret string that can be used for JWT token encryption');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$secret = $this->generateRandomString(32);
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Secret key: "%s"'), $secret)
|
||||
);
|
||||
(new SymfonyStyle($input, $output))->success(sprintf('Secret key: "%s"', $secret));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortUrlCommand extends Command
|
||||
@@ -19,20 +18,13 @@ class DeleteShortUrlCommand extends Command
|
||||
public const NAME = 'short-url:delete';
|
||||
private const ALIASES = ['short-code:delete'];
|
||||
|
||||
/**
|
||||
* @var DeleteShortUrlServiceInterface
|
||||
*/
|
||||
/** @var DeleteShortUrlServiceInterface */
|
||||
private $deleteShortUrlService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService, TranslatorInterface $translator)
|
||||
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService)
|
||||
{
|
||||
$this->deleteShortUrlService = $deleteShortUrlService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->deleteShortUrlService = $deleteShortUrlService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -40,22 +32,14 @@ class DeleteShortUrlCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate('Deletes a short URL')
|
||||
)
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code for the short URL to be deleted')
|
||||
)
|
||||
->setDescription('Deletes a short URL')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code for the short URL to be deleted')
|
||||
->addOption(
|
||||
'ignore-threshold',
|
||||
'i',
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate(
|
||||
'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
|
||||
. 'accidentally deleted'
|
||||
)
|
||||
'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
|
||||
. 'accidentally deleted'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,9 +52,7 @@ class DeleteShortUrlCommand extends Command
|
||||
try {
|
||||
$this->runDelete($io, $shortCode, $ignoreThreshold);
|
||||
} catch (Exception\InvalidShortCodeException $e) {
|
||||
$io->error(
|
||||
sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
||||
);
|
||||
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode));
|
||||
} catch (Exception\DeleteShortUrlException $e) {
|
||||
$this->retry($io, $shortCode, $e);
|
||||
}
|
||||
@@ -78,25 +60,24 @@ class DeleteShortUrlCommand extends Command
|
||||
|
||||
private function retry(SymfonyStyle $io, string $shortCode, Exception\DeleteShortUrlException $e): void
|
||||
{
|
||||
$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.'
|
||||
), $shortCode, $e->getVisitsThreshold());
|
||||
$warningMsg = sprintf(
|
||||
'It was not possible to delete the short URL with short code "%s" because it has more than %s visits.',
|
||||
$shortCode,
|
||||
$e->getVisitsThreshold()
|
||||
);
|
||||
$io->writeln('<bg=yellow>' . $warningMsg . '</>');
|
||||
$forceDelete = $io->confirm($this->translator->translate('Do you want to delete it anyway?'), false);
|
||||
$forceDelete = $io->confirm('Do you want to delete it anyway?', false);
|
||||
|
||||
if ($forceDelete) {
|
||||
$this->runDelete($io, $shortCode, true);
|
||||
} else {
|
||||
$io->warning($this->translator->translate('Short URL was not deleted.'));
|
||||
$io->warning('Short URL was not deleted.');
|
||||
}
|
||||
}
|
||||
|
||||
private function runDelete(SymfonyStyle $io, string $shortCode, bool $ignoreThreshold): void
|
||||
{
|
||||
$this->deleteShortUrlService->deleteByShortCode($shortCode, $ignoreThreshold);
|
||||
$io->success(sprintf(
|
||||
$this->translator->translate('Short URL with short code "%s" successfully deleted.'),
|
||||
$shortCode
|
||||
));
|
||||
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $shortCode));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
|
||||
class GeneratePreviewCommand extends Command
|
||||
@@ -18,28 +17,16 @@ class GeneratePreviewCommand extends Command
|
||||
public const NAME = 'short-url:process-previews';
|
||||
private const ALIASES = ['shortcode:process-previews', 'short-code:process-previews'];
|
||||
|
||||
/**
|
||||
* @var PreviewGeneratorInterface
|
||||
*/
|
||||
/** @var PreviewGeneratorInterface */
|
||||
private $previewGenerator;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
*/
|
||||
/** @var ShortUrlServiceInterface */
|
||||
private $shortUrlService;
|
||||
|
||||
public function __construct(
|
||||
ShortUrlServiceInterface $shortUrlService,
|
||||
PreviewGeneratorInterface $previewGenerator,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, PreviewGeneratorInterface $previewGenerator)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->previewGenerator = $previewGenerator;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -48,9 +35,7 @@ class GeneratePreviewCommand extends Command
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate(
|
||||
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||
)
|
||||
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,17 +51,17 @@ class GeneratePreviewCommand extends Command
|
||||
}
|
||||
} while ($page <= $shortUrls->count());
|
||||
|
||||
(new SymfonyStyle($input, $output))->success($this->translator->translate('Finished processing all URLs'));
|
||||
(new SymfonyStyle($input, $output))->success('Finished processing all URLs');
|
||||
}
|
||||
|
||||
private function processUrl($url, OutputInterface $output): void
|
||||
{
|
||||
try {
|
||||
$output->write(sprintf($this->translator->translate('Processing URL %s...'), $url));
|
||||
$output->write(sprintf('Processing URL %s...', $url));
|
||||
$this->previewGenerator->generatePreview($url);
|
||||
$output->writeln($this->translator->translate(' <info>Success!</info>'));
|
||||
$output->writeln(' <info>Success!</info>');
|
||||
} catch (PreviewGenerationException $e) {
|
||||
$output->writeln(' <error>' . $this->translator->translate('Error') . '</error>');
|
||||
$output->writeln(' <error>Error</error>');
|
||||
if ($output->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function array_merge;
|
||||
use function explode;
|
||||
use function sprintf;
|
||||
@@ -27,28 +26,16 @@ class GenerateShortUrlCommand extends Command
|
||||
public const NAME = 'short-url:generate';
|
||||
private const ALIASES = ['shortcode:generate', 'short-code:generate'];
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
/** @var UrlShortenerInterface */
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var array */
|
||||
private $domainConfig;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(
|
||||
UrlShortenerInterface $urlShortener,
|
||||
TranslatorInterface $translator,
|
||||
array $domainConfig
|
||||
) {
|
||||
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->translator = $translator;
|
||||
$this->domainConfig = $domainConfig;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -56,30 +43,40 @@ class GenerateShortUrlCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$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'))
|
||||
->setDescription('Generates a short URL for provided long URL and returns it')
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse')
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
|
||||
$this->translator->translate('Tags to apply to the new short URL')
|
||||
'Tags to apply to the new short URL'
|
||||
)
|
||||
->addOption('validSince', 's', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
->addOption(
|
||||
'validSince',
|
||||
's',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date from which this short URL will be valid. '
|
||||
. 'If someone tries to access it before this date, it will not be found.'
|
||||
))
|
||||
->addOption('validUntil', 'u', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
)
|
||||
->addOption(
|
||||
'validUntil',
|
||||
'u',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'The date until which this short URL will be valid. '
|
||||
. 'If someone tries to access it after this date, it will not be found.'
|
||||
))
|
||||
->addOption('customSlug', 'c', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
)
|
||||
->addOption(
|
||||
'customSlug',
|
||||
'c',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'If provided, this slug will be used instead of generating a short code'
|
||||
))
|
||||
->addOption('maxVisits', 'm', InputOption::VALUE_REQUIRED, $this->translator->translate(
|
||||
)
|
||||
->addOption(
|
||||
'maxVisits',
|
||||
'm',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'This will limit the number of visits for this short URL.'
|
||||
));
|
||||
);
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
@@ -90,9 +87,7 @@ class GenerateShortUrlCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$longUrl = $io->ask(
|
||||
$this->translator->translate('A long URL was not provided. Which URL do you want to be shortened?')
|
||||
);
|
||||
$longUrl = $io->ask('A long URL was not provided. Which URL do you want to be shortened?');
|
||||
if (! empty($longUrl)) {
|
||||
$input->setArgument('longUrl', $longUrl);
|
||||
}
|
||||
@@ -103,7 +98,7 @@ class GenerateShortUrlCommand extends Command
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
if (empty($longUrl)) {
|
||||
$io->error($this->translator->translate('A URL was not provided!'));
|
||||
$io->error('A URL was not provided!');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -129,21 +124,15 @@ class GenerateShortUrlCommand extends Command
|
||||
$shortUrl = $this->buildShortUrl($this->domainConfig, $shortCode);
|
||||
|
||||
$io->writeln([
|
||||
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('Processed long URL: <info>%s</info>', $longUrl),
|
||||
sprintf('Generated short URL: <info>%s</info>', $shortUrl),
|
||||
]);
|
||||
} catch (InvalidUrlException $e) {
|
||||
$io->error(sprintf(
|
||||
$this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'),
|
||||
$longUrl
|
||||
));
|
||||
$io->error(sprintf('Provided URL "%s" is invalid. Try with a different one.', $longUrl));
|
||||
} catch (NonUniqueSlugException $e) {
|
||||
$io->error(sprintf(
|
||||
$this->translator->translate(
|
||||
'Provided slug "%s" is already in use by another URL. Try with a different one.'
|
||||
),
|
||||
$customSlug
|
||||
));
|
||||
$io->error(
|
||||
sprintf('Provided slug "%s" is already in use by another URL. Try with a different one.', $customSlug)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -13,7 +14,7 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
use function array_map;
|
||||
use function Functional\select_keys;
|
||||
|
||||
@@ -22,19 +23,12 @@ class GetVisitsCommand extends Command
|
||||
public const NAME = 'short-url:visits';
|
||||
private const ALIASES = ['shortcode:visits', 'short-code:visits'];
|
||||
|
||||
/**
|
||||
* @var VisitsTrackerInterface
|
||||
*/
|
||||
/** @var VisitsTrackerInterface */
|
||||
private $visitsTracker;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
|
||||
public function __construct(VisitsTrackerInterface $visitsTracker)
|
||||
{
|
||||
$this->visitsTracker = $visitsTracker;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
@@ -43,25 +37,19 @@ class GetVisitsCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate('Returns the detailed visits information for provided short code')
|
||||
)
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code which visits we want to get')
|
||||
)
|
||||
->setDescription('Returns the detailed visits information for provided short code')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get')
|
||||
->addOption(
|
||||
'startDate',
|
||||
's',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('Allows to filter visits, returning only those older than start date')
|
||||
'Allows to filter visits, returning only those older than start date'
|
||||
)
|
||||
->addOption(
|
||||
'endDate',
|
||||
'e',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('Allows to filter visits, returning only those newer than end date')
|
||||
'Allows to filter visits, returning only those newer than end date'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,9 +61,7 @@ class GetVisitsCommand extends Command
|
||||
}
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $io->ask(
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to use?')
|
||||
);
|
||||
$shortCode = $io->ask('A short code was not provided. Which short code do you want to use?');
|
||||
if (! empty($shortCode)) {
|
||||
$input->setArgument('shortCode', $shortCode);
|
||||
}
|
||||
@@ -88,18 +74,15 @@ class GetVisitsCommand extends Command
|
||||
$startDate = $this->getDateOption($input, 'startDate');
|
||||
$endDate = $this->getDateOption($input, 'endDate');
|
||||
|
||||
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
|
||||
$paginator = $this->visitsTracker->info($shortCode, new VisitsParams(new DateRange($startDate, $endDate)));
|
||||
$visits = ArrayUtils::iteratorToArray($paginator->getCurrentItems());
|
||||
|
||||
$rows = array_map(function (Visit $visit) {
|
||||
$rowData = $visit->jsonSerialize();
|
||||
$rowData['country'] = $visit->getVisitLocation()->getCountryName();
|
||||
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
|
||||
}, $visits);
|
||||
$io->table([
|
||||
$this->translator->translate('Referer'),
|
||||
$this->translator->translate('Date'),
|
||||
$this->translator->translate('User agent'),
|
||||
$this->translator->translate('Country'),
|
||||
], $rows);
|
||||
$io->table(['Referer', 'Date', 'User agent', 'Country'], $rows);
|
||||
}
|
||||
|
||||
private function getDateOption(InputInterface $input, $key)
|
||||
|
||||
@@ -12,7 +12,6 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function array_values;
|
||||
use function count;
|
||||
use function explode;
|
||||
@@ -26,27 +25,15 @@ class ListShortUrlsCommand extends Command
|
||||
public const NAME = 'short-url:list';
|
||||
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
||||
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
*/
|
||||
/** @var ShortUrlServiceInterface */
|
||||
private $shortUrlService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var array */
|
||||
private $domainConfig;
|
||||
|
||||
public function __construct(
|
||||
ShortUrlServiceInterface $shortUrlService,
|
||||
TranslatorInterface $translator,
|
||||
array $domainConfig
|
||||
) {
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->translator = $translator;
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, array $domainConfig)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->domainConfig = $domainConfig;
|
||||
}
|
||||
|
||||
@@ -55,45 +42,33 @@ class ListShortUrlsCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription($this->translator->translate('List all short URLs'))
|
||||
->setDescription('List all short URLs')
|
||||
->addOption(
|
||||
'page',
|
||||
'p',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf(
|
||||
$this->translator->translate('The first page to list (%s items per page)'),
|
||||
PaginableRepositoryAdapter::ITEMS_PER_PAGE
|
||||
),
|
||||
sprintf('The first page to list (%s items per page)', PaginableRepositoryAdapter::ITEMS_PER_PAGE),
|
||||
'1'
|
||||
)
|
||||
->addOption(
|
||||
'searchTerm',
|
||||
's',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate(
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields'
|
||||
)
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields'
|
||||
)
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('A comma-separated list of tags to filter results')
|
||||
'A comma-separated list of tags to filter results'
|
||||
)
|
||||
->addOption(
|
||||
'orderBy',
|
||||
'o',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate(
|
||||
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
|
||||
)
|
||||
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
|
||||
)
|
||||
->addOption(
|
||||
'showTags',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate('Whether to display the tags or not')
|
||||
);
|
||||
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
@@ -110,15 +85,9 @@ class ListShortUrlsCommand extends Command
|
||||
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
|
||||
$page++;
|
||||
|
||||
$headers = [
|
||||
$this->translator->translate('Short code'),
|
||||
$this->translator->translate('Short URL'),
|
||||
$this->translator->translate('Long URL'),
|
||||
$this->translator->translate('Date created'),
|
||||
$this->translator->translate('Visits count'),
|
||||
];
|
||||
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
|
||||
if ($showTags) {
|
||||
$headers[] = $this->translator->translate('Tags');
|
||||
$headers[] = 'Tags';
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
@@ -137,12 +106,9 @@ class ListShortUrlsCommand extends Command
|
||||
|
||||
if ($this->isLastPage($result)) {
|
||||
$continue = false;
|
||||
$io->success($this->translator->translate('Short URLs properly listed'));
|
||||
$io->success('Short URLs properly listed');
|
||||
} else {
|
||||
$continue = $io->confirm(
|
||||
sprintf($this->translator->translate('Continue with page') . ' <options=bold>%s</>?', $page),
|
||||
false
|
||||
);
|
||||
$continue = $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
|
||||
}
|
||||
} while ($continue);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
|
||||
class ResolveUrlCommand extends Command
|
||||
@@ -19,20 +18,13 @@ class ResolveUrlCommand extends Command
|
||||
public const NAME = 'short-url:parse';
|
||||
private const ALIASES = ['shortcode:parse', 'short-code:parse'];
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
/** @var UrlShortenerInterface */
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
|
||||
public function __construct(UrlShortenerInterface $urlShortener)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
@@ -40,12 +32,8 @@ class ResolveUrlCommand extends Command
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code to parse')
|
||||
);
|
||||
->setDescription('Returns the long URL behind a short code')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse');
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
@@ -56,9 +44,7 @@ class ResolveUrlCommand extends Command
|
||||
}
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$shortCode = $io->ask(
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to parse?')
|
||||
);
|
||||
$shortCode = $io->ask('A short code was not provided. Which short code do you want to parse?');
|
||||
if (! empty($shortCode)) {
|
||||
$input->setArgument('shortCode', $shortCode);
|
||||
}
|
||||
@@ -71,17 +57,11 @@ class ResolveUrlCommand extends Command
|
||||
|
||||
try {
|
||||
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||
$output->writeln(
|
||||
sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $url->getLongUrl())
|
||||
);
|
||||
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
|
||||
} catch (InvalidShortCodeException $e) {
|
||||
$io->error(
|
||||
sprintf($this->translator->translate('Provided short code "%s" has an invalid format.'), $shortCode)
|
||||
);
|
||||
$io->error(sprintf('Provided short code "%s" has an invalid format.', $shortCode));
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$io->error(
|
||||
sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
|
||||
);
|
||||
$io->error(sprintf('Provided short code "%s" could not be found.', $shortCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,38 +9,30 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class CreateTagCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:create';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
/** @var TagServiceInterface */
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Creates one or more tags.'))
|
||||
->setDescription('Creates one or more tags.')
|
||||
->addOption(
|
||||
'name',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
$this->translator->translate('The name of the tags to create')
|
||||
'The name of the tags to create'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,11 +42,11 @@ class CreateTagCommand extends Command
|
||||
$tagNames = $input->getOption('name');
|
||||
|
||||
if (empty($tagNames)) {
|
||||
$io->warning($this->translator->translate('You have to provide at least one tag name'));
|
||||
$io->warning('You have to provide at least one tag name');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tagService->createTags($tagNames);
|
||||
$io->success($this->translator->translate('Tags properly created'));
|
||||
$io->success('Tags properly created');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,38 +9,30 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class DeleteTagsCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:delete';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
/** @var TagServiceInterface */
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Deletes one or more tags.'))
|
||||
->setDescription('Deletes one or more tags.')
|
||||
->addOption(
|
||||
'name',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
$this->translator->translate('The name of the tags to delete')
|
||||
'The name of the tags to delete'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,11 +42,11 @@ class DeleteTagsCommand extends Command
|
||||
$tagNames = $input->getOption('name');
|
||||
|
||||
if (empty($tagNames)) {
|
||||
$io->warning($this->translator->translate('You have to provide at least one tag name'));
|
||||
$io->warning('You have to provide at least one tag name');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tagService->deleteTags($tagNames);
|
||||
$io->success($this->translator->translate('Tags properly deleted'));
|
||||
$io->success('Tags properly deleted');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,47 +9,39 @@ use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function Functional\map;
|
||||
|
||||
class ListTagsCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:list';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
/** @var TagServiceInterface */
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Lists existing tags.'));
|
||||
->setDescription('Lists existing tags.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->table([$this->translator->translate('Name')], $this->getTagsRows());
|
||||
$io->table(['Name'], $this->getTagsRows());
|
||||
}
|
||||
|
||||
private function getTagsRows(): array
|
||||
{
|
||||
$tags = $this->tagService->listTags();
|
||||
if (empty($tags)) {
|
||||
return [[$this->translator->translate('No tags yet')]];
|
||||
return [['No tags yet']];
|
||||
}
|
||||
|
||||
return map($tags, function (Tag $tag) {
|
||||
|
||||
@@ -10,36 +10,28 @@ use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
|
||||
class RenameTagCommand extends Command
|
||||
{
|
||||
public const NAME = 'tag:rename';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
/** @var TagServiceInterface */
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
public function __construct(TagServiceInterface $tagService)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Renames one existing tag.'))
|
||||
->addArgument('oldName', InputArgument::REQUIRED, $this->translator->translate('Current name of the tag.'))
|
||||
->addArgument('newName', InputArgument::REQUIRED, $this->translator->translate('New name of the tag.'));
|
||||
->setDescription('Renames one existing tag.')
|
||||
->addArgument('oldName', InputArgument::REQUIRED, 'Current name of the tag.')
|
||||
->addArgument('newName', InputArgument::REQUIRED, 'New name of the tag.');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
@@ -50,9 +42,9 @@ class RenameTagCommand extends Command
|
||||
|
||||
try {
|
||||
$this->tagService->renameTag($oldName, $newName);
|
||||
$io->success($this->translator->translate('Tag properly renamed.'));
|
||||
$io->success('Tag properly renamed.');
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$io->error(sprintf($this->translator->translate('A tag with name "%s" was not found'), $oldName));
|
||||
$io->error(sprintf('A tag with name "%s" was not found', $oldName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,98 +6,99 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use Symfony\Component\Lock\Factory as Locker;
|
||||
use function sprintf;
|
||||
|
||||
class ProcessVisitsCommand extends Command
|
||||
{
|
||||
public const NAME = 'visit:process';
|
||||
|
||||
/**
|
||||
* @var VisitServiceInterface
|
||||
*/
|
||||
/** @var VisitServiceInterface */
|
||||
private $visitService;
|
||||
/**
|
||||
* @var IpLocationResolverInterface
|
||||
*/
|
||||
/** @var IpLocationResolverInterface */
|
||||
private $ipLocationResolver;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/** @var Locker */
|
||||
private $locker;
|
||||
/** @var OutputInterface */
|
||||
private $output;
|
||||
|
||||
public function __construct(
|
||||
VisitServiceInterface $visitService,
|
||||
IpLocationResolverInterface $ipLocationResolver,
|
||||
TranslatorInterface $translator
|
||||
Locker $locker
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->visitService = $visitService;
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->locker = $locker;
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(
|
||||
$this->translator->translate('Processes visits where location is not set yet')
|
||||
);
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Processes visits where location is not set yet');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->output = $output;
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$visits = $this->visitService->getUnlocatedVisits();
|
||||
|
||||
foreach ($visits as $visit) {
|
||||
if (! $visit->hasRemoteAddr()) {
|
||||
$io->writeln(
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||
|
||||
$location = new VisitLocation($result);
|
||||
$visit->setVisitLocation($location);
|
||||
$this->visitService->saveVisit($visit);
|
||||
|
||||
$io->writeln(sprintf(
|
||||
' [<info>' . $this->translator->translate('Address located at "%s"') . '</info>]',
|
||||
$location->getCountryName()
|
||||
));
|
||||
} catch (WrongIpException $e) {
|
||||
$io->writeln(
|
||||
sprintf(
|
||||
' [<fg=red>%s</>]',
|
||||
$this->translator->translate('An error occurred while locating IP. Skipped')
|
||||
)
|
||||
);
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
}
|
||||
$lock = $this->locker->createLock(self::NAME);
|
||||
if (! $lock->acquire()) {
|
||||
$io->warning(sprintf('There is already an instance of the "%s" command in execution', self::NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
$io->success($this->translator->translate('Finished processing all IPs'));
|
||||
try {
|
||||
$this->visitService->locateVisits(
|
||||
[$this, 'getGeolocationDataForVisit'],
|
||||
function (VisitLocation $location) use ($output) {
|
||||
$output->writeln(sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName()));
|
||||
}
|
||||
);
|
||||
|
||||
$io->success('Finished processing all IPs');
|
||||
} finally {
|
||||
$lock->release();
|
||||
}
|
||||
}
|
||||
|
||||
public function getGeolocationDataForVisit(Visit $visit): array
|
||||
{
|
||||
if (! $visit->hasRemoteAddr()) {
|
||||
$this->output->writeln(
|
||||
'<comment>Ignored visit with no IP address</comment>',
|
||||
OutputInterface::VERBOSITY_VERBOSE
|
||||
);
|
||||
throw new IpCannotBeLocatedException('Ignored visit with no IP address');
|
||||
}
|
||||
|
||||
$ipAddr = $visit->getRemoteAddr();
|
||||
$this->output->write(sprintf('Processing IP <fg=blue>%s</>', $ipAddr));
|
||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||
$this->output->writeln(' [<comment>Ignored localhost address</comment>]');
|
||||
throw new IpCannotBeLocatedException('Ignored localhost address');
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||
} catch (WrongIpException $e) {
|
||||
$this->output->writeln(' [<fg=red>An error occurred while locating IP. Skipped</>]');
|
||||
if ($this->output->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $this->output);
|
||||
}
|
||||
|
||||
throw new IpCannotBeLocatedException('An error occurred while locating IP', $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,39 +10,29 @@ 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
|
||||
*/
|
||||
/** @var DbUpdaterInterface */
|
||||
private $geoLiteDbUpdater;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
public function __construct(DbUpdaterInterface $geoLiteDbUpdater, TranslatorInterface $translator)
|
||||
public function __construct(DbUpdaterInterface $geoLiteDbUpdater)
|
||||
{
|
||||
$this->geoLiteDbUpdater = $geoLiteDbUpdater;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
$this->geoLiteDbUpdater = $geoLiteDbUpdater;
|
||||
}
|
||||
|
||||
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(
|
||||
->setDescription('Updates the GeoLite2 database file used to geolocate IP addresses')
|
||||
->setHelp(
|
||||
'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
|
||||
@@ -60,12 +50,12 @@ class UpdateDbCommand extends Command
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
|
||||
$io->success($this->translator->translate('GeoLite2 database properly updated'));
|
||||
$io->success('GeoLite2 database properly updated');
|
||||
} catch (RuntimeException $e) {
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
|
||||
$io->error($this->translator->translate('An error occurred while updating GeoLite2 database'));
|
||||
$io->error('An error occurred while updating GeoLite2 database');
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ use Psr\Container\NotFoundExceptionInterface;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
@@ -34,8 +33,6 @@ class ApplicationFactory implements FactoryInterface
|
||||
{
|
||||
$config = $container->get('config')['cli'];
|
||||
$appOptions = $container->get(AppOptions::class);
|
||||
$translator = $container->get(Translator::class);
|
||||
$translator->setLocale($config['locale']);
|
||||
|
||||
$commands = $config['commands'] ?? [];
|
||||
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());
|
||||
|
||||
@@ -10,23 +10,18 @@ use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class DisableKeyCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $apiKeyService;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $apiKeyService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||
$command = new DisableKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||
$command = new DisableKeyCommand($this->apiKeyService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
@@ -39,10 +34,14 @@ class DisableKeyCommandTest extends TestCase
|
||||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$this->apiKeyService->disable($apiKey)->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:disable',
|
||||
'apiKey' => $apiKey,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('API key "abcd1234" properly disabled', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,14 +50,15 @@ class DisableKeyCommandTest extends TestCase
|
||||
public function errorIsReturnedIfServiceThrowsException()
|
||||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class)
|
||||
->shouldBeCalledOnce();
|
||||
$disable = $this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:disable',
|
||||
'apiKey' => $apiKey,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('API key "abcd1234" does not exist.', $output);
|
||||
$disable->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,23 +12,18 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class GenerateKeyCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $apiKeyService;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $apiKeyService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||
$command = new GenerateKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||
$command = new GenerateKeyCommand($this->apiKeyService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
@@ -39,11 +34,15 @@ class GenerateKeyCommandTest extends TestCase
|
||||
*/
|
||||
public function noExpirationDateIsDefinedIfNotProvided()
|
||||
{
|
||||
$this->apiKeyService->create(null)->shouldBeCalledOnce()
|
||||
->willReturn(new ApiKey());
|
||||
$create = $this->apiKeyService->create(null)->willReturn(new ApiKey());
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('Generated API key: ', $output);
|
||||
$create->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,23 +10,18 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class ListKeysCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $apiKeyService;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $apiKeyService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||
$command = new ListKeysCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||
$command = new ListKeysCommand($this->apiKeyService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
@@ -35,30 +30,46 @@ class ListKeysCommandTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function ifEnabledOnlyIsNotProvidedEverythingIsListed()
|
||||
public function everythingIsListedIfEnabledOnlyIsNotProvided()
|
||||
{
|
||||
$this->apiKeyService->listKeys(false)->willReturn([
|
||||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
])->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:list',
|
||||
'command' => ListKeysCommand::NAME,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('Key', $output);
|
||||
$this->assertContains('Is enabled', $output);
|
||||
$this->assertContains(' +++ ', $output);
|
||||
$this->assertNotContains(' --- ', $output);
|
||||
$this->assertContains('Expiration date', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function ifEnabledOnlyIsProvidedOnlyThoseKeysAreListed()
|
||||
public function onlyEnabledKeysAreListedIfEnabledOnlyIsProvided()
|
||||
{
|
||||
$this->apiKeyService->listKeys(true)->willReturn([
|
||||
new ApiKey(),
|
||||
(new ApiKey())->disable(),
|
||||
new ApiKey(),
|
||||
])->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:list',
|
||||
'command' => ListKeysCommand::NAME,
|
||||
'--enabledOnly' => true,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('Key', $output);
|
||||
$this->assertNotContains('Is enabled', $output);
|
||||
$this->assertNotContains(' +++ ', $output);
|
||||
$this->assertNotContains(' --- ', $output);
|
||||
$this->assertContains('Expiration date', $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,21 +7,18 @@ use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use function implode;
|
||||
use function sort;
|
||||
use function str_split;
|
||||
|
||||
class GenerateCharsetCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$command = new GenerateCharsetCommand(Translator::factory([]));
|
||||
$command = new GenerateCharsetCommand();
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
|
||||
@@ -11,26 +11,21 @@ use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use function array_pop;
|
||||
use function sprintf;
|
||||
|
||||
class DeleteShortCodeCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $service;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
|
||||
|
||||
$command = new DeleteShortUrlCommand($this->service->reveal(), Translator::factory([]));
|
||||
$command = new DeleteShortUrlCommand($this->service->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
use function count;
|
||||
@@ -21,17 +20,11 @@ use function substr_count;
|
||||
|
||||
class GeneratePreviewCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $previewGenerator;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $shortUrlService;
|
||||
|
||||
public function setUp()
|
||||
@@ -39,11 +32,7 @@ class GeneratePreviewCommandTest extends TestCase
|
||||
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
|
||||
|
||||
$command = new GeneratePreviewCommand(
|
||||
$this->shortUrlService->reveal(),
|
||||
$this->previewGenerator->reveal(),
|
||||
Translator::factory([])
|
||||
);
|
||||
$command = new GeneratePreviewCommand($this->shortUrlService->reveal(), $this->previewGenerator->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
@@ -62,13 +51,22 @@ class GeneratePreviewCommandTest extends TestCase
|
||||
]);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledOnce();
|
||||
|
||||
$this->previewGenerator->generatePreview('http://foo.com')->shouldBeCalledOnce();
|
||||
$this->previewGenerator->generatePreview('https://bar.com')->shouldBeCalledOnce();
|
||||
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledOnce();
|
||||
$generatePreview1 = $this->previewGenerator->generatePreview('http://foo.com')->willReturn('');
|
||||
$generatePreview2 = $this->previewGenerator->generatePreview('https://bar.com')->willReturn('');
|
||||
$generatePreview3 = $this->previewGenerator->generatePreview('http://baz.com/something')->willReturn('');
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:process-previews',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('Processing URL http://foo.com', $output);
|
||||
$this->assertContains('Processing URL https://bar.com', $output);
|
||||
$this->assertContains('Processing URL http://baz.com/something', $output);
|
||||
$this->assertContains('Finished processing all URLs', $output);
|
||||
$generatePreview1->shouldHaveBeenCalledOnce();
|
||||
$generatePreview2->shouldHaveBeenCalledOnce();
|
||||
$generatePreview3->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,33 +3,29 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use function strpos;
|
||||
|
||||
class GenerateShortcodeCommandTest extends TestCase
|
||||
class GenerateShortUrlCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $urlShortener;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $urlShortener;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), Translator::factory([]), [
|
||||
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), [
|
||||
'schema' => 'http',
|
||||
'hostname' => 'foo.com',
|
||||
]);
|
||||
@@ -43,18 +39,19 @@ class GenerateShortcodeCommandTest extends TestCase
|
||||
*/
|
||||
public function properShortCodeIsCreatedIfLongUrlIsCorrect()
|
||||
{
|
||||
$this->urlShortener->urlToShortCode(Argument::cetera())
|
||||
->willReturn(
|
||||
(new ShortUrl(''))->setShortCode('abc123')
|
||||
)
|
||||
->shouldBeCalledOnce();
|
||||
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn(
|
||||
(new ShortUrl(''))->setShortCode('abc123')
|
||||
);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:generate',
|
||||
'longUrl' => 'http://domain.com/foo/bar',
|
||||
'--maxVisits' => '3',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(strpos($output, 'http://foo.com/abc123') > 0);
|
||||
|
||||
$this->assertContains('http://foo.com/abc123', $output);
|
||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,4 +72,29 @@ class GenerateShortcodeCommandTest extends TestCase
|
||||
$output
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function properlyProcessesProvidedTags()
|
||||
{
|
||||
$urlToShortCode = $this->urlShortener->urlToShortCode(
|
||||
Argument::type(UriInterface::class),
|
||||
Argument::that(function (array $tags) {
|
||||
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $tags);
|
||||
return $tags;
|
||||
}),
|
||||
Argument::cetera()
|
||||
)->willReturn((new ShortUrl(''))->setShortCode('abc123'));
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:generate',
|
||||
'longUrl' => 'http://domain.com/foo/bar',
|
||||
'--tags' => ['foo,bar', 'baz', 'boo,zar'],
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('http://foo.com/abc123', $output);
|
||||
$urlToShortCode->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
@@ -13,27 +13,24 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use function strpos;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class GetVisitsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $visitsTracker;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $visitsTracker;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->visitsTracker = $this->prophesize(VisitsTrackerInterface::class);
|
||||
$command = new GetVisitsCommand($this->visitsTracker->reveal(), Translator::factory([]));
|
||||
$command = new GetVisitsCommand($this->visitsTracker->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
@@ -45,8 +42,9 @@ class GetVisitsCommandTest extends TestCase
|
||||
public function noDateFlagsTriesToListWithoutDateRange()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([])
|
||||
->shouldBeCalledOnce();
|
||||
$this->visitsTracker->info($shortCode, new VisitsParams(new DateRange(null, null)))->willReturn(
|
||||
new Paginator(new ArrayAdapter([]))
|
||||
)->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
@@ -62,8 +60,11 @@ class GetVisitsCommandTest extends TestCase
|
||||
$shortCode = 'abc123';
|
||||
$startDate = '2016-01-01';
|
||||
$endDate = '2016-02-01';
|
||||
$this->visitsTracker->info($shortCode, new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
|
||||
->willReturn([])
|
||||
$this->visitsTracker->info(
|
||||
$shortCode,
|
||||
new VisitsParams(new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
|
||||
)
|
||||
->willReturn(new Paginator(new ArrayAdapter([])))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
@@ -80,19 +81,21 @@ class GetVisitsCommandTest extends TestCase
|
||||
public function outputIsProperlyGenerated()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, Argument::any())->willReturn([
|
||||
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->setVisitLocation(
|
||||
new VisitLocation(['country_name' => 'Spain'])
|
||||
),
|
||||
])->shouldBeCalledOnce();
|
||||
$this->visitsTracker->info($shortCode, Argument::any())->willReturn(
|
||||
new Paginator(new ArrayAdapter([
|
||||
(new Visit(new ShortUrl(''), new Visitor('bar', 'foo', '')))->locate(
|
||||
new VisitLocation(['country_name' => 'Spain'])
|
||||
),
|
||||
]))
|
||||
)->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertGreaterThan(0, strpos($output, 'foo'));
|
||||
$this->assertGreaterThan(0, strpos($output, 'Spain'));
|
||||
$this->assertGreaterThan(0, strpos($output, 'bar'));
|
||||
$this->assertContains('foo', $output);
|
||||
$this->assertContains('Spain', $output);
|
||||
$this->assertContains('bar', $output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,26 +11,21 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class ListShortcodesCommandTest extends TestCase
|
||||
class ListShortUrlsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $shortUrlService;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $shortUrlService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
|
||||
$app = new Application();
|
||||
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), Translator::factory([]), []);
|
||||
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), []);
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
@@ -52,7 +47,7 @@ class ListShortcodesCommandTest extends TestCase
|
||||
*/
|
||||
public function loadingMorePagesCallsListMoreTimes()
|
||||
{
|
||||
// The paginator will return more than one page for the first 3 times
|
||||
// The paginator will return more than one page
|
||||
$data = [];
|
||||
for ($i = 0; $i < 50; $i++) {
|
||||
$data[] = new ShortUrl('url_' . $i);
|
||||
@@ -64,6 +59,11 @@ class ListShortcodesCommandTest extends TestCase
|
||||
|
||||
$this->commandTester->setInputs(['y', 'y', 'n']);
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('Continue with page 2?', $output);
|
||||
$this->assertContains('Continue with page 3?', $output);
|
||||
$this->assertContains('Continue with page 4?', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -77,11 +77,20 @@ class ListShortcodesCommandTest extends TestCase
|
||||
$data[] = new ShortUrl('url_' . $i);
|
||||
}
|
||||
|
||||
$this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data)))
|
||||
->shouldBeCalledOnce();
|
||||
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter($data)))
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('url_1', $output);
|
||||
$this->assertContains('url_9', $output);
|
||||
$this->assertNotContains('url_10', $output);
|
||||
$this->assertNotContains('url_20', $output);
|
||||
$this->assertNotContains('url_30', $output);
|
||||
$this->assertContains('Continue with page 2?', $output);
|
||||
$this->assertNotContains('Continue with page 3?', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -12,24 +12,19 @@ use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use const PHP_EOL;
|
||||
|
||||
class ResolveUrlCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $urlShortener;
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/** @var ObjectProphecy */
|
||||
private $urlShortener;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||
$command = new ResolveUrlCommand($this->urlShortener->reveal(), Translator::factory([]));
|
||||
$command = new ResolveUrlCommand($this->urlShortener->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
|
||||
@@ -11,24 +11,19 @@ use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class CreateTagCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new CreateTagCommand($this->tagService->reveal(), Translator::factory([]));
|
||||
$command = new CreateTagCommand($this->tagService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
|
||||
@@ -10,28 +10,21 @@ use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class DeleteTagsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DeleteTagsCommand
|
||||
*/
|
||||
/** @var DeleteTagsCommand */
|
||||
private $command;
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new DeleteTagsCommand($this->tagService->reveal(), Translator::factory([]));
|
||||
$command = new DeleteTagsCommand($this->tagService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
|
||||
@@ -11,28 +11,21 @@ use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class ListTagsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ListTagsCommand
|
||||
*/
|
||||
/** @var ListTagsCommand */
|
||||
private $command;
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new ListTagsCommand($this->tagService->reveal(), Translator::factory([]));
|
||||
$command = new ListTagsCommand($this->tagService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
|
||||
@@ -12,28 +12,21 @@ use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class RenameTagCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var RenameTagCommand
|
||||
*/
|
||||
/** @var RenameTagCommand */
|
||||
private $command;
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new RenameTagCommand($this->tagService->reveal(), Translator::factory([]));
|
||||
$command = new RenameTagCommand($this->tagService->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
|
||||
@@ -7,41 +7,52 @@ use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\IpGeolocation\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Exception\IpCannotBeLocatedException;
|
||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use function count;
|
||||
use Symfony\Component\Lock;
|
||||
use Throwable;
|
||||
use function array_shift;
|
||||
use function sprintf;
|
||||
|
||||
class ProcessVisitsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $visitService;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $ipResolver;
|
||||
/** @var ObjectProphecy */
|
||||
private $locker;
|
||||
/** @var ObjectProphecy */
|
||||
private $lock;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->visitService = $this->prophesize(VisitService::class);
|
||||
$this->ipResolver = $this->prophesize(IpApiLocationResolver::class);
|
||||
|
||||
$this->locker = $this->prophesize(Lock\Factory::class);
|
||||
$this->lock = $this->prophesize(Lock\LockInterface::class);
|
||||
$this->lock->acquire()->willReturn(true);
|
||||
$this->lock->release()->will(function () {
|
||||
});
|
||||
$this->locker->createLock(Argument::type('string'))->willReturn($this->lock->reveal());
|
||||
|
||||
$command = new ProcessVisitsCommand(
|
||||
$this->visitService->reveal(),
|
||||
$this->ipResolver->reveal(),
|
||||
Translator::factory([])
|
||||
$this->locker->reveal()
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
@@ -52,59 +63,131 @@ class ProcessVisitsCommandTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function allReturnedVisitsIpsAreProcessed()
|
||||
public function allPendingVisitsAreProcessed()
|
||||
{
|
||||
$shortUrl = new ShortUrl('');
|
||||
$visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
|
||||
$location = new VisitLocation([]);
|
||||
|
||||
$visits = [
|
||||
new Visit($shortUrl, new Visitor('', '', '1.2.3.4')),
|
||||
new Visit($shortUrl, new Visitor('', '', '4.3.2.1')),
|
||||
new Visit($shortUrl, new Visitor('', '', '12.34.56.78')),
|
||||
];
|
||||
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
||||
->shouldBeCalledOnce();
|
||||
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
|
||||
function (array $args) use ($visit, $location) {
|
||||
$firstCallback = array_shift($args);
|
||||
$firstCallback($visit);
|
||||
|
||||
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits));
|
||||
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||
->shouldBeCalledTimes(count($visits));
|
||||
$secondCallback = array_shift($args);
|
||||
$secondCallback($location, $visit);
|
||||
}
|
||||
);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('Processing IP 1.2.3.0', $output);
|
||||
$this->assertContains('Processing IP 4.3.2.0', $output);
|
||||
$this->assertContains('Processing IP 12.34.56.0', $output);
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideIgnoredAddresses
|
||||
*/
|
||||
public function localhostAndEmptyAddressesAreIgnored(?string $address, string $message)
|
||||
{
|
||||
$visit = new Visit(new ShortUrl(''), new Visitor('', '', $address));
|
||||
$location = new VisitLocation([]);
|
||||
|
||||
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
|
||||
function (array $args) use ($visit, $location) {
|
||||
$firstCallback = array_shift($args);
|
||||
$firstCallback($visit);
|
||||
|
||||
$secondCallback = array_shift($args);
|
||||
$secondCallback($location, $visit);
|
||||
}
|
||||
);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
|
||||
|
||||
try {
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
} catch (Throwable $e) {
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertInstanceOf(IpCannotBeLocatedException::class, $e);
|
||||
|
||||
$this->assertContains($message, $output);
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldNotHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
||||
public function provideIgnoredAddresses(): array
|
||||
{
|
||||
return [
|
||||
['', 'Ignored visit with no IP address'],
|
||||
[null, 'Ignored visit with no IP address'],
|
||||
[IpAddress::LOCALHOST, 'Ignored localhost address'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function localhostAndEmptyAddressIsIgnored()
|
||||
public function errorWhileLocatingIpIsDisplayed()
|
||||
{
|
||||
$shortUrl = new ShortUrl('');
|
||||
$visit = new Visit(new ShortUrl(''), new Visitor('', '', '1.2.3.4'));
|
||||
$location = new VisitLocation([]);
|
||||
|
||||
$visits = [
|
||||
new Visit($shortUrl, new Visitor('', '', '1.2.3.4')),
|
||||
new Visit($shortUrl, new Visitor('', '', '4.3.2.1')),
|
||||
new Visit($shortUrl, new Visitor('', '', '12.34.56.78')),
|
||||
new Visit($shortUrl, new Visitor('', '', '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)
|
||||
->shouldBeCalledOnce();
|
||||
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(
|
||||
function (array $args) use ($visit, $location) {
|
||||
$firstCallback = array_shift($args);
|
||||
$firstCallback($visit);
|
||||
|
||||
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits) - 4);
|
||||
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||
->shouldBeCalledTimes(count($visits) - 4);
|
||||
$secondCallback = array_shift($args);
|
||||
$secondCallback($location, $visit);
|
||||
}
|
||||
);
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willThrow(WrongIpException::class);
|
||||
|
||||
try {
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
} catch (Throwable $e) {
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertInstanceOf(IpCannotBeLocatedException::class, $e);
|
||||
|
||||
$this->assertContains('An error occurred while locating IP. Skipped', $output);
|
||||
$locateVisits->shouldHaveBeenCalledOnce();
|
||||
$resolveIpLocation->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function noActionIsPerformedIfLockIsAcquired()
|
||||
{
|
||||
$this->lock->acquire()->willReturn(false);
|
||||
|
||||
$locateVisits = $this->visitService->locateVisits(Argument::cetera())->will(function () {
|
||||
});
|
||||
$resolveIpLocation = $this->ipResolver->resolveIpLocation(Argument::any())->willReturn([]);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertContains('Ignored localhost address', $output);
|
||||
$this->assertContains('Ignored visit with no IP address', $output);
|
||||
|
||||
$this->assertContains(
|
||||
sprintf('There is already an instance of the "%s" command', ProcessVisitsCommand::NAME),
|
||||
$output
|
||||
);
|
||||
$locateVisits->shouldNotHaveBeenCalled();
|
||||
$resolveIpLocation->shouldNotHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,24 +11,19 @@ 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
|
||||
*/
|
||||
/** @var CommandTester */
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
/** @var ObjectProphecy */
|
||||
private $dbUpdater;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
|
||||
|
||||
$command = new UpdateDbCommand($this->dbUpdater->reveal(), Translator::factory([]));
|
||||
$command = new UpdateDbCommand($this->dbUpdater->reveal());
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
|
||||
@@ -8,10 +8,8 @@ use Shlinkio\Shlink\CLI\ConfigProvider;
|
||||
|
||||
class ConfigProviderTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ConfigProvider
|
||||
*/
|
||||
protected $configProvider;
|
||||
/** @var ConfigProvider */
|
||||
private $configProvider;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
@@ -23,10 +21,9 @@ class ConfigProviderTest extends TestCase
|
||||
*/
|
||||
public function confiIsProperlyReturned()
|
||||
{
|
||||
$config = $this->configProvider->__invoke();
|
||||
$config = ($this->configProvider)();
|
||||
|
||||
$this->assertArrayHasKey('cli', $config);
|
||||
$this->assertArrayHasKey('dependencies', $config);
|
||||
$this->assertArrayHasKey('translator', $config);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,19 @@ declare(strict_types=1);
|
||||
namespace ShlinkioTest\Shlink\CLI\Factory;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
use function array_merge;
|
||||
|
||||
class ApplicationFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ApplicationFactory
|
||||
*/
|
||||
protected $factory;
|
||||
/** @var ApplicationFactory */
|
||||
private $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
@@ -29,7 +28,7 @@ class ApplicationFactoryTest extends TestCase
|
||||
*/
|
||||
public function serviceIsCreated()
|
||||
{
|
||||
$instance = $this->factory->__invoke($this->createServiceManager(), '');
|
||||
$instance = ($this->factory)($this->createServiceManager(), '');
|
||||
$this->assertInstanceOf(Application::class, $instance);
|
||||
}
|
||||
|
||||
@@ -40,28 +39,43 @@ class ApplicationFactoryTest extends TestCase
|
||||
{
|
||||
$sm = $this->createServiceManager([
|
||||
'commands' => [
|
||||
'foo',
|
||||
'bar',
|
||||
'baz',
|
||||
'foo' => 'foo',
|
||||
'bar' => 'bar',
|
||||
'baz' => 'baz',
|
||||
],
|
||||
]);
|
||||
$sm->setService('foo', $this->prophesize(Command::class)->reveal());
|
||||
$sm->setService('baz', $this->prophesize(Command::class)->reveal());
|
||||
$sm->setService('foo', $this->createCommandMock('foo')->reveal());
|
||||
$sm->setService('bar', $this->createCommandMock('bar')->reveal());
|
||||
|
||||
/** @var Application $instance */
|
||||
$instance = $this->factory->__invoke($sm, '');
|
||||
$instance = ($this->factory)($sm, '');
|
||||
$this->assertInstanceOf(Application::class, $instance);
|
||||
$this->assertCount(2, $instance->all());
|
||||
|
||||
$this->assertTrue($instance->has('foo'));
|
||||
$this->assertTrue($instance->has('bar'));
|
||||
$this->assertFalse($instance->has('baz'));
|
||||
}
|
||||
|
||||
protected function createServiceManager($config = [])
|
||||
private function createServiceManager(array $config = []): ServiceManager
|
||||
{
|
||||
return new ServiceManager(['services' => [
|
||||
'config' => [
|
||||
'cli' => array_merge($config, ['locale' => 'en']),
|
||||
],
|
||||
AppOptions::class => new AppOptions(),
|
||||
Translator::class => Translator::factory([]),
|
||||
]]);
|
||||
}
|
||||
|
||||
private function createCommandMock(string $name): ObjectProphecy
|
||||
{
|
||||
$command = $this->prophesize(Command::class);
|
||||
$command->getName()->willReturn($name);
|
||||
$command->getDefinition()->willReturn($name);
|
||||
$command->isEnabled()->willReturn(true);
|
||||
$command->getAliases()->willReturn([]);
|
||||
$command->setApplication(Argument::type(Application::class))->willReturn(function () {
|
||||
});
|
||||
|
||||
return $command;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ return [
|
||||
EntityManager::class => Factory\EntityManagerFactory::class,
|
||||
GuzzleClient::class => InvokableFactory::class,
|
||||
Cache::class => Factory\CacheFactory::class,
|
||||
'Logger_Shlink' => Factory\LoggerFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
Reader::class => ConfigAbstractFactory::class,
|
||||
|
||||
@@ -31,6 +30,7 @@ return [
|
||||
Template\Extension\TranslatorExtension::class => ConfigAbstractFactory::class,
|
||||
|
||||
Middleware\LocaleMiddleware::class => ConfigAbstractFactory::class,
|
||||
Middleware\CloseDbConnectionMiddleware::class => ConfigAbstractFactory::class,
|
||||
IpAddress::class => Middleware\IpAddressMiddlewareFactory::class,
|
||||
|
||||
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
||||
@@ -78,6 +78,7 @@ return [
|
||||
|
||||
Template\Extension\TranslatorExtension::class => ['translator'],
|
||||
Middleware\LocaleMiddleware::class => ['translator'],
|
||||
Middleware\CloseDbConnectionMiddleware::class => ['em'],
|
||||
|
||||
IpGeolocation\IpApiLocationResolver::class => ['httpClient'],
|
||||
IpGeolocation\GeoLite2LocationResolver::class => [Reader::class],
|
||||
|
||||
@@ -9,9 +9,7 @@ use function is_array;
|
||||
|
||||
final class PathCollection
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var array */
|
||||
private $array;
|
||||
|
||||
public function __construct(array $array)
|
||||
|
||||
18
module/Common/src/Exec/ExecutionContext.php
Normal file
18
module/Common/src/Exec/ExecutionContext.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Exec;
|
||||
|
||||
use const PHP_SAPI;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
abstract class ExecutionContext
|
||||
{
|
||||
public const WEB = 'shlink_web';
|
||||
public const CLI = 'shlink_cli';
|
||||
|
||||
public static function currentContextIsSwoole(): bool
|
||||
{
|
||||
return PHP_SAPI === 'cli' && env('CURRENT_SHLINK_CONTEXT', self::WEB) === self::WEB;
|
||||
}
|
||||
}
|
||||
@@ -45,11 +45,7 @@ class CacheFactory implements FactoryInterface
|
||||
return $adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
* @return Cache\CacheProvider
|
||||
*/
|
||||
protected function getAdapter(ContainerInterface $container)
|
||||
private function getAdapter(ContainerInterface $container): Cache\CacheProvider
|
||||
{
|
||||
// Try to get the adapter from config
|
||||
$config = $container->get('config');
|
||||
@@ -61,11 +57,7 @@ class CacheFactory implements FactoryInterface
|
||||
return env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $cacheConfig
|
||||
* @return Cache\CacheProvider
|
||||
*/
|
||||
protected function resolveCacheAdapter(array $cacheConfig)
|
||||
private function resolveCacheAdapter(array $cacheConfig): Cache\CacheProvider
|
||||
{
|
||||
switch ($cacheConfig['adapter']) {
|
||||
case Cache\ArrayCache::class:
|
||||
|
||||
@@ -29,10 +29,10 @@ class LoggerFactory implements FactoryInterface
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->has('config') ? $container->get('config') : [];
|
||||
Cascade::fileConfig(isset($config['logger']) ? $config['logger'] : ['loggers' => []]);
|
||||
Cascade::fileConfig($config['logger'] ?? ['loggers' => []]);
|
||||
|
||||
// Compose requested logger name
|
||||
$loggerName = isset($options) & isset($options['logger_name']) ? $options['logger_name'] : 'Logger';
|
||||
$loggerName = $options['logger_name'] ?? 'Logger';
|
||||
$nameParts = explode('_', $requestedName);
|
||||
if (count($nameParts) > 1) {
|
||||
$loggerName = $nameParts[1];
|
||||
|
||||
@@ -27,6 +27,6 @@ class TranslatorFactory implements FactoryInterface
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->get('config');
|
||||
return Translator::factory(isset($config['translator']) ? $config['translator'] : []);
|
||||
return Translator::factory($config['translator'] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,9 @@ class ImageFactory implements FactoryInterface
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->get('config')['phpwkhtmltopdf'];
|
||||
$image = new Image(isset($config['images']) ? $config['images'] : null);
|
||||
$image = new Image($config['images'] ?? null);
|
||||
|
||||
if (isset($options) && isset($options['url'])) {
|
||||
if ($options['url'] ?? null) {
|
||||
$image->setPage($options['url']);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,7 @@ use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
class ChainIpLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @var IpLocationResolverInterface[]
|
||||
*/
|
||||
/** @var IpLocationResolverInterface[] */
|
||||
private $resolvers;
|
||||
|
||||
public function __construct(IpLocationResolverInterface ...$resolvers)
|
||||
|
||||
@@ -19,17 +19,11 @@ class DbUpdater implements DbUpdaterInterface
|
||||
private const DB_COMPRESSED_FILE = 'GeoLite2-City.tar.gz';
|
||||
private const DB_DECOMPRESSED_FILE = 'GeoLite2-City.mmdb';
|
||||
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
/** @var ClientInterface */
|
||||
private $httpClient;
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
/** @var Filesystem */
|
||||
private $filesystem;
|
||||
/**
|
||||
* @var GeoLite2Options
|
||||
*/
|
||||
/** @var GeoLite2Options */
|
||||
private $options;
|
||||
|
||||
public function __construct(ClientInterface $httpClient, Filesystem $filesystem, GeoLite2Options $options)
|
||||
|
||||
@@ -13,9 +13,7 @@ use function Functional\first;
|
||||
|
||||
class GeoLite2LocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @var Reader
|
||||
*/
|
||||
/** @var Reader */
|
||||
private $geoLiteDbReader;
|
||||
|
||||
public function __construct(Reader $geoLiteDbReader)
|
||||
|
||||
@@ -14,9 +14,7 @@ class IpApiLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
private const SERVICE_PATTERN = 'http://ip-api.com/json/%s';
|
||||
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
/** @var Client */
|
||||
private $httpClient;
|
||||
|
||||
public function __construct(Client $httpClient)
|
||||
|
||||
51
module/Common/src/Logger/Swoole/AccessLogFactory.php
Normal file
51
module/Common/src/Logger/Swoole/AccessLogFactory.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Logger\Swoole;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Zend\Expressive\Swoole\Log;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class AccessLogFactory 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 ServiceNotCreatedException if an exception is raised when creating a service.
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->has('config') ? $container->get('config') : [];
|
||||
$config = $config['zend-expressive-swoole']['swoole-http-server']['logger'] ?? [];
|
||||
|
||||
return new Log\Psr3AccessLogDecorator(
|
||||
$this->getLogger($container, $config),
|
||||
$this->getFormatter($container, $config),
|
||||
$config['use-hostname-lookups'] ?? false
|
||||
);
|
||||
}
|
||||
|
||||
private function getLogger(ContainerInterface $container, array $config): LoggerInterface
|
||||
{
|
||||
$loggerName = $config['logger_name'] ?? LoggerInterface::class;
|
||||
return $container->has($loggerName) ? $container->get($loggerName) : new Log\StdoutLogger();
|
||||
}
|
||||
|
||||
private function getFormatter(ContainerInterface $container, array $config): Log\AccessLogFormatterInterface
|
||||
{
|
||||
if ($container->has(Log\AccessLogFormatterInterface::class)) {
|
||||
return $container->get(Log\AccessLogFormatterInterface::class);
|
||||
}
|
||||
|
||||
return new Log\AccessLogFormatter($config['format'] ?? Log\AccessLogFormatter::FORMAT_COMMON);
|
||||
}
|
||||
}
|
||||
34
module/Common/src/Middleware/CloseDbConnectionMiddleware.php
Normal file
34
module/Common/src/Middleware/CloseDbConnectionMiddleware.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Middleware;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
|
||||
class CloseDbConnectionMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/** @var EntityManagerInterface */
|
||||
private $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming server request and return a response, optionally delegating
|
||||
* response creation to a handler.
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$handledRequest = $handler->handle($request);
|
||||
$this->em->getConnection()->close();
|
||||
$this->em->clear();
|
||||
|
||||
return $handledRequest;
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,9 @@ use function explode;
|
||||
|
||||
class LocaleMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var Translator
|
||||
*/
|
||||
private const ACCEPT_LANGUAGE = 'Accept-Language';
|
||||
|
||||
/** @var Translator */
|
||||
private $translator;
|
||||
|
||||
public function __construct(Translator $translator)
|
||||
@@ -36,20 +36,16 @@ class LocaleMiddleware implements MiddlewareInterface
|
||||
*/
|
||||
public function process(Request $request, DelegateInterface $delegate): Response
|
||||
{
|
||||
if (! $request->hasHeader('Accept-Language')) {
|
||||
if (! $request->hasHeader(self::ACCEPT_LANGUAGE)) {
|
||||
return $delegate->handle($request);
|
||||
}
|
||||
|
||||
$locale = $request->getHeaderLine('Accept-Language');
|
||||
$locale = $request->getHeaderLine(self::ACCEPT_LANGUAGE);
|
||||
$this->translator->setLocale($this->normalizeLocale($locale));
|
||||
return $delegate->handle($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $locale
|
||||
* @return string
|
||||
*/
|
||||
protected function normalizeLocale($locale)
|
||||
private function normalizeLocale(string $locale): string
|
||||
{
|
||||
$parts = explode('_', $locale);
|
||||
if (count($parts) > 1) {
|
||||
|
||||
@@ -12,21 +12,13 @@ class PaginableRepositoryAdapter implements AdapterInterface
|
||||
{
|
||||
public const ITEMS_PER_PAGE = 10;
|
||||
|
||||
/**
|
||||
* @var PaginableRepositoryInterface
|
||||
*/
|
||||
/** @var PaginableRepositoryInterface */
|
||||
private $paginableRepository;
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
/** @var null|string */
|
||||
private $searchTerm;
|
||||
/**
|
||||
* @var null|array|string
|
||||
*/
|
||||
/** @var null|array|string */
|
||||
private $orderBy;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
/** @var array */
|
||||
private $tags;
|
||||
|
||||
public function __construct(
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Response;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface as StatusCode;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\Stream;
|
||||
@@ -13,7 +14,7 @@ class PixelResponse extends Response
|
||||
private const BASE_64_IMAGE = 'R0lGODlhAQABAJAAAP8AAAAAACH5BAUQAAAALAAAAAABAAEAAAICBAEAOw==';
|
||||
private const CONTENT_TYPE = 'image/gif';
|
||||
|
||||
public function __construct(int $status = 200, array $headers = [])
|
||||
public function __construct(int $status = StatusCode::STATUS_OK, array $headers = [])
|
||||
{
|
||||
$headers['content-type'] = self::CONTENT_TYPE;
|
||||
parent::__construct($this->createBody(), $status, $headers);
|
||||
@@ -27,7 +28,7 @@ class PixelResponse extends Response
|
||||
private function createBody(): StreamInterface
|
||||
{
|
||||
$body = new Stream('php://temp', 'wb+');
|
||||
$body->write((string) base64_decode(self::BASE_64_IMAGE));
|
||||
$body->write(base64_decode(self::BASE_64_IMAGE));
|
||||
$body->rewind();
|
||||
return $body;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Common\Response;
|
||||
|
||||
use Endroid\QrCode\QrCode;
|
||||
use Fig\Http\Message\StatusCodeInterface as StatusCode;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\Stream;
|
||||
@@ -12,7 +13,7 @@ class QrCodeResponse extends Response
|
||||
{
|
||||
use Response\InjectContentTypeTrait;
|
||||
|
||||
public function __construct(QrCode $qrCode, $status = 200, array $headers = [])
|
||||
public function __construct(QrCode $qrCode, int $status = StatusCode::STATUS_OK, array $headers = [])
|
||||
{
|
||||
parent::__construct(
|
||||
$this->createBody($qrCode),
|
||||
@@ -21,13 +22,7 @@ class QrCodeResponse extends Response
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the message body.
|
||||
*
|
||||
* @param QrCode $qrCode
|
||||
* @return StreamInterface
|
||||
*/
|
||||
private function createBody(QrCode $qrCode)
|
||||
private function createBody(QrCode $qrCode): StreamInterface
|
||||
{
|
||||
$body = new Stream('php://temp', 'wb+');
|
||||
$body->write($qrCode->get());
|
||||
|
||||
@@ -12,17 +12,11 @@ use function urlencode;
|
||||
|
||||
class PreviewGenerator implements PreviewGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
private $location;
|
||||
/**
|
||||
* @var ImageBuilderInterface
|
||||
*/
|
||||
/** @var ImageBuilderInterface */
|
||||
private $imageBuilder;
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
/** @var Filesystem */
|
||||
private $filesystem;
|
||||
|
||||
public function __construct(ImageBuilderInterface $imageBuilder, Filesystem $filesystem, $location)
|
||||
@@ -35,11 +29,9 @@ class PreviewGenerator implements PreviewGeneratorInterface
|
||||
/**
|
||||
* Generates and stores preview for provided website and returns the path to the image file
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws PreviewGenerationException
|
||||
*/
|
||||
public function generatePreview($url)
|
||||
public function generatePreview(string $url): string
|
||||
{
|
||||
/** @var Image $image */
|
||||
$image = $this->imageBuilder->build(Image::class, ['url' => $url]);
|
||||
|
||||
@@ -10,9 +10,7 @@ interface PreviewGeneratorInterface
|
||||
/**
|
||||
* Generates and stores preview for provided website and returns the path to the image file
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws PreviewGenerationException
|
||||
*/
|
||||
public function generatePreview($url);
|
||||
public function generatePreview(string $url): string;
|
||||
}
|
||||
|
||||
@@ -9,9 +9,7 @@ use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class TranslatorExtension implements ExtensionInterface
|
||||
{
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
/** @var TranslatorInterface */
|
||||
private $translator;
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
@@ -19,9 +17,8 @@ class TranslatorExtension implements ExtensionInterface
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
public function register(Engine $engine)
|
||||
public function register(Engine $engine): void
|
||||
{
|
||||
$engine->registerFunction('translate', [$this->translator, 'translate']);
|
||||
$engine->registerFunction('translate_plural', [$this->translator, 'translatePlural']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,9 @@ use Cake\Chronos\Chronos;
|
||||
|
||||
final class DateRange
|
||||
{
|
||||
/**
|
||||
* @var Chronos|null
|
||||
*/
|
||||
/** @var Chronos|null */
|
||||
private $startDate;
|
||||
/**
|
||||
* @var Chronos|null
|
||||
*/
|
||||
/** @var Chronos|null */
|
||||
private $endDate;
|
||||
|
||||
public function __construct(?Chronos $startDate = null, ?Chronos $endDate = null)
|
||||
|
||||
@@ -15,21 +15,13 @@ final class IpAddress
|
||||
private const OBFUSCATED_OCTET = '0';
|
||||
public const LOCALHOST = '127.0.0.1';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
private $firstOctet;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
private $secondOctet;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
private $thirdOctet;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
/** @var string */
|
||||
private $fourthOctet;
|
||||
|
||||
private function __construct(string $firstOctet, string $secondOctet, string $thirdOctet, string $fourthOctet)
|
||||
|
||||
@@ -3,37 +3,25 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Util;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface as StatusCode;
|
||||
use finfo;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\Stream;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
use const FILEINFO_MIME;
|
||||
use function basename;
|
||||
|
||||
trait ResponseUtilsTrait
|
||||
{
|
||||
protected function generateDownloadFileResponse(string $filePath): ResponseInterface
|
||||
{
|
||||
return $this->generateBinaryResponse($filePath, [
|
||||
'Content-Disposition' => 'attachment; filename=' . basename($filePath),
|
||||
'Content-Transfer-Encoding' => 'Binary',
|
||||
'Content-Description' => 'File Transfer',
|
||||
'Pragma' => 'public',
|
||||
'Expires' => '0',
|
||||
'Cache-Control' => 'must-revalidate',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function generateImageResponse(string $imagePath): ResponseInterface
|
||||
private function generateImageResponse(string $imagePath): ResponseInterface
|
||||
{
|
||||
return $this->generateBinaryResponse($imagePath);
|
||||
}
|
||||
|
||||
protected function generateBinaryResponse(string $path, array $extraHeaders = []): ResponseInterface
|
||||
private function generateBinaryResponse(string $path, array $extraHeaders = []): ResponseInterface
|
||||
{
|
||||
$body = new Stream($path);
|
||||
return new Response($body, 200, ArrayUtils::merge([
|
||||
return new Response($body, StatusCode::STATUS_OK, ArrayUtils::merge([
|
||||
'Content-Type' => (new finfo(FILEINFO_MIME))->file($path),
|
||||
'Content-Length' => (string) $body->getSize(),
|
||||
], $extraHeaders));
|
||||
|
||||
@@ -9,7 +9,7 @@ use function strlen;
|
||||
|
||||
trait StringUtilsTrait
|
||||
{
|
||||
private function generateRandomString($length = 10): string
|
||||
private function generateRandomString(int $length = 10): string
|
||||
{
|
||||
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
$charactersLength = strlen($characters);
|
||||
|
||||
@@ -10,9 +10,7 @@ abstract class DatabaseTestCase extends TestCase
|
||||
{
|
||||
protected const ENTITIES_TO_EMPTY = [];
|
||||
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
/** @var EntityManagerInterface */
|
||||
public static $em;
|
||||
|
||||
protected function getEntityManager(): EntityManagerInterface
|
||||
|
||||
@@ -8,9 +8,7 @@ use Shlinkio\Shlink\Common\Collection\PathCollection;
|
||||
|
||||
class PathCollectionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var PathCollection
|
||||
*/
|
||||
/** @var PathCollection */
|
||||
private $collection;
|
||||
|
||||
public function setUp()
|
||||
|
||||
@@ -8,10 +8,8 @@ use Shlinkio\Shlink\Common\ConfigProvider;
|
||||
|
||||
class ConfigProviderTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ConfigProvider
|
||||
*/
|
||||
protected $configProvider;
|
||||
/** @var ConfigProvider */
|
||||
private $configProvider;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
|
||||
35
module/Common/test/Exception/WrongIpExceptionTest.php
Normal file
35
module/Common/test/Exception/WrongIpExceptionTest.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Exception;
|
||||
|
||||
use Exception;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
class WrongIpExceptionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function fromIpAddressProperlyCreatesExceptionWithoutPrev()
|
||||
{
|
||||
$e = WrongIpException::fromIpAddress('1.2.3.4');
|
||||
|
||||
$this->assertEquals('Provided IP "1.2.3.4" is invalid', $e->getMessage());
|
||||
$this->assertEquals(0, $e->getCode());
|
||||
$this->assertNull($e->getPrevious());
|
||||
}
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function fromIpAddressProperlyCreatesExceptionWithPrev()
|
||||
{
|
||||
$prev = new Exception('Previous error');
|
||||
$e = WrongIpException::fromIpAddress('1.2.3.4', $prev);
|
||||
|
||||
$this->assertEquals('Provided IP "1.2.3.4" is invalid', $e->getMessage());
|
||||
$this->assertEquals(0, $e->getCode());
|
||||
$this->assertSame($prev, $e->getPrevious());
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,8 @@ use function sys_get_temp_dir;
|
||||
|
||||
class CacheFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CacheFactory
|
||||
*/
|
||||
protected $factory;
|
||||
/** @var CacheFactory */
|
||||
private $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
|
||||
@@ -11,9 +11,7 @@ use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class DottedAccessConfigAbstractFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DottedAccessConfigAbstractFactory
|
||||
*/
|
||||
/** @var DottedAccessConfigAbstractFactory */
|
||||
private $factory;
|
||||
|
||||
public function setUp()
|
||||
|
||||
@@ -12,10 +12,8 @@ use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var EmptyResponseImplicitOptionsMiddlewareFactory
|
||||
*/
|
||||
protected $factory;
|
||||
/** @var EmptyResponseImplicitOptionsMiddlewareFactory */
|
||||
private $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
|
||||
@@ -10,10 +10,8 @@ use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class EntityManagerFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var EntityManagerFactory
|
||||
*/
|
||||
protected $factory;
|
||||
/** @var EntityManagerFactory */
|
||||
private $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user