mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-05 06:43:12 +08:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47117d1fb7 | ||
|
|
cb8ef408a4 | ||
|
|
e5f21a88fa | ||
|
|
0458c4f798 | ||
|
|
75f6160432 | ||
|
|
5337eb48e7 | ||
|
|
86c30ee731 | ||
|
|
d68dc38959 | ||
|
|
0525639329 | ||
|
|
0d9c7282df | ||
|
|
3b95925217 | ||
|
|
fa595f7aa3 | ||
|
|
ff80f32f72 | ||
|
|
e55dbef2fc | ||
|
|
ebf2e459e8 | ||
|
|
1b5081ae21 | ||
|
|
d5736756f7 | ||
|
|
757cf2e193 | ||
|
|
3a75ac0486 | ||
|
|
3c3ef6fa05 | ||
|
|
3282bfd03b | ||
|
|
0813df6b29 | ||
|
|
df74a04085 | ||
|
|
8323b87076 | ||
|
|
48f01921e1 | ||
|
|
ae9d99257e | ||
|
|
0183c8a4b7 | ||
|
|
9a2ca35e6e | ||
|
|
2edb48e314 | ||
|
|
a81fd497d4 | ||
|
|
49cca5cd69 | ||
|
|
f92cff6241 | ||
|
|
1b4343ffc2 | ||
|
|
d5392a5f59 | ||
|
|
a65ce649ac | ||
|
|
d5dc6cea99 | ||
|
|
5ecfe9f0f0 | ||
|
|
0f5fb066d1 | ||
|
|
8e61639598 | ||
|
|
e88468d867 | ||
|
|
bc46e2f509 | ||
|
|
2241279bb6 | ||
|
|
25ffbed756 | ||
|
|
8784843a7a | ||
|
|
a964e2b3c9 | ||
|
|
7f7efd45ab | ||
|
|
af8f5afef8 | ||
|
|
dcfaed437c | ||
|
|
47e2322e33 | ||
|
|
00e7d57245 | ||
|
|
d53a3222d0 | ||
|
|
80fe3a73e2 | ||
|
|
7ab993b764 | ||
|
|
622edd2ed1 | ||
|
|
1f5faee356 | ||
|
|
076b0cf867 | ||
|
|
d4168bebc6 | ||
|
|
13c3629cd6 | ||
|
|
1eff9801e8 |
20
.travis.yml
20
.travis.yml
@@ -1,5 +1,7 @@
|
||||
language: php
|
||||
|
||||
sudo: false # Use containerized environment
|
||||
|
||||
branches:
|
||||
only:
|
||||
- /.*/
|
||||
@@ -12,7 +14,7 @@ before_install:
|
||||
- phpenv config-add data/infra/travis-php/memcached.ini
|
||||
- phpenv config-add data/infra/travis-php/apcu.ini
|
||||
|
||||
before_script:
|
||||
install:
|
||||
- composer self-update
|
||||
- composer install --no-interaction
|
||||
|
||||
@@ -20,9 +22,21 @@ script:
|
||||
- mkdir build
|
||||
- composer check
|
||||
|
||||
after_script:
|
||||
after_success:
|
||||
- vendor/bin/phpcov merge build --clover build/clover.xml
|
||||
- wget https://scrutinizer-ci.com/ocular.phar
|
||||
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
|
||||
|
||||
sudo: false
|
||||
# Before deploying, build dist file for current travis tag
|
||||
before_deploy:
|
||||
- ./build.sh ${TRAVIS_TAG#?}
|
||||
|
||||
deploy:
|
||||
provider: releases
|
||||
api_key:
|
||||
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
|
||||
file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip"
|
||||
skip_cleanup: true
|
||||
on:
|
||||
tags: true
|
||||
php: 7.1
|
||||
|
||||
44
CHANGELOG.md
44
CHANGELOG.md
@@ -1,5 +1,49 @@
|
||||
# CHANGELOG
|
||||
|
||||
## 1.13.0 - 2018-10-06
|
||||
|
||||
#### Added
|
||||
|
||||
* [#197](https://github.com/shlinkio/shlink/issues/197) Added [cakephp/chronos](https://book.cakephp.org/3.0/en/chronos.html) library for date manipulations.
|
||||
* [#214](https://github.com/shlinkio/shlink/issues/214) Improved build script, which allows builds to be done without "jumping" outside the project directory, and generates smaller dist files.
|
||||
|
||||
It also allows automating the dist file generation in travis-ci builds.
|
||||
|
||||
* [#207](https://github.com/shlinkio/shlink/issues/207) Added two new config options which are asked during installation process. The config options already existed in previous shlink version, but you had to manually set their values.
|
||||
|
||||
These are the new options:
|
||||
|
||||
* Visits threshold to allow short URLs to be deleted.
|
||||
* Check the visits threshold when trying to delete a short URL via REST API.
|
||||
|
||||
#### Changed
|
||||
|
||||
* [#211](https://github.com/shlinkio/shlink/issues/211) Extracted installer to its own module, which will simplify moving it to a separated package in the future.
|
||||
* [#200](https://github.com/shlinkio/shlink/issues/200) and [#201](https://github.com/shlinkio/shlink/issues/201) Renamed REST Action classes and CLI Command classes to use the concept of `ShortUrl` instead of the concept of `ShortCode` when referring to the entity, and left the `short code` concept to the identifier which is used as a unique code for a specific `Short URL`.
|
||||
* [#181](https://github.com/shlinkio/shlink/issues/181) When importing the configuration from a previous shlink installation, it no longer asks to import every block. Instead, it is capable of detecting only new config options introduced in the new version, and ask only for those.
|
||||
|
||||
If no new options are found and you have selected to import config, no further questions will be asked and shlink will just import the old config.
|
||||
|
||||
#### Deprecated
|
||||
|
||||
* [#205](https://github.com/shlinkio/shlink/issues/205) Deprecated `[POST /authenticate]` endpoint, and allowed any API request to be automatically authenticated using the `X-Api-Key` header with a valid API key.
|
||||
|
||||
This effectively deprecates the `Authorization: Bearer <JWT>` authentication form, but it will keep working.
|
||||
|
||||
* As of [#200](https://github.com/shlinkio/shlink/issues/200) and [#201](https://github.com/shlinkio/shlink/issues/201) REST urls have changed from `/short-codes/...` to `/short-urls/...`, and the command namespaces have changed from `short-code:...` to `short-url:...`.
|
||||
|
||||
In both cases, backwards compatibility has been retained and the old ones are aliases for the new ones, but the old ones are considered deprecated.
|
||||
|
||||
#### Removed
|
||||
|
||||
* *Nothing*
|
||||
|
||||
#### Fixed
|
||||
|
||||
* [#203](https://github.com/shlinkio/shlink/issues/203) Fixed some warnings thrown while unzipping distributable files.
|
||||
* [#206](https://github.com/shlinkio/shlink/issues/206) An error is now thrown during installation if any required param is left empty, making the installer display a message and ask again until a value is set.
|
||||
|
||||
|
||||
## 1.12.0 - 2018-09-15
|
||||
|
||||
#### Added
|
||||
|
||||
@@ -37,13 +37,13 @@ Then, you will need a built version of the project. There are a few ways to get
|
||||
|
||||
After that, you will have a `shlink_x.x.x_dist.zip` dist file inside the `build` directory.
|
||||
|
||||
This is the process used when releasing new shlink versions. After tagging the new version with git, the Github release is created, attaching generated dist file to it.
|
||||
This is the process used when releasing new shlink versions. After tagging the new version with git, the Github release is automatically created by [travis](https://travis-ci.org/shlinkio/shlink), attaching generated dist file to it.
|
||||
|
||||
Despite how you built the project, you are going to need to install it now, by following these steps:
|
||||
|
||||
* 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 will guide you through the installation process.
|
||||
* Setup the application by running the `bin/install` script. It is a command line tool that will guide you through the installation process. **Take into account that this tool has to be run directly on the server where you plan to host Shlink. Do not run it before uploading/moving it there.**
|
||||
* Configure the web server of your choice to serve shlink using your short domain.
|
||||
|
||||
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.
|
||||
|
||||
29
bin/install
29
bin/install
@@ -1,29 +1,12 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizer;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$container = new ServiceManager([
|
||||
'factories' => [
|
||||
Application::class => InstallApplicationFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
],
|
||||
'services' => [
|
||||
'config' => [
|
||||
ConfigAbstractFactory::class => [
|
||||
DatabaseConfigCustomizer::class => [Filesystem::class]
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
/** @var ServiceLocatorInterface $container */
|
||||
$container = include __DIR__ . '/../config/install-container.php';
|
||||
$container->build(Application::class)->run();
|
||||
|
||||
29
bin/update
29
bin/update
@@ -1,29 +1,12 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizer;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$container = new ServiceManager([
|
||||
'factories' => [
|
||||
Application::class => InstallApplicationFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
],
|
||||
'services' => [
|
||||
'config' => [
|
||||
ConfigAbstractFactory::class => [
|
||||
DatabaseConfigCustomizer::class => [Filesystem::class]
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
/** @var ServiceLocatorInterface $container */
|
||||
$container = include __DIR__ . '/../config/install-container.php';
|
||||
$container->build(Application::class, ['isUpdate' => true])->run();
|
||||
|
||||
52
build.sh
52
build.sh
@@ -8,53 +8,51 @@ if [ "$#" -ne 1 ]; then
|
||||
fi
|
||||
|
||||
version=$1
|
||||
builtcontent=$(readlink -f "../shlink_${version}_dist")
|
||||
builtcontent="./build/shlink_${version}_dist"
|
||||
projectdir=$(pwd)
|
||||
[ -f ./composer.phar ] && composerBin='./composer.phar' || composerBin='composer'
|
||||
|
||||
# Copy project content to temp dir
|
||||
echo 'Copying project files...'
|
||||
rm -rf "${builtcontent}"
|
||||
mkdir "${builtcontent}"
|
||||
sudo chmod -R 777 "${projectdir}"/data/infra/{database,nginx}
|
||||
cp -R "${projectdir}"/* "${builtcontent}"
|
||||
mkdir -p "${builtcontent}"
|
||||
rsync -av * "${builtcontent}" \
|
||||
--exclude=data/infra \
|
||||
--exclude=**/.gitignore \
|
||||
--exclude=CHANGELOG.md \
|
||||
--exclude=composer.lock \
|
||||
--exclude=vendor \
|
||||
--exclude=docs \
|
||||
--exclude=indocker \
|
||||
--exclude=docker* \
|
||||
--exclude=func_tests_bootstrap.php \
|
||||
--exclude=php* \
|
||||
--exclude=infection.json \
|
||||
--exclude=phpstan.neon \
|
||||
--exclude=config/autoload/*local* \
|
||||
--exclude=**/test* \
|
||||
--exclude=build*
|
||||
cd "${builtcontent}"
|
||||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies with $composerBin..."
|
||||
rm -rf vendor
|
||||
rm -f composer.lock
|
||||
$composerBin self-update
|
||||
$composerBin install --no-dev --optimize-autoloader --no-progress --no-interaction
|
||||
${composerBin} self-update
|
||||
${composerBin} install --no-dev --optimize-autoloader --no-progress --no-interaction
|
||||
|
||||
# Delete development files
|
||||
echo 'Deleting dev files...'
|
||||
rm build.sh
|
||||
rm CHANGELOG.md
|
||||
rm composer.*
|
||||
rm LICENSE
|
||||
rm indocker
|
||||
rm docker-compose.yml
|
||||
rm docker-compose.override.yml
|
||||
rm docker-compose.override.yml.dist
|
||||
rm func_tests_bootstrap.php
|
||||
rm php*
|
||||
rm README.md
|
||||
rm infection.json
|
||||
rm -rf build
|
||||
rm -ff data/database.sqlite
|
||||
rm -rf data/infra
|
||||
rm -rf data/{cache,log,proxies}/{*,.gitignore}
|
||||
rm -rf config/params/{*,.gitignore}
|
||||
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}
|
||||
rm -f data/database.sqlite
|
||||
|
||||
# Update shlink version in config
|
||||
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
||||
|
||||
# Compressing file
|
||||
echo 'Compressing files...'
|
||||
rm -f "${projectdir}"/build/shlink_${version}_dist.zip
|
||||
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip "../shlink_${version}_dist"
|
||||
cd "${projectdir}"/build
|
||||
rm -f ./shlink_${version}_dist.zip
|
||||
zip -ry ./shlink_${version}_dist.zip ./shlink_${version}_dist
|
||||
cd "${projectdir}"
|
||||
rm -rf "${builtcontent}"
|
||||
|
||||
echo 'Done!'
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"acelaya/ze-content-based-error-handler": "^2.2",
|
||||
"cakephp/chronos": "^1.2",
|
||||
"cocur/slugify": "^3.0",
|
||||
"doctrine/cache": "^1.6",
|
||||
"doctrine/migrations": "^1.4",
|
||||
@@ -26,13 +27,13 @@
|
||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||
"monolog/monolog": "^1.21",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"symfony/console": "^4.0",
|
||||
"symfony/console": "^4.0 <4.1.5",
|
||||
"symfony/filesystem": "^4.0",
|
||||
"symfony/process": "^4.0",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"zendframework/zend-config": "^3.0",
|
||||
"zendframework/zend-config-aggregator": "^1.0",
|
||||
"zendframework/zend-diactoros": "^1.7",
|
||||
"zendframework/zend-diactoros": "^2.0",
|
||||
"zendframework/zend-expressive": "^3.0",
|
||||
"zendframework/zend-expressive-fastroute": "^3.0",
|
||||
"zendframework/zend-expressive-helpers": "^5.0",
|
||||
@@ -61,7 +62,8 @@
|
||||
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
|
||||
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
|
||||
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
|
||||
"Shlinkio\\Shlink\\Common\\": "module/Common/src"
|
||||
"Shlinkio\\Shlink\\Common\\": "module/Common/src",
|
||||
"Shlinkio\\Shlink\\Installer\\": "module/Installer/src"
|
||||
},
|
||||
"files": [
|
||||
"module/Common/functions/functions.php"
|
||||
@@ -78,7 +80,8 @@
|
||||
"ShlinkioTest\\Shlink\\Common\\": [
|
||||
"module/Common/test",
|
||||
"module/Common/test-func"
|
||||
]
|
||||
],
|
||||
"ShlinkioTest\\Shlink\\Installer\\": "module/Installer/test"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
@@ -86,30 +89,44 @@
|
||||
"@cs",
|
||||
"@stan",
|
||||
"@test",
|
||||
"@func-test",
|
||||
"@infect"
|
||||
],
|
||||
|
||||
"cs": "phpcs",
|
||||
"cs-fix": "phpcbf",
|
||||
"serve": "php -S 0.0.0.0:8000 -t public/",
|
||||
"test": "phpunit --coverage-php build/coverage-unit.cov",
|
||||
"pretty-test": "phpunit --coverage-html build/coverage",
|
||||
"func-test": "phpunit -c phpunit-func.xml --coverage-php build/coverage-func.cov",
|
||||
"complete-pretty-test": [
|
||||
"@test",
|
||||
"@func-test",
|
||||
"cs:fix": "phpcbf",
|
||||
"stan": "phpstan analyse module/*/src/ --level=6 -c phpstan.neon",
|
||||
|
||||
"test": [
|
||||
"@test:unit",
|
||||
"@test:func"
|
||||
],
|
||||
"test:unit": "phpunit --coverage-php build/coverage-unit.cov",
|
||||
"test:func": "phpunit -c phpunit-func.xml --coverage-php build/coverage-func.cov",
|
||||
|
||||
"test:pretty": [
|
||||
"@test:unit",
|
||||
"@test:func",
|
||||
"phpcov merge build --html build/html"
|
||||
],
|
||||
"stan": "phpstan analyse module/*/src/ --level=6 -c phpstan.neon",
|
||||
"infect": "infection --threads=4 --min-msi=55 --only-covered --log-verbosity=2",
|
||||
"infect-show": "infection --threads=4 --min-msi=55 --only-covered --log-verbosity=2 --show-mutations",
|
||||
"expressive": "expressive"
|
||||
"test:unit:pretty": "phpunit --coverage-html build/coverage",
|
||||
|
||||
"infect": "infection --threads=4 --min-msi=60 --only-covered --log-verbosity=2",
|
||||
"infect:show": "infection --threads=4 --min-msi=60 --only-covered --log-verbosity=2 --show-mutations"
|
||||
},
|
||||
"scripts-descriptions": {
|
||||
"check": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"test\" and \"infect\"</>",
|
||||
"cs": "<fg=blue;options=bold>Checks coding styles</>",
|
||||
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
|
||||
"stan": "<fg=blue;options=bold>Inspects code with phpstan</>",
|
||||
"test": "<fg=blue;options=bold>Runs all test suites</>",
|
||||
"test:unit": "<fg=blue;options=bold>Runs unit test suites</>",
|
||||
"test:func": "<fg=blue;options=bold>Runs functional test suites (covering entity repositories)</>",
|
||||
"test:pretty": "<fg=blue;options=bold>Runs all test suites and generates an HTML code coverage report</>",
|
||||
"test:unit:pretty": "<fg=blue;options=bold>Runs unit test suites and generates an HTML code coverage report</>",
|
||||
"infect": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing</>",
|
||||
"infect:show": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing and shows applied mutators</>"
|
||||
},
|
||||
"config": {
|
||||
"process-timeout": 0,
|
||||
"sort-packages": true,
|
||||
"platform": {
|
||||
"php": "7.1.8"
|
||||
}
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
||||
use Shlinkio\Shlink\Core\Response\NotFoundHandler;
|
||||
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware;
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Zend\Expressive;
|
||||
use Zend\Stratigility\Middleware\ErrorHandler;
|
||||
|
||||
@@ -17,14 +13,15 @@ return [
|
||||
'middleware' => [
|
||||
ErrorHandler::class,
|
||||
Expressive\Helper\ContentLengthMiddleware::class,
|
||||
LocaleMiddleware::class,
|
||||
Common\Middleware\LocaleMiddleware::class,
|
||||
],
|
||||
'priority' => 11,
|
||||
'priority' => 12,
|
||||
],
|
||||
'pre-routing-rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
PathVersionMiddleware::class,
|
||||
Rest\Middleware\PathVersionMiddleware::class,
|
||||
Rest\Middleware\ShortUrl\ShortCodePathMiddleware::class,
|
||||
],
|
||||
'priority' => 11,
|
||||
],
|
||||
@@ -39,10 +36,10 @@ return [
|
||||
'rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
CrossDomainMiddleware::class,
|
||||
Rest\Middleware\CrossDomainMiddleware::class,
|
||||
Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
|
||||
BodyParserMiddleware::class,
|
||||
CheckAuthenticationMiddleware::class,
|
||||
Rest\Middleware\BodyParserMiddleware::class,
|
||||
Rest\Middleware\AuthenticationMiddleware::class,
|
||||
],
|
||||
'priority' => 5,
|
||||
],
|
||||
@@ -50,7 +47,7 @@ return [
|
||||
'post-routing' => [
|
||||
'middleware' => [
|
||||
Expressive\Router\Middleware\DispatchMiddleware::class,
|
||||
NotFoundHandler::class,
|
||||
Core\Response\NotFoundHandler::class,
|
||||
],
|
||||
'priority' => 1,
|
||||
],
|
||||
|
||||
@@ -1,23 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Acelaya\ExpressiveErrorHandler;
|
||||
use Shlinkio\Shlink\CLI;
|
||||
use Shlinkio\Shlink\Common;
|
||||
use Shlinkio\Shlink\Core;
|
||||
use Shlinkio\Shlink\Rest;
|
||||
use Zend\ConfigAggregator;
|
||||
use Zend\Expressive;
|
||||
|
||||
/**
|
||||
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
|
||||
* then ``local.php`` and finally ``*.local.php``. This way local settings overwrite global settings.
|
||||
*
|
||||
* The configuration can be cached. This can be done by setting ``config_cache_enabled`` to ``true``.
|
||||
*
|
||||
* Obviously, if you use closures in your config you can't cache it.
|
||||
*/
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator([
|
||||
Expressive\ConfigProvider::class,
|
||||
Expressive\Router\ConfigProvider::class,
|
||||
@@ -31,6 +20,7 @@ return (new ConfigAggregator\ConfigAggregator([
|
||||
Common\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
CLI\ConfigProvider::class,
|
||||
Installer\ConfigProvider::class,
|
||||
Rest\ConfigProvider::class,
|
||||
new ConfigAggregator\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
|
||||
], 'data/cache/app_config.php'))->getMergedConfig();
|
||||
|
||||
29
config/install-container.php
Normal file
29
config/install-container.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\DatabaseConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Factory\InstallApplicationFactory;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$container = new ServiceManager([
|
||||
'factories' => [
|
||||
Application::class => InstallApplicationFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
],
|
||||
'services' => [
|
||||
'config' => [
|
||||
ConfigAbstractFactory::class => [
|
||||
DatabaseConfigCustomizer::class => [Filesystem::class],
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
return $container;
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM php:7.1-fpm-alpine
|
||||
FROM php:7.1.22-fpm-alpine
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
RUN apk update
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"post": {
|
||||
"deprecated": true,
|
||||
"operationId": "authenticate",
|
||||
"tags": [
|
||||
"Authentication"
|
||||
],
|
||||
"summary": "Perform authentication",
|
||||
"description": "Performs an authentication",
|
||||
"summary": "[Deprecated] Perform authentication",
|
||||
"description": "**This endpoint is deprecated, since the authentication can be performed via API key now**. Performs an authentication.",
|
||||
"requestBody": {
|
||||
"description": "Request body.",
|
||||
"required": true,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "listShortUrls",
|
||||
"tags": [
|
||||
"ShortCodes"
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "List short URLs",
|
||||
"description": "Returns the list of short codes",
|
||||
"description": "Returns the list of short URLs.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "page",
|
||||
@@ -53,6 +54,9 @@
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
@@ -142,12 +146,16 @@
|
||||
},
|
||||
|
||||
"post": {
|
||||
"operationId": "createShortUrl",
|
||||
"tags": [
|
||||
"ShortCodes"
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Create short URL",
|
||||
"description": "Creates a new short code",
|
||||
"description": "Creates a new short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "shortenUrl",
|
||||
"tags": [
|
||||
"ShortCodes"
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Create a short URL",
|
||||
"description": "Creates a short URL in a single API call. Useful for third party integrations",
|
||||
"description": "Creates a short URL in a single API call. Useful for third party integrations.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "apiKey",
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "getShortUrl",
|
||||
"tags": [
|
||||
"ShortCodes"
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Parse short code",
|
||||
"description": "Get the long URL behind a short code.",
|
||||
"description": "Get the long URL behind a short URL's short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
@@ -17,6 +18,9 @@
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
@@ -78,11 +82,12 @@
|
||||
},
|
||||
|
||||
"put": {
|
||||
"operationId": "editShortUrl",
|
||||
"tags": [
|
||||
"ShortCodes"
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Edit short code",
|
||||
"description": "Update certain meta arguments from an existing short URL.",
|
||||
"summary": "Edit short URL",
|
||||
"description": "Update certain meta arguments from an existing short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
@@ -120,6 +125,9 @@
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
@@ -162,11 +170,12 @@
|
||||
},
|
||||
|
||||
"delete": {
|
||||
"operationId": "deleteShortUrl",
|
||||
"tags": [
|
||||
"ShortCodes"
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Delete short code",
|
||||
"description": "Deletes the short URL for provided short code.",
|
||||
"summary": "Delete short URL",
|
||||
"description": "Deletes the short URL for provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
@@ -179,13 +188,16 @@
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The short code has been properly deleted."
|
||||
"description": "The short URL has been properly deleted."
|
||||
},
|
||||
"400": {
|
||||
"description": "The visits threshold in shlink does not allow this short URL to be deleted.",
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"put": {
|
||||
"operationId": "editShortUrlTags",
|
||||
"tags": [
|
||||
"ShortCodes",
|
||||
"Tags"
|
||||
"Short URLs"
|
||||
],
|
||||
"summary": "Edit tags on short URL",
|
||||
"description": "Edit the tags on provided short code.",
|
||||
"description": "Edit the tags on URL identified by provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The shortCode in which we want to edit tags.",
|
||||
"description": "The short code for the short URL in which we want to edit tags.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
@@ -41,6 +41,9 @@
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "getShortUrlVisits",
|
||||
"tags": [
|
||||
"ShortCodes",
|
||||
"Visits"
|
||||
],
|
||||
"summary": "List visits for short URL",
|
||||
"description": "Get the list of visits on provided short code.",
|
||||
"description": "Get the list of visits on the short URL behind provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "shortCode",
|
||||
"in": "path",
|
||||
"description": "The shortCode from which we want to get the visits.",
|
||||
"description": "The short code for the short URL from which we want to get the visits.",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
@@ -36,6 +36,9 @@
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"get": {
|
||||
"operationId": "listTags",
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "List existing tags",
|
||||
"description": "Returns the list of all tags used in any short URL, ordered by name",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
@@ -60,12 +64,16 @@
|
||||
},
|
||||
|
||||
"post": {
|
||||
"operationId": "createTags",
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Create tags",
|
||||
"description": "Provided a list of tags, creates all that do not yet exist",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
@@ -143,12 +151,16 @@
|
||||
},
|
||||
|
||||
"put": {
|
||||
"operationId": "renameTag",
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Rename tag",
|
||||
"description": "Renames one existing tag",
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
@@ -216,6 +228,7 @@
|
||||
},
|
||||
|
||||
"delete": {
|
||||
"operationId": "deleteTags",
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
@@ -236,6 +249,9 @@
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
{
|
||||
"ApiKey": []
|
||||
},
|
||||
{
|
||||
"Bearer": []
|
||||
}
|
||||
|
||||
@@ -23,8 +23,14 @@
|
||||
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"ApiKey": {
|
||||
"description": "A valid shlink API key",
|
||||
"type": "apiKey",
|
||||
"in": "header",
|
||||
"name": "X-Api-Key"
|
||||
},
|
||||
"Bearer": {
|
||||
"description": "The JWT identifying a previously logged API key",
|
||||
"description": "**[Deprecated]** The JWT identifying a previously authenticated API key",
|
||||
"type": "http",
|
||||
"scheme": "bearer",
|
||||
"bearerFormat": "JWT"
|
||||
@@ -32,30 +38,49 @@
|
||||
}
|
||||
},
|
||||
|
||||
"paths": {
|
||||
"/v1/authenticate": {
|
||||
"$ref": "paths/v1_authenticate.json"
|
||||
"tags": [
|
||||
{
|
||||
"name": "Short URLs",
|
||||
"description": "Operations that can be performed on short URLs"
|
||||
},
|
||||
{
|
||||
"name": "Tags",
|
||||
"description": "Let you handle the list of available tags"
|
||||
},
|
||||
{
|
||||
"name": "Visits",
|
||||
"description": "Operations to manage visits on short URLs"
|
||||
},
|
||||
{
|
||||
"name": "Authentication",
|
||||
"description": "Authentication-related endpoints"
|
||||
}
|
||||
],
|
||||
|
||||
"/v1/short-codes": {
|
||||
"$ref": "paths/v1_short-codes.json"
|
||||
"paths": {
|
||||
"/v1/short-urls": {
|
||||
"$ref": "paths/v1_short-urls.json"
|
||||
},
|
||||
"/v1/short-codes/shorten": {
|
||||
"$ref": "paths/v1_short-codes_shorten.json"
|
||||
"/v1/short-urls/shorten": {
|
||||
"$ref": "paths/v1_short-urls_shorten.json"
|
||||
},
|
||||
"/v1/short-codes/{shortCode}": {
|
||||
"$ref": "paths/v1_short-codes_{shortCode}.json"
|
||||
"/v1/short-urls/{shortCode}": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}.json"
|
||||
},
|
||||
"/v1/short-codes/{shortCode}/tags": {
|
||||
"$ref": "paths/v1_short-codes_{shortCode}_tags.json"
|
||||
"/v1/short-urls/{shortCode}/tags": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_tags.json"
|
||||
},
|
||||
|
||||
"/v1/tags": {
|
||||
"$ref": "paths/v1_tags.json"
|
||||
},
|
||||
|
||||
"/v1/short-codes/{shortCode}/visits": {
|
||||
"$ref": "paths/v1_short-codes_{shortCode}_visits.json"
|
||||
"/v1/short-urls/{shortCode}/visits": {
|
||||
"$ref": "paths/v1_short-urls_{shortCode}_visits.json"
|
||||
},
|
||||
|
||||
"/v1/authenticate": {
|
||||
"$ref": "paths/v1_authenticate.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command;
|
||||
use Shlinkio\Shlink\Common;
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
return [
|
||||
|
||||
'cli' => [
|
||||
'locale' => Common\env('CLI_LOCALE', 'en'),
|
||||
'locale' => env('CLI_LOCALE', 'en'),
|
||||
'commands' => [
|
||||
Command\Shortcode\GenerateShortcodeCommand::NAME => Command\Shortcode\GenerateShortcodeCommand::class,
|
||||
Command\Shortcode\ResolveUrlCommand::NAME => Command\Shortcode\ResolveUrlCommand::class,
|
||||
Command\Shortcode\ListShortcodesCommand::NAME => Command\Shortcode\ListShortcodesCommand::class,
|
||||
Command\Shortcode\GetVisitsCommand::NAME => Command\Shortcode\GetVisitsCommand::class,
|
||||
Command\Shortcode\GeneratePreviewCommand::NAME => Command\Shortcode\GeneratePreviewCommand::class,
|
||||
Command\Shortcode\DeleteShortCodeCommand::NAME => Command\Shortcode\DeleteShortCodeCommand::class,
|
||||
Command\ShortUrl\GenerateShortUrlCommand::NAME => Command\ShortUrl\GenerateShortUrlCommand::class,
|
||||
Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,
|
||||
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
|
||||
Command\ShortUrl\GetVisitsCommand::NAME => Command\ShortUrl\GetVisitsCommand::class,
|
||||
Command\ShortUrl\GeneratePreviewCommand::NAME => Command\ShortUrl\GeneratePreviewCommand::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::NAME => Command\Visit\ProcessVisitsCommand::class,
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\CLI\Command;
|
||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
use Shlinkio\Shlink\Common\Service\IpApiLocationResolver;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Service;
|
||||
@@ -15,14 +15,14 @@ return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Application::class => ApplicationFactory::class,
|
||||
Application::class => Factory\ApplicationFactory::class,
|
||||
|
||||
Command\Shortcode\GenerateShortcodeCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Shortcode\ResolveUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Shortcode\ListShortcodesCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Shortcode\GetVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Shortcode\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Shortcode\DeleteShortCodeCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
@@ -41,24 +41,24 @@ return [
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Command\Shortcode\GenerateShortcodeCommand::class => [
|
||||
Command\ShortUrl\GenerateShortUrlCommand::class => [
|
||||
Service\UrlShortener::class,
|
||||
'translator',
|
||||
'config.url_shortener.domain',
|
||||
],
|
||||
Command\Shortcode\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'],
|
||||
Command\Shortcode\ListShortcodesCommand::class => [
|
||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'],
|
||||
Command\ShortUrl\ListShortUrlsCommand::class => [
|
||||
Service\ShortUrlService::class,
|
||||
'translator',
|
||||
'config.url_shortener.domain',
|
||||
],
|
||||
Command\Shortcode\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'],
|
||||
Command\Shortcode\GeneratePreviewCommand::class => [
|
||||
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'],
|
||||
Command\ShortUrl\GeneratePreviewCommand::class => [
|
||||
Service\ShortUrlService::class,
|
||||
PreviewGenerator::class,
|
||||
'translator',
|
||||
],
|
||||
Command\Shortcode\DeleteShortCodeCommand::class => [
|
||||
Command\ShortUrl\DeleteShortUrlCommand::class => [
|
||||
Service\ShortUrl\DeleteShortUrlService::class,
|
||||
'translator',
|
||||
],
|
||||
|
||||
Binary file not shown.
@@ -1,8 +1,8 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2018-09-15 17:57+0200\n"
|
||||
"PO-Revision-Date: 2018-09-15 18:02+0200\n"
|
||||
"POT-Creation-Date: 2018-09-16 18:36+0200\n"
|
||||
"PO-Revision-Date: 2018-09-16 18:37+0200\n"
|
||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es_ES\n"
|
||||
@@ -83,8 +83,8 @@ msgstr "Clave secreta: \"%s\""
|
||||
msgid "Deletes a short URL"
|
||||
msgstr "Elimina una URL"
|
||||
|
||||
msgid "The short code to be deleted"
|
||||
msgstr "El código corto a eliminar"
|
||||
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 "
|
||||
@@ -135,9 +135,8 @@ msgstr " <info>¡Correcto!</info>"
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
msgid "Generates a short code for provided URL and returns the short URL"
|
||||
msgstr ""
|
||||
"Genera un código corto para la URL proporcionada y devuelve la URL acortada"
|
||||
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"
|
||||
@@ -268,8 +267,8 @@ msgstr "Número de visitas"
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
msgid "Short codes properly listed"
|
||||
msgstr "Códigos cortos correctamente listados"
|
||||
msgid "Short URLs properly listed"
|
||||
msgstr "URLs cortas listadas correctamente"
|
||||
|
||||
msgid "Continue with page"
|
||||
msgstr "Continuar con la página"
|
||||
|
||||
@@ -10,10 +10,11 @@ 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
|
||||
{
|
||||
const NAME = 'api-key:disable';
|
||||
public const NAME = 'api-key:disable';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
@@ -31,14 +32,14 @@ class DisableKeyCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
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'));
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$apiKey = $input->getArgument('apiKey');
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
@@ -10,10 +11,11 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
|
||||
class GenerateKeyCommand extends Command
|
||||
{
|
||||
const NAME = 'api-key:generate';
|
||||
public const NAME = 'api-key:generate';
|
||||
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
@@ -31,7 +33,7 @@ class GenerateKeyCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Generates a new valid API key.'))
|
||||
@@ -43,10 +45,10 @@ class GenerateKeyCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$expirationDate = $input->getOption('expirationDate');
|
||||
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? new \DateTime($expirationDate) : null);
|
||||
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? Chronos::parse($expirationDate) : null);
|
||||
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey)
|
||||
|
||||
@@ -11,6 +11,8 @@ 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 sprintf;
|
||||
|
||||
class ListKeysCommand extends Command
|
||||
{
|
||||
@@ -36,7 +38,7 @@ class ListKeysCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Lists all the available API keys.'))
|
||||
@@ -48,7 +50,7 @@ class ListKeysCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$enabledOnly = $input->getOption('enabledOnly');
|
||||
@@ -62,16 +64,16 @@ class ListKeysCommand extends Command
|
||||
$messagePattern = $this->determineMessagePattern($row);
|
||||
|
||||
// Set columns for this row
|
||||
$rowData = [\sprintf($messagePattern, $key)];
|
||||
$rowData = [sprintf($messagePattern, $key)];
|
||||
if (! $enabledOnly) {
|
||||
$rowData[] = \sprintf($messagePattern, $this->getEnabledSymbol($row));
|
||||
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($row));
|
||||
}
|
||||
$rowData[] = $expiration !== null ? $expiration->format(\DateTime::ATOM) : '-';
|
||||
$rowData[] = $expiration !== null ? $expiration->toAtomString() : '-';
|
||||
|
||||
$rows[] = $rowData;
|
||||
}
|
||||
|
||||
$io->table(\array_filter([
|
||||
$io->table(array_filter([
|
||||
$this->translator->translate('Key'),
|
||||
! $enabledOnly ? $this->translator->translate('Is enabled') : null,
|
||||
$this->translator->translate('Expiration date'),
|
||||
|
||||
@@ -9,10 +9,12 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
use function str_shuffle;
|
||||
|
||||
class GenerateCharsetCommand extends Command
|
||||
{
|
||||
const NAME = 'config:generate-charset';
|
||||
public const NAME = 'config:generate-charset';
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
@@ -25,7 +27,7 @@ class GenerateCharsetCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(sprintf($this->translator->translate(
|
||||
@@ -34,11 +36,11 @@ class GenerateCharsetCommand extends Command
|
||||
), UrlShortener::DEFAULT_CHARS));
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
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)
|
||||
sprintf($this->translator->translate('Character set: "%s"'), $charSet)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,13 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
use function sprintf;
|
||||
|
||||
class GenerateSecretCommand extends Command
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
const NAME = 'config:generate-secret';
|
||||
public const NAME = 'config:generate-secret';
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
@@ -27,7 +28,7 @@ class GenerateSecretCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription($this->translator->translate(
|
||||
@@ -35,7 +36,7 @@ class GenerateSecretCommand extends Command
|
||||
));
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$secret = $this->generateRandomString(32);
|
||||
(new SymfonyStyle($input, $output))->success(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
@@ -13,10 +13,10 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class DeleteShortCodeCommand extends Command
|
||||
class DeleteShortUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-code:delete';
|
||||
private const ALIASES = [];
|
||||
public const NAME = 'short-url:delete';
|
||||
private const ALIASES = ['short-code:delete'];
|
||||
|
||||
/**
|
||||
* @var DeleteShortUrlServiceInterface
|
||||
@@ -45,7 +45,7 @@ class DeleteShortCodeCommand extends Command
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code to be deleted')
|
||||
$this->translator->translate('The short code for the short URL to be deleted')
|
||||
)
|
||||
->addOption(
|
||||
'ignore-threshold',
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
|
||||
@@ -14,8 +14,8 @@ use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GeneratePreviewCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-code:process-previews';
|
||||
private const ALIASES = ['shortcode:process-previews'];
|
||||
public const NAME = 'short-url:process-previews';
|
||||
private const ALIASES = ['shortcode:process-previews', 'short-code:process-previews'];
|
||||
|
||||
/**
|
||||
* @var PreviewGeneratorInterface
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
@@ -16,12 +17,12 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateShortcodeCommand extends Command
|
||||
class GenerateShortUrlCommand extends Command
|
||||
{
|
||||
use ShortUrlBuilderTrait;
|
||||
|
||||
public const NAME = 'short-code:generate';
|
||||
private const ALIASES = ['shortcode:generate'];
|
||||
public const NAME = 'short-url:generate';
|
||||
private const ALIASES = ['shortcode:generate', 'short-code:generate'];
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
@@ -53,7 +54,7 @@ class GenerateShortcodeCommand extends Command
|
||||
->setName(self::NAME)
|
||||
->setAliases(self::ALIASES)
|
||||
->setDescription(
|
||||
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
|
||||
$this->translator->translate('Generates a short URL for provided long URL and returns it')
|
||||
)
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
|
||||
->addOption(
|
||||
@@ -143,9 +144,9 @@ class GenerateShortcodeCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
private function getOptionalDate(InputInterface $input, string $fieldName): ?\DateTime
|
||||
private function getOptionalDate(InputInterface $input, string $fieldName): ?Chronos
|
||||
{
|
||||
$since = $input->getOption($fieldName);
|
||||
return $since !== null ? new \DateTime($since) : null;
|
||||
return $since !== null ? Chronos::parse($since) : null;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -15,8 +16,8 @@ use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GetVisitsCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-code:visits';
|
||||
private const ALIASES = ['shortcode:visits'];
|
||||
public const NAME = 'short-url:visits';
|
||||
private const ALIASES = ['shortcode:visits', 'short-code:visits'];
|
||||
|
||||
/**
|
||||
* @var VisitsTrackerInterface
|
||||
@@ -107,10 +108,6 @@ class GetVisitsCommand extends Command
|
||||
private function getDateOption(InputInterface $input, $key)
|
||||
{
|
||||
$value = $input->getOption($key);
|
||||
if (! empty($value)) {
|
||||
$value = new \DateTime($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
return ! empty($value) ? Chronos::parse($value) : $value;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
@@ -14,12 +14,12 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ListShortcodesCommand extends Command
|
||||
class ListShortUrlsCommand extends Command
|
||||
{
|
||||
use PaginatorUtilsTrait;
|
||||
|
||||
public const NAME = 'short-code:list';
|
||||
private const ALIASES = ['shortcode:list'];
|
||||
public const NAME = 'short-url:list';
|
||||
private const ALIASES = ['shortcode:list', 'short-code:list'];
|
||||
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
@@ -132,7 +132,7 @@ class ListShortcodesCommand extends Command
|
||||
|
||||
if ($this->isLastPage($result)) {
|
||||
$continue = false;
|
||||
$io->success($this->translator->translate('Short codes properly listed'));
|
||||
$io->success($this->translator->translate('Short URLs properly listed'));
|
||||
} else {
|
||||
$continue = $io->confirm(
|
||||
\sprintf($this->translator->translate('Continue with page') . ' <options=bold>%s</>?', $page),
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
@@ -15,8 +15,8 @@ use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ResolveUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-code:parse';
|
||||
private const ALIASES = ['shortcode:parse'];
|
||||
public const NAME = 'short-url:parse';
|
||||
private const ALIASES = ['shortcode:parse', 'short-code:parse'];
|
||||
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
@@ -13,7 +13,7 @@ use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class CreateTagCommand extends Command
|
||||
{
|
||||
const NAME = 'tag:create';
|
||||
public const NAME = 'tag:create';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
@@ -31,7 +31,7 @@ class CreateTagCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
@@ -44,7 +44,7 @@ class CreateTagCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$tagNames = $input->getOption('name');
|
||||
|
||||
@@ -13,7 +13,7 @@ use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class DeleteTagsCommand extends Command
|
||||
{
|
||||
const NAME = 'tag:delete';
|
||||
public const NAME = 'tag:delete';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
@@ -31,7 +31,7 @@ class DeleteTagsCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
@@ -44,7 +44,7 @@ class DeleteTagsCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$tagNames = $input->getOption('name');
|
||||
|
||||
@@ -10,10 +10,11 @@ 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 array_map;
|
||||
|
||||
class ListTagsCommand extends Command
|
||||
{
|
||||
const NAME = 'tag:list';
|
||||
public const NAME = 'tag:list';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
@@ -31,14 +32,14 @@ class ListTagsCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription($this->translator->translate('Lists existing tags.'));
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->table([$this->translator->translate('Name')], $this->getTagsRows());
|
||||
@@ -51,7 +52,7 @@ class ListTagsCommand extends Command
|
||||
return [[$this->translator->translate('No tags yet')]];
|
||||
}
|
||||
|
||||
return \array_map(function (Tag $tag) {
|
||||
return array_map(function (Tag $tag) {
|
||||
return [$tag->getName()];
|
||||
}, $tags);
|
||||
}
|
||||
|
||||
@@ -11,10 +11,11 @@ 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
|
||||
{
|
||||
const NAME = 'tag:rename';
|
||||
public const NAME = 'tag:rename';
|
||||
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
@@ -32,7 +33,7 @@ class RenameTagCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
@@ -41,7 +42,7 @@ class RenameTagCommand extends Command
|
||||
->addArgument('newName', InputArgument::REQUIRED, $this->translator->translate('New name of the tag.'));
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$oldName = $input->getArgument('oldName');
|
||||
@@ -51,7 +52,7 @@ class RenameTagCommand extends Command
|
||||
$this->tagService->renameTag($oldName, $newName);
|
||||
$io->success($this->translator->translate('Tag properly renamed.'));
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$io->error(\sprintf($this->translator->translate('A tag with name "%s" was not found'), $oldName));
|
||||
$io->error(sprintf($this->translator->translate('A tag with name "%s" was not found'), $oldName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ 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 sleep;
|
||||
use function sprintf;
|
||||
|
||||
class ProcessVisitsCommand extends Command
|
||||
{
|
||||
@@ -42,7 +44,7 @@ class ProcessVisitsCommand extends Command
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription(
|
||||
@@ -50,7 +52,7 @@ class ProcessVisitsCommand extends Command
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
protected function execute(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$visits = $this->visitService->getUnlocatedVisits();
|
||||
@@ -58,10 +60,10 @@ class ProcessVisitsCommand extends Command
|
||||
$count = 0;
|
||||
foreach ($visits as $visit) {
|
||||
$ipAddr = $visit->getRemoteAddr();
|
||||
$io->write(\sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||
$io->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||
if ($ipAddr === IpAddress::LOCALHOST) {
|
||||
$io->writeln(
|
||||
\sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
||||
sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -75,13 +77,13 @@ class ProcessVisitsCommand extends Command
|
||||
$visit->setVisitLocation($location);
|
||||
$this->visitService->saveVisit($visit);
|
||||
|
||||
$io->writeln(\sprintf(
|
||||
$io->writeln(sprintf(
|
||||
' (' . $this->translator->translate('Address located at "%s"') . ')',
|
||||
$location->getCityName()
|
||||
));
|
||||
} catch (WrongIpException $e) {
|
||||
$io->writeln(
|
||||
\sprintf(' <error>%s</error>', $this->translator->translate('An error occurred while locating IP'))
|
||||
sprintf(' <error>%s</error>', $this->translator->translate('An error occurred while locating IP'))
|
||||
);
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
@@ -91,11 +93,11 @@ class ProcessVisitsCommand extends Command
|
||||
if ($count === $this->ipLocationResolver->getApiLimit()) {
|
||||
$count = 0;
|
||||
$seconds = $this->ipLocationResolver->getApiInterval();
|
||||
$io->note(\sprintf(
|
||||
$io->note(sprintf(
|
||||
$this->translator->translate('IP location resolver limit reached. Waiting %s seconds...'),
|
||||
$seconds
|
||||
));
|
||||
\sleep($seconds);
|
||||
sleep($seconds);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class ApplicationConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$io->title('APPLICATION');
|
||||
|
||||
if ($appConfig->hasApp() && $io->confirm('Do you want to keep imported application config?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$validator = function ($value) {
|
||||
return $value;
|
||||
};
|
||||
$appConfig->setApp([
|
||||
'SECRET' => $io->ask(
|
||||
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
|
||||
null,
|
||||
$validator
|
||||
) ?: $this->generateRandomString(32),
|
||||
'DISABLE_TRACK_PARAM' => $io->ask(
|
||||
'Provide a parameter name that you will be able to use to disable tracking on specific request to '
|
||||
. 'short URLs (leave empty and this feature won\'t be enabled)',
|
||||
null,
|
||||
$validator
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
interface ConfigCustomizerInterface
|
||||
{
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig);
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class DatabaseConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
const DATABASE_DRIVERS = [
|
||||
'MySQL' => 'pdo_mysql',
|
||||
'PostgreSQL' => 'pdo_pgsql',
|
||||
'SQLite' => 'pdo_sqlite',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
private $filesystem;
|
||||
|
||||
public function __construct(Filesystem $filesystem)
|
||||
{
|
||||
$this->filesystem = $filesystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
* @throws IOException
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$io->title('DATABASE');
|
||||
|
||||
if ($appConfig->hasDatabase() && $io->confirm('Do you want to keep imported database config?')) {
|
||||
// If the user selected to keep DB config and is configured to use sqlite, copy DB file
|
||||
if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) {
|
||||
try {
|
||||
$this->filesystem->copy(
|
||||
$appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH,
|
||||
CustomizableAppConfig::SQLITE_DB_PATH
|
||||
);
|
||||
} catch (IOException $e) {
|
||||
$io->error('It wasn\'t possible to import the SQLite database');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Select database type
|
||||
$params = [];
|
||||
$databases = \array_keys(self::DATABASE_DRIVERS);
|
||||
$dbType = $io->choice('Select database type', $databases, $databases[0]);
|
||||
$params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
|
||||
|
||||
// Ask for connection params if database is not SQLite
|
||||
if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
|
||||
$params['NAME'] = $io->ask('Database name', 'shlink');
|
||||
$params['USER'] = $io->ask('Database username');
|
||||
$params['PASSWORD'] = $io->ask('Database password');
|
||||
$params['HOST'] = $io->ask('Database host', 'localhost');
|
||||
$params['PORT'] = $io->ask('Database port', $this->getDefaultDbPort($params['DRIVER']));
|
||||
}
|
||||
|
||||
$appConfig->setDatabase($params);
|
||||
}
|
||||
|
||||
private function getDefaultDbPort(string $driver): string
|
||||
{
|
||||
return $driver === 'pdo_mysql' ? '3306' : '5432';
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class LanguageConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
const SUPPORTED_LANGUAGES = ['en', 'es'];
|
||||
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$io->title('LANGUAGE');
|
||||
|
||||
if ($appConfig->hasLanguage() && $io->confirm('Do you want to keep imported language?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$appConfig->setLanguage([
|
||||
'DEFAULT' => $this->chooseLanguage('Select default language for the application in general', $io),
|
||||
'CLI' => $this->chooseLanguage('Select default language for CLI executions', $io),
|
||||
]);
|
||||
}
|
||||
|
||||
private function chooseLanguage(string $message, SymfonyStyle $io): string
|
||||
{
|
||||
return $io->choice($message, self::SUPPORTED_LANGUAGES, self::SUPPORTED_LANGUAGES[0]);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class UrlShortenerConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
/**
|
||||
* @param SymfonyStyle $io
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$io->title('URL SHORTENER');
|
||||
|
||||
if ($appConfig->hasUrlShortener() && $io->confirm('Do you want to keep imported URL shortener config?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask for URL shortener params
|
||||
$appConfig->setUrlShortener([
|
||||
'SCHEMA' => $io->choice(
|
||||
'Select schema for generated short URLs',
|
||||
['http', 'https'],
|
||||
'http'
|
||||
),
|
||||
'HOSTNAME' => $io->ask('Hostname for generated URLs'),
|
||||
'CHARS' => $io->ask(
|
||||
'Character set for generated short codes (leave empty to autogenerate one)',
|
||||
null,
|
||||
function ($value) {
|
||||
return $value;
|
||||
}
|
||||
) ?: \str_shuffle(UrlShortener::DEFAULT_CHARS),
|
||||
'VALIDATE_URL' => $io->confirm('Do you want to validate long urls by 200 HTTP status code on response'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Model;
|
||||
|
||||
use Zend\Stdlib\ArraySerializableInterface;
|
||||
|
||||
final class CustomizableAppConfig implements ArraySerializableInterface
|
||||
{
|
||||
const SQLITE_DB_PATH = 'data/database.sqlite';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $database;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $language;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $app;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $importedInstallationPath;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDatabase()
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $database
|
||||
* @return $this
|
||||
*/
|
||||
public function setDatabase(array $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDatabase()
|
||||
{
|
||||
return ! empty($this->database);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getUrlShortener()
|
||||
{
|
||||
return $this->urlShortener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $urlShortener
|
||||
* @return $this
|
||||
*/
|
||||
public function setUrlShortener(array $urlShortener)
|
||||
{
|
||||
$this->urlShortener = $urlShortener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasUrlShortener()
|
||||
{
|
||||
return ! empty($this->urlShortener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $language
|
||||
* @return $this
|
||||
*/
|
||||
public function setLanguage(array $language)
|
||||
{
|
||||
$this->language = $language;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasLanguage()
|
||||
{
|
||||
return ! empty($this->language);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getApp()
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $app
|
||||
* @return $this
|
||||
*/
|
||||
public function setApp(array $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasApp()
|
||||
{
|
||||
return ! empty($this->app);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getImportedInstallationPath()
|
||||
{
|
||||
return $this->importedInstallationPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $importedInstallationPath
|
||||
* @return $this|self
|
||||
*/
|
||||
public function setImportedInstallationPath($importedInstallationPath)
|
||||
{
|
||||
$this->importedInstallationPath = $importedInstallationPath;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasImportedInstallationPath()
|
||||
{
|
||||
return $this->importedInstallationPath !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange internal values from provided array
|
||||
*
|
||||
* @param array $array
|
||||
* @return void
|
||||
*/
|
||||
public function exchangeArray(array $array)
|
||||
{
|
||||
if (isset($array['app_options'], $array['app_options']['secret_key'])) {
|
||||
$this->setApp([
|
||||
'SECRET' => $array['app_options']['secret_key'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (isset($array['entity_manager'], $array['entity_manager']['connection'])) {
|
||||
$this->deserializeDatabase($array['entity_manager']['connection']);
|
||||
}
|
||||
|
||||
if (isset($array['translator'], $array['translator']['locale'], $array['cli'], $array['cli']['locale'])) {
|
||||
$this->setLanguage([
|
||||
'DEFAULT' => $array['translator']['locale'],
|
||||
'CLI' => $array['cli']['locale'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (isset($array['url_shortener'])) {
|
||||
$urlShortener = $array['url_shortener'];
|
||||
$this->setUrlShortener([
|
||||
'SCHEMA' => $urlShortener['domain']['schema'],
|
||||
'HOSTNAME' => $urlShortener['domain']['hostname'],
|
||||
'CHARS' => $urlShortener['shortcode_chars'],
|
||||
'VALIDATE_URL' => $urlShortener['validate_url'] ?? true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function deserializeDatabase(array $conn)
|
||||
{
|
||||
if (! isset($conn['driver'])) {
|
||||
return;
|
||||
}
|
||||
$driver = $conn['driver'];
|
||||
|
||||
$params = ['DRIVER' => $driver];
|
||||
if ($driver !== 'pdo_sqlite') {
|
||||
$params['USER'] = $conn['user'];
|
||||
$params['PASSWORD'] = $conn['password'];
|
||||
$params['NAME'] = $conn['dbname'];
|
||||
$params['HOST'] = $conn['host'];
|
||||
$params['PORT'] = $conn['port'];
|
||||
}
|
||||
|
||||
$this->setDatabase($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array representation of the object
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getArrayCopy()
|
||||
{
|
||||
$config = [
|
||||
'app_options' => [
|
||||
'secret_key' => $this->app['SECRET'],
|
||||
'disable_track_param' => $this->app['DISABLE_TRACK_PARAM'] ?? null,
|
||||
],
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => $this->database['DRIVER'],
|
||||
],
|
||||
],
|
||||
'translator' => [
|
||||
'locale' => $this->language['DEFAULT'],
|
||||
],
|
||||
'cli' => [
|
||||
'locale' => $this->language['CLI'],
|
||||
],
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => $this->urlShortener['SCHEMA'],
|
||||
'hostname' => $this->urlShortener['HOSTNAME'],
|
||||
],
|
||||
'shortcode_chars' => $this->urlShortener['CHARS'],
|
||||
'validate_url' => $this->urlShortener['VALIDATE_URL'],
|
||||
],
|
||||
];
|
||||
|
||||
// Build dynamic database config based on selected driver
|
||||
if ($this->database['DRIVER'] === 'pdo_sqlite') {
|
||||
$config['entity_manager']['connection']['path'] = self::SQLITE_DB_PATH;
|
||||
} else {
|
||||
$config['entity_manager']['connection']['user'] = $this->database['USER'];
|
||||
$config['entity_manager']['connection']['password'] = $this->database['PASSWORD'];
|
||||
$config['entity_manager']['connection']['dbname'] = $this->database['NAME'];
|
||||
$config['entity_manager']['connection']['host'] = $this->database['HOST'];
|
||||
$config['entity_manager']['connection']['port'] = $this->database['PORT'];
|
||||
|
||||
if ($this->database['DRIVER'] === 'pdo_mysql') {
|
||||
$config['entity_manager']['connection']['driverOptions'] = [
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
@@ -50,8 +51,8 @@ class GenerateKeyCommandTest extends TestCase
|
||||
*/
|
||||
public function expirationDateIsDefinedIfProvided()
|
||||
{
|
||||
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1)
|
||||
->willReturn(new ApiKey());
|
||||
$this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledTimes(1)
|
||||
->willReturn(new ApiKey());
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
'--expirationDate' => '2016-01-01',
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\DeleteShortCodeCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
@@ -28,7 +28,7 @@ class DeleteShortCodeCommandTest extends TestCase
|
||||
{
|
||||
$this->service = $this->prophesize(DeleteShortUrlServiceInterface::class);
|
||||
|
||||
$command = new DeleteShortCodeCommand($this->service->reveal(), Translator::factory([]));
|
||||
$command = new DeleteShortUrlCommand($this->service->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\GeneratePreviewCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GeneratePreviewCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\GenerateShortcodeCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
@@ -28,7 +28,7 @@ class GenerateShortcodeCommandTest extends TestCase
|
||||
public function setUp()
|
||||
{
|
||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||
$command = new GenerateShortcodeCommand($this->urlShortener->reveal(), Translator::factory([]), [
|
||||
$command = new GenerateShortUrlCommand($this->urlShortener->reveal(), Translator::factory([]), [
|
||||
'schema' => 'http',
|
||||
'hostname' => 'foo.com',
|
||||
]);
|
||||
@@ -1,12 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\GetVisitsCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
@@ -58,7 +59,7 @@ class GetVisitsCommandTest extends TestCase
|
||||
$shortCode = 'abc123';
|
||||
$startDate = '2016-01-01';
|
||||
$endDate = '2016-02-01';
|
||||
$this->visitsTracker->info($shortCode, new DateRange(new \DateTime($startDate), new \DateTime($endDate)))
|
||||
$this->visitsTracker->info($shortCode, new DateRange(Chronos::parse($startDate), Chronos::parse($endDate)))
|
||||
->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\ListShortcodesCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
@@ -30,7 +30,7 @@ class ListShortcodesCommandTest extends TestCase
|
||||
{
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
|
||||
$app = new Application();
|
||||
$command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([]), []);
|
||||
$command = new ListShortUrlsCommand($this->shortUrlService->reveal(), Translator::factory([]), []);
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
@@ -1,91 +0,0 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ApplicationConfigCustomizer;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class ApplicationConfigCustomizerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ApplicationConfigCustomizer
|
||||
*/
|
||||
private $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $io;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->io = $this->prophesize(SymfonyStyle::class);
|
||||
$this->io->title(Argument::any())->willReturn(null);
|
||||
|
||||
$this->plugin = new ApplicationConfigCustomizer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('the_secret');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertTrue($config->hasApp());
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'the_secret',
|
||||
'DISABLE_TRACK_PARAM' => 'the_secret',
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
{
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('the_new_secret');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'the_new_secret',
|
||||
'DISABLE_TRACK_PARAM' => 'the_new_secret',
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'foo',
|
||||
], $config->getApp());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
use const JSON_ERROR_NONE;
|
||||
use function array_key_exists;
|
||||
use function array_shift;
|
||||
use function getenv;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function json_last_error;
|
||||
use function json_last_error_msg;
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
|
||||
@@ -40,3 +47,54 @@ function env($key, $default = null)
|
||||
|
||||
return trim($value);
|
||||
}
|
||||
|
||||
function contains($needle, array $haystack): bool
|
||||
{
|
||||
return in_array($needle, $haystack, true);
|
||||
}
|
||||
|
||||
function json_decode(string $json, int $depth = 512, int $options = 0): array
|
||||
{
|
||||
$data = \json_decode($json, true, $depth, $options);
|
||||
if (JSON_ERROR_NONE !== json_last_error()) {
|
||||
throw new Exception\InvalidArgumentException('Error decoding JSON: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
function array_path_exists(array $path, array $array): bool
|
||||
{
|
||||
// As soon as a step is not found, the path does not exist
|
||||
$step = array_shift($path);
|
||||
if (! array_key_exists($step, $array)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Once the path is empty, we have found all the parts in the path
|
||||
if (empty($path)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If current value is not an array, then we have not found the path
|
||||
$newArray = $array[$step];
|
||||
if (! is_array($newArray)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_path_exists($path, $newArray);
|
||||
}
|
||||
|
||||
function array_get_path(array $path, array $array)
|
||||
{
|
||||
do {
|
||||
$step = array_shift($path);
|
||||
if (! is_array($array) || ! array_key_exists($step, $array)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$array = $array[$step];
|
||||
} while (! empty($path));
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
@@ -3,10 +3,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Exception;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class WrongIpException extends RuntimeException
|
||||
{
|
||||
public static function fromIpAddress($ipAddress, \Throwable $prev = null): self
|
||||
{
|
||||
return new self(\sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
||||
return new self(sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,17 @@ namespace Shlinkio\Shlink\Common\Factory;
|
||||
use Doctrine\Common\Cache;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Shlinkio\Shlink\Common;
|
||||
use Memcached;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
use function Shlinkio\Shlink\Common\contains;
|
||||
use function Shlinkio\Shlink\Common\env;
|
||||
|
||||
class CacheFactory implements FactoryInterface
|
||||
{
|
||||
const VALID_CACHE_ADAPTERS = [
|
||||
private const VALID_CACHE_ADAPTERS = [
|
||||
Cache\ApcuCache::class,
|
||||
Cache\ArrayCache::class,
|
||||
Cache\FilesystemCache::class,
|
||||
@@ -51,14 +53,12 @@ class CacheFactory implements FactoryInterface
|
||||
{
|
||||
// Try to get the adapter from config
|
||||
$config = $container->get('config');
|
||||
if (isset($config['cache'], $config['cache']['adapter'])
|
||||
&& in_array($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS)
|
||||
) {
|
||||
if (isset($config['cache']['adapter']) && contains($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS)) {
|
||||
return $this->resolveCacheAdapter($config['cache']);
|
||||
}
|
||||
|
||||
// If the adapter has not been set in config, create one based on environment
|
||||
return Common\env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||
return env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,8 +75,8 @@ class CacheFactory implements FactoryInterface
|
||||
case Cache\PhpFileCache::class:
|
||||
return new $cacheConfig['adapter']($cacheConfig['options']['dir']);
|
||||
case Cache\MemcachedCache::class:
|
||||
$memcached = new \Memcached();
|
||||
$servers = isset($cacheConfig['options']['servers']) ? $cacheConfig['options']['servers'] : [];
|
||||
$memcached = new Memcached();
|
||||
$servers = $cacheConfig['options']['servers'] ?? [];
|
||||
|
||||
foreach ($servers as $server) {
|
||||
if (! isset($server['host'])) {
|
||||
|
||||
@@ -5,10 +5,13 @@ namespace Shlinkio\Shlink\Common\Factory;
|
||||
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Doctrine\DBAL\DBALException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Tools\Setup;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
@@ -16,15 +19,10 @@ use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
class EntityManagerFactory implements FactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
* @throws ORMException
|
||||
* @throws DBALException
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
@@ -35,6 +33,8 @@ class EntityManagerFactory implements FactoryInterface
|
||||
$connectionConfig = $emConfig['connection'] ?? [];
|
||||
$ormConfig = $emConfig['orm'] ?? [];
|
||||
|
||||
Type::addType(ChronosDateTimeType::CHRONOS_DATETIME, ChronosDateTimeType::class);
|
||||
|
||||
return EntityManager::create($connectionConfig, Setup::createAnnotationMetadataConfiguration(
|
||||
$ormConfig['entities_paths'] ?? [],
|
||||
$isDevMode,
|
||||
|
||||
@@ -5,7 +5,10 @@ namespace Shlinkio\Shlink\Common\Service;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use function Shlinkio\Shlink\Common\json_decode;
|
||||
use function sprintf;
|
||||
|
||||
class IpApiLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
@@ -29,10 +32,12 @@ class IpApiLocationResolver implements IpLocationResolverInterface
|
||||
public function resolveIpLocation(string $ipAddress): array
|
||||
{
|
||||
try {
|
||||
$response = $this->httpClient->get(\sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||
return $this->mapFields(\json_decode((string) $response->getBody(), true));
|
||||
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||
return $this->mapFields(json_decode((string) $response->getBody()));
|
||||
} catch (GuzzleException $e) {
|
||||
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||
} catch (InvalidArgumentException $e) {
|
||||
throw new WrongIpException('IP-API returned invalid body while locating IP address', 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
52
module/Common/src/Type/ChronosDateTimeType.php
Normal file
52
module/Common/src/Type/ChronosDateTimeType.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Type;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Doctrine\DBAL\Types\DateTimeImmutableType;
|
||||
|
||||
class ChronosDateTimeType extends DateTimeImmutableType
|
||||
{
|
||||
public const CHRONOS_DATETIME = 'chronos_datetime';
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return self::CHRONOS_DATETIME;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConversionException
|
||||
*/
|
||||
public function convertToPHPValue($value, AbstractPlatform $platform): ?Chronos
|
||||
{
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$dateTime = parent::convertToPHPValue($value, $platform);
|
||||
return Chronos::instance($dateTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ConversionException
|
||||
*/
|
||||
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string
|
||||
{
|
||||
if (null === $value) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if ($value instanceof \DateTimeInterface) {
|
||||
return $value->format($platform->getDateTimeFormatString());
|
||||
}
|
||||
|
||||
throw ConversionException::conversionFailedInvalidType(
|
||||
$value,
|
||||
$this->getName(),
|
||||
['null', \DateTimeInterface::class]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,42 +3,35 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Common\Util;
|
||||
|
||||
class DateRange
|
||||
use Cake\Chronos\Chronos;
|
||||
|
||||
final class DateRange
|
||||
{
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
* @var Chronos|null
|
||||
*/
|
||||
private $startDate;
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
* @var Chronos|null
|
||||
*/
|
||||
private $endDate;
|
||||
|
||||
public function __construct(\DateTimeInterface $startDate = null, \DateTimeInterface $endDate = null)
|
||||
public function __construct(?Chronos $startDate = null, ?Chronos $endDate = null)
|
||||
{
|
||||
$this->startDate = $startDate;
|
||||
$this->endDate = $endDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getStartDate()
|
||||
public function getStartDate(): ?Chronos
|
||||
{
|
||||
return $this->startDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getEndDate()
|
||||
public function getEndDate(): ?Chronos
|
||||
{
|
||||
return $this->endDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->startDate === null && $this->endDate === null;
|
||||
|
||||
94
module/Common/test/Type/ChronosDateTimeTypeTest.php
Normal file
94
module/Common/test/Type/ChronosDateTimeTypeTest.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Type;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Type\ChronosDateTimeType;
|
||||
|
||||
class ChronosDateTimeTypeTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ChronosDateTimeType
|
||||
*/
|
||||
private $type;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
if (! Type::hasType(ChronosDateTimeType::CHRONOS_DATETIME)) {
|
||||
Type::addType(ChronosDateTimeType::CHRONOS_DATETIME, ChronosDateTimeType::class);
|
||||
}
|
||||
|
||||
$this->type = Type::getType(ChronosDateTimeType::CHRONOS_DATETIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function nameIsReturned()
|
||||
{
|
||||
$this->assertEquals(ChronosDateTimeType::CHRONOS_DATETIME, $this->type->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideValues
|
||||
*/
|
||||
public function valueIsConverted(?string $value, ?string $expected)
|
||||
{
|
||||
$platform = $this->prophesize(AbstractPlatform::class);
|
||||
$platform->getDateTimeFormatString()->willReturn('Y-m-d H:i:s');
|
||||
|
||||
$result = $this->type->convertToPHPValue($value, $platform->reveal());
|
||||
|
||||
if ($expected === null) {
|
||||
$this->assertNull($result);
|
||||
} else {
|
||||
$this->assertInstanceOf($expected, $result);
|
||||
}
|
||||
}
|
||||
|
||||
public function provideValues(): array
|
||||
{
|
||||
return [
|
||||
[null, null],
|
||||
['now', Chronos::class],
|
||||
['2017-01-01', Chronos::class],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider providePhpValues
|
||||
*/
|
||||
public function valueIsConvertedToDatabaseFormat(?\DateTimeInterface $value, ?string $expected)
|
||||
{
|
||||
$platform = $this->prophesize(AbstractPlatform::class);
|
||||
$platform->getDateTimeFormatString()->willReturn('Y-m-d');
|
||||
|
||||
$this->assertEquals($expected, $this->type->convertToDatabaseValue($value, $platform->reveal()));
|
||||
}
|
||||
|
||||
public function providePhpValues(): array
|
||||
{
|
||||
return [
|
||||
[null, null],
|
||||
[new \DateTimeImmutable('2017-01-01'), '2017-01-01'],
|
||||
[Chronos::parse('2017-02-01'), '2017-02-01'],
|
||||
[new \DateTime('2017-03-01'), '2017-03-01'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function exceptionIsThrownIfInvalidValueIsParsedToDatabase()
|
||||
{
|
||||
$this->expectException(ConversionException::class);
|
||||
$this->type->convertToDatabaseValue(new \stdClass(), $this->prophesize(AbstractPlatform::class)->reveal());
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Common\Util;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
|
||||
@@ -24,8 +25,8 @@ class DateRangeTest extends TestCase
|
||||
*/
|
||||
public function providedDatesAreSet()
|
||||
{
|
||||
$startDate = new \DateTime();
|
||||
$endDate = new \DateTime();
|
||||
$startDate = Chronos::now();
|
||||
$endDate = Chronos::now();
|
||||
$range = new DateRange($startDate, $endDate);
|
||||
$this->assertSame($startDate, $range->getStartDate());
|
||||
$this->assertSame($endDate, $range->getEndDate());
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Entity;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
@@ -36,8 +37,8 @@ class ShortUrl extends AbstractEntity
|
||||
*/
|
||||
private $shortCode;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(name="date_created", type="datetime")
|
||||
* @var Chronos
|
||||
* @ORM\Column(name="date_created", type="chronos_datetime")
|
||||
*/
|
||||
private $dateCreated;
|
||||
/**
|
||||
@@ -56,13 +57,13 @@ class ShortUrl extends AbstractEntity
|
||||
*/
|
||||
private $tags;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(name="valid_since", type="datetime", nullable=true)
|
||||
* @var Chronos|null
|
||||
* @ORM\Column(name="valid_since", type="chronos_datetime", nullable=true)
|
||||
*/
|
||||
private $validSince;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(name="valid_until", type="datetime", nullable=true)
|
||||
* @var Chronos|null
|
||||
* @ORM\Column(name="valid_until", type="chronos_datetime", nullable=true)
|
||||
*/
|
||||
private $validUntil;
|
||||
/**
|
||||
@@ -74,7 +75,7 @@ class ShortUrl extends AbstractEntity
|
||||
public function __construct()
|
||||
{
|
||||
$this->shortCode = '';
|
||||
$this->dateCreated = new \DateTime();
|
||||
$this->dateCreated = Chronos::now();
|
||||
$this->visits = new ArrayCollection();
|
||||
$this->tags = new ArrayCollection();
|
||||
}
|
||||
@@ -117,12 +118,12 @@ class ShortUrl extends AbstractEntity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateCreated(): \DateTime
|
||||
public function getDateCreated(): Chronos
|
||||
{
|
||||
return $this->dateCreated;
|
||||
}
|
||||
|
||||
public function setDateCreated(\DateTime $dateCreated): self
|
||||
public function setDateCreated(Chronos $dateCreated): self
|
||||
{
|
||||
$this->dateCreated = $dateCreated;
|
||||
return $this;
|
||||
@@ -151,23 +152,23 @@ class ShortUrl extends AbstractEntity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValidSince(): ?\DateTime
|
||||
public function getValidSince(): ?Chronos
|
||||
{
|
||||
return $this->validSince;
|
||||
}
|
||||
|
||||
public function setValidSince(?\DateTime $validSince): self
|
||||
public function setValidSince(?Chronos $validSince): self
|
||||
{
|
||||
$this->validSince = $validSince;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValidUntil(): ?\DateTime
|
||||
public function getValidUntil(): ?Chronos
|
||||
{
|
||||
return $this->validUntil;
|
||||
}
|
||||
|
||||
public function setValidUntil(?\DateTime $validUntil): self
|
||||
public function setValidUntil(?Chronos $validUntil): self
|
||||
{
|
||||
$this->validUntil = $validUntil;
|
||||
return $this;
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Entity;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
@@ -25,8 +26,8 @@ class Visit extends AbstractEntity implements \JsonSerializable
|
||||
*/
|
||||
private $referer;
|
||||
/**
|
||||
* @var \DateTime
|
||||
* @ORM\Column(type="datetime", nullable=false)
|
||||
* @var Chronos
|
||||
* @ORM\Column(type="chronos_datetime", nullable=false)
|
||||
*/
|
||||
private $date;
|
||||
/**
|
||||
@@ -54,7 +55,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->date = new \DateTime();
|
||||
$this->date = Chronos::now();
|
||||
}
|
||||
|
||||
public function getReferer(): string
|
||||
@@ -68,12 +69,12 @@ class Visit extends AbstractEntity implements \JsonSerializable
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDate(): \DateTime
|
||||
public function getDate(): Chronos
|
||||
{
|
||||
return $this->date;
|
||||
}
|
||||
|
||||
public function setDate(\DateTime $date): self
|
||||
public function setDate(Chronos $date): self
|
||||
{
|
||||
$this->date = $date;
|
||||
return $this;
|
||||
@@ -148,7 +149,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
|
||||
{
|
||||
return [
|
||||
'referer' => $this->referer,
|
||||
'date' => isset($this->date) ? $this->date->format(\DateTime::ATOM) : null,
|
||||
'date' => isset($this->date) ? $this->date->toAtomString() : null,
|
||||
'userAgent' => $this->userAgent,
|
||||
'visitLocation' => $this->visitLocation,
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Shlinkio\Shlink\Core\Model;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
final class CreateShortCodeData
|
||||
final class CreateShortUrlData
|
||||
{
|
||||
/**
|
||||
* @var UriInterface
|
||||
@@ -3,17 +3,19 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Model;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
|
||||
use function is_string;
|
||||
|
||||
final class ShortUrlMeta
|
||||
{
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
* @var Chronos|null
|
||||
*/
|
||||
private $validSince;
|
||||
/**
|
||||
* @var \DateTime|null
|
||||
* @var Chronos|null
|
||||
*/
|
||||
private $validUntil;
|
||||
/**
|
||||
@@ -43,8 +45,8 @@ final class ShortUrlMeta
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|\DateTimeInterface|null $validSince
|
||||
* @param string|\DateTimeInterface|null $validUntil
|
||||
* @param string|Chronos|null $validSince
|
||||
* @param string|Chronos|null $validUntil
|
||||
* @param string|null $customSlug
|
||||
* @param int|null $maxVisits
|
||||
* @return ShortUrlMeta
|
||||
@@ -86,26 +88,23 @@ final class ShortUrlMeta
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|\DateTime|null $date
|
||||
* @return \DateTime|null
|
||||
* @param string|Chronos|null $date
|
||||
* @return Chronos|null
|
||||
*/
|
||||
private function parseDateField($date): ?\DateTime
|
||||
private function parseDateField($date): ?Chronos
|
||||
{
|
||||
if ($date === null || $date instanceof \DateTime) {
|
||||
if ($date === null || $date instanceof Chronos) {
|
||||
return $date;
|
||||
}
|
||||
|
||||
if (\is_string($date)) {
|
||||
return new \DateTime($date);
|
||||
if (is_string($date)) {
|
||||
return Chronos::parse($date);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getValidSince(): ?\DateTime
|
||||
public function getValidSince(): ?Chronos
|
||||
{
|
||||
return $this->validSince;
|
||||
}
|
||||
@@ -115,10 +114,7 @@ final class ShortUrlMeta
|
||||
return $this->validSince !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTime|null
|
||||
*/
|
||||
public function getValidUntil(): ?\DateTime
|
||||
public function getValidUntil(): ?Chronos
|
||||
{
|
||||
return $this->validUntil;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,15 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Repository;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use function array_column;
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
use function key;
|
||||
use function Shlinkio\Shlink\Common\contains;
|
||||
|
||||
class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface
|
||||
{
|
||||
@@ -54,19 +60,19 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
'shortCode' => 'shortCode',
|
||||
'dateCreated' => 'dateCreated',
|
||||
];
|
||||
$fieldName = \is_array($orderBy) ? \key($orderBy) : $orderBy;
|
||||
$order = \is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
|
||||
$fieldName = is_array($orderBy) ? key($orderBy) : $orderBy;
|
||||
$order = is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
|
||||
|
||||
if (\in_array($fieldName, ['visits', 'visitsCount', 'visitCount'], true)) {
|
||||
if (contains($fieldName, ['visits', 'visitsCount', 'visitCount'])) {
|
||||
$qb->addSelect('COUNT(DISTINCT v) AS totalVisits')
|
||||
->leftJoin('s.visits', 'v')
|
||||
->groupBy('s')
|
||||
->orderBy('totalVisits', $order);
|
||||
|
||||
return \array_column($qb->getQuery()->getResult(), 0);
|
||||
return array_column($qb->getQuery()->getResult(), 0);
|
||||
}
|
||||
|
||||
if (\array_key_exists($fieldName, $fieldNameMap)) {
|
||||
if (array_key_exists($fieldName, $fieldNameMap)) {
|
||||
$qb->orderBy('s.' . $fieldNameMap[$fieldName], $order);
|
||||
}
|
||||
return $qb->getQuery()->getResult();
|
||||
@@ -131,7 +137,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
|
||||
*/
|
||||
public function findOneByShortCode(string $shortCode): ?ShortUrl
|
||||
{
|
||||
$now = new \DateTimeImmutable();
|
||||
$now = Chronos::now();
|
||||
|
||||
$qb = $this->createQueryBuilder('s');
|
||||
$qb->where($qb->expr()->eq('s.shortCode', ':shortCode'))
|
||||
|
||||
@@ -9,6 +9,9 @@ use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Expressive\Template\TemplateRendererInterface;
|
||||
use function array_shift;
|
||||
use function explode;
|
||||
use function Shlinkio\Shlink\Common\contains;
|
||||
|
||||
class NotFoundHandler implements RequestHandlerInterface
|
||||
{
|
||||
@@ -39,12 +42,12 @@ class NotFoundHandler implements RequestHandlerInterface
|
||||
*/
|
||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||
{
|
||||
$accepts = \explode(',', $request->getHeaderLine('Accept'));
|
||||
$accept = \array_shift($accepts);
|
||||
$accepts = explode(',', $request->getHeaderLine('Accept'));
|
||||
$accept = array_shift($accepts);
|
||||
$status = StatusCodeInterface::STATUS_NOT_FOUND;
|
||||
|
||||
// If the first accepted type is json, return a json response
|
||||
if (\in_array($accept, ['application/json', 'text/json', 'application/x-json'], true)) {
|
||||
if (contains($accept, ['application/json', 'text/json', 'application/x-json'])) {
|
||||
return new Response\JsonResponse([
|
||||
'error' => 'NOT_FOUND',
|
||||
'message' => 'Not found',
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Cocur\Slugify\Slugify;
|
||||
use Cocur\Slugify\SlugifyInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@@ -61,14 +62,6 @@ class UrlShortener implements UrlShortenerInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and persists a unique shortcode generated for provided url
|
||||
*
|
||||
* @param UriInterface $url
|
||||
* @param string[] $tags
|
||||
* @param \DateTime|null $validSince
|
||||
* @param \DateTime|null $validUntil
|
||||
* @param string|null $customSlug
|
||||
* @param int|null $maxVisits
|
||||
* @throws NonUniqueSlugException
|
||||
* @throws InvalidUrlException
|
||||
* @throws RuntimeException
|
||||
@@ -76,10 +69,10 @@ class UrlShortener implements UrlShortenerInterface
|
||||
public function urlToShortCode(
|
||||
UriInterface $url,
|
||||
array $tags = [],
|
||||
\DateTime $validSince = null,
|
||||
\DateTime $validUntil = null,
|
||||
string $customSlug = null,
|
||||
int $maxVisits = null
|
||||
?Chronos $validSince = null,
|
||||
?Chronos $validUntil = null,
|
||||
?string $customSlug = null,
|
||||
?int $maxVisits = null
|
||||
): ShortUrl {
|
||||
// If the URL validation is enabled, check that the URL actually exists
|
||||
if ($this->urlValidationEnabled) {
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Service;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
@@ -14,14 +15,6 @@ use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||
interface UrlShortenerInterface
|
||||
{
|
||||
/**
|
||||
* Creates and persists a unique shortcode generated for provided url
|
||||
*
|
||||
* @param UriInterface $url
|
||||
* @param string[] $tags
|
||||
* @param \DateTime|null $validSince
|
||||
* @param \DateTime|null $validUntil
|
||||
* @param string|null $customSlug
|
||||
* @param int|null $maxVisits
|
||||
* @throws NonUniqueSlugException
|
||||
* @throws InvalidUrlException
|
||||
* @throws RuntimeException
|
||||
@@ -29,10 +22,10 @@ interface UrlShortenerInterface
|
||||
public function urlToShortCode(
|
||||
UriInterface $url,
|
||||
array $tags = [],
|
||||
\DateTime $validSince = null,
|
||||
\DateTime $validUntil = null,
|
||||
string $customSlug = null,
|
||||
int $maxVisits = null
|
||||
?Chronos $validSince = null,
|
||||
?Chronos $validUntil = null,
|
||||
?string $customSlug = null,
|
||||
?int $maxVisits = null
|
||||
): ShortUrl;
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,6 @@ class ShortUrlDataTransformer implements DataTransformerInterface
|
||||
|
||||
/**
|
||||
* @param ShortUrl $value
|
||||
* @return array
|
||||
*/
|
||||
public function transform($value): array
|
||||
{
|
||||
@@ -36,7 +35,7 @@ class ShortUrlDataTransformer implements DataTransformerInterface
|
||||
'shortCode' => $shortCode,
|
||||
'shortUrl' => $this->buildShortUrl($this->domainConfig, $shortCode),
|
||||
'longUrl' => $longUrl,
|
||||
'dateCreated' => $dateCreated !== null ? $dateCreated->format(\DateTime::ATOM) : null,
|
||||
'dateCreated' => $dateCreated !== null ? $dateCreated->toAtomString() : null,
|
||||
'visitsCount' => $value->getVisitsCount(),
|
||||
'tags' => \array_map([$this, 'serializeTag'], $value->getTags()->toArray()),
|
||||
|
||||
|
||||
@@ -12,12 +12,12 @@ class ShortUrlMetaInputFilter extends InputFilter
|
||||
{
|
||||
use InputFactoryTrait;
|
||||
|
||||
const VALID_SINCE = 'validSince';
|
||||
const VALID_UNTIL = 'validUntil';
|
||||
const CUSTOM_SLUG = 'customSlug';
|
||||
const MAX_VISITS = 'maxVisits';
|
||||
public const VALID_SINCE = 'validSince';
|
||||
public const VALID_UNTIL = 'validUntil';
|
||||
public const CUSTOM_SLUG = 'customSlug';
|
||||
public const MAX_VISITS = 'maxVisits';
|
||||
|
||||
public function __construct(array $data = null)
|
||||
public function __construct(?array $data = null)
|
||||
{
|
||||
$this->initialize();
|
||||
if ($data !== null) {
|
||||
@@ -25,7 +25,7 @@ class ShortUrlMetaInputFilter extends InputFilter
|
||||
}
|
||||
}
|
||||
|
||||
private function initialize()
|
||||
private function initialize(): void
|
||||
{
|
||||
$validSince = $this->createInput(self::VALID_SINCE, false);
|
||||
$validSince->getValidatorChain()->attach(new Date(['format' => \DateTime::ATOM]));
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Repository;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
@@ -41,7 +42,7 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
|
||||
$bar = new ShortUrl();
|
||||
$bar->setOriginalUrl('bar')
|
||||
->setShortCode('bar_very_long_text')
|
||||
->setValidSince((new \DateTime())->add(new \DateInterval('P1M')));
|
||||
->setValidSince(Chronos::now()->addMonth());
|
||||
$this->getEntityManager()->persist($bar);
|
||||
|
||||
$visits = [];
|
||||
|
||||
@@ -3,12 +3,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Repository;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||
use ShlinkioTest\Shlink\Common\DbUnit\DatabaseTestCase;
|
||||
use function sprintf;
|
||||
|
||||
class VisitRepositoryTest extends DatabaseTestCase
|
||||
{
|
||||
@@ -61,7 +63,7 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
for ($i = 0; $i < 6; $i++) {
|
||||
$visit = new Visit();
|
||||
$visit->setShortUrl($shortUrl)
|
||||
->setDate(new \DateTime('2016-01-0' . ($i + 1)));
|
||||
->setDate(Chronos::parse(sprintf('2016-01-0%s', $i + 1)));
|
||||
|
||||
$this->getEntityManager()->persist($visit);
|
||||
}
|
||||
@@ -70,11 +72,11 @@ class VisitRepositoryTest extends DatabaseTestCase
|
||||
$this->assertCount(0, $this->repo->findVisitsByShortUrl('invalid'));
|
||||
$this->assertCount(6, $this->repo->findVisitsByShortUrl($shortUrl->getId()));
|
||||
$this->assertCount(2, $this->repo->findVisitsByShortUrl($shortUrl->getId(), new DateRange(
|
||||
new \DateTime('2016-01-02'),
|
||||
new \DateTime('2016-01-03')
|
||||
Chronos::parse('2016-01-02'),
|
||||
Chronos::parse('2016-01-03')
|
||||
)));
|
||||
$this->assertCount(4, $this->repo->findVisitsByShortUrl($shortUrl->getId(), new DateRange(
|
||||
new \DateTime('2016-01-03')
|
||||
Chronos::parse('2016-01-03')
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Model;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
@@ -42,10 +43,10 @@ class ShortUrlMetaTest extends TestCase
|
||||
*/
|
||||
public function properlyCreatedInstanceReturnsValues()
|
||||
{
|
||||
$meta = ShortUrlMeta::createFromParams((new \DateTime('2015-01-01'))->format(\DateTime::ATOM), null, 'foobar');
|
||||
$meta = ShortUrlMeta::createFromParams(Chronos::parse('2015-01-01')->toAtomString(), null, 'foobar');
|
||||
|
||||
$this->assertTrue($meta->hasValidSince());
|
||||
$this->assertEquals(new \DateTime('2015-01-01'), $meta->getValidSince());
|
||||
$this->assertEquals(Chronos::parse('2015-01-01'), $meta->getValidSince());
|
||||
|
||||
$this->assertFalse($meta->hasValidUntil());
|
||||
$this->assertNull($meta->getValidUntil());
|
||||
|
||||
@@ -3,6 +3,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Service;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -104,15 +105,15 @@ class ShortUrlServiceTest extends TestCase
|
||||
$flush = $this->em->flush($shortUrl)->willReturn(null);
|
||||
|
||||
$result = $this->service->updateMetadataByShortCode('abc123', ShortUrlMeta::createFromParams(
|
||||
(new \DateTime('2017-01-01 00:00:00'))->format(\DateTime::ATOM),
|
||||
(new \DateTime('2017-01-05 00:00:00'))->format(\DateTime::ATOM),
|
||||
Chronos::parse('2017-01-01 00:00:00')->toAtomString(),
|
||||
Chronos::parse('2017-01-05 00:00:00')->toAtomString(),
|
||||
null,
|
||||
5
|
||||
));
|
||||
|
||||
$this->assertSame($shortUrl, $result);
|
||||
$this->assertEquals(new \DateTime('2017-01-01 00:00:00'), $shortUrl->getValidSince());
|
||||
$this->assertEquals(new \DateTime('2017-01-05 00:00:00'), $shortUrl->getValidUntil());
|
||||
$this->assertEquals(Chronos::parse('2017-01-01 00:00:00'), $shortUrl->getValidSince());
|
||||
$this->assertEquals(Chronos::parse('2017-01-05 00:00:00'), $shortUrl->getValidUntil());
|
||||
$this->assertEquals(5, $shortUrl->getMaxVisits());
|
||||
$findShortUrl->shouldHaveBeenCalled();
|
||||
$getRepo->shouldHaveBeenCalled();
|
||||
|
||||
4
module/Installer/config/module.config.php
Normal file
4
module/Installer/config/module.config.php
Normal file
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
return [];
|
||||
@@ -1,13 +1,14 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Install;
|
||||
namespace Shlinkio\Shlink\Installer\Command;
|
||||
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerManagerInterface;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Installer\Config\ConfigCustomizerManagerInterface;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Installer\Util\AskUtilsTrait;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
@@ -23,7 +24,9 @@ use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class InstallCommand extends Command
|
||||
{
|
||||
const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
|
||||
use AskUtilsTrait;
|
||||
|
||||
public const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
|
||||
|
||||
/**
|
||||
* @var SymfonyStyle
|
||||
@@ -102,7 +105,7 @@ class InstallCommand extends Command
|
||||
|
||||
$this->io->writeln([
|
||||
'<info>Welcome to Shlink!!</info>',
|
||||
'This will guide you through the installation process.',
|
||||
'This tool will guide you through the installation process.',
|
||||
]);
|
||||
|
||||
// Check if a cached config file exists and drop it if so
|
||||
@@ -143,7 +146,7 @@ class InstallCommand extends Command
|
||||
$this->io->writeln(['<info>Custom configuration properly generated!</info>', '']);
|
||||
|
||||
// If current command is not update, generate database
|
||||
if (! $this->isUpdate) {
|
||||
if (! $this->isUpdate) {
|
||||
$this->io->write('Initializing database...');
|
||||
if (! $this->runPhpCommand(
|
||||
'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
|
||||
@@ -186,7 +189,10 @@ class InstallCommand extends Command
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
// Ask the user if he/she wants to import an older configuration
|
||||
$importConfig = $this->io->confirm('Do you want to import configuration from previous installation?');
|
||||
$importConfig = $this->io->confirm(
|
||||
'Do you want to import configuration from previous installation? (You will still be asked for any new '
|
||||
. 'config option that did not exist in previous shlink versions)'
|
||||
);
|
||||
if (! $importConfig) {
|
||||
return $config;
|
||||
}
|
||||
@@ -194,7 +200,9 @@ class InstallCommand extends Command
|
||||
// Ask the user for the older shlink path
|
||||
$keepAsking = true;
|
||||
do {
|
||||
$config->setImportedInstallationPath($this->io->ask(
|
||||
$config->setImportedInstallationPath($this->askRequired(
|
||||
$this->io,
|
||||
'previous installation path',
|
||||
'Previous shlink installation path from which to import config'
|
||||
));
|
||||
$configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install;
|
||||
namespace Shlinkio\Shlink\Installer\Config;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerInterface;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\ConfigCustomizerInterface;
|
||||
use Zend\ServiceManager\AbstractPluginManager;
|
||||
|
||||
class ConfigCustomizerManager extends AbstractPluginManager implements ConfigCustomizerManagerInterface
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Install;
|
||||
namespace Shlinkio\Shlink\Installer\Config;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Shlinkio\Shlink\Installer\Exception\InvalidConfigOptionException;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function array_diff;
|
||||
use function array_keys;
|
||||
use function is_numeric;
|
||||
use function sprintf;
|
||||
|
||||
class ApplicationConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
public const SECRET = 'SECRET';
|
||||
public const DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM';
|
||||
public const CHECK_VISITS_THRESHOLD = 'CHECK_VISITS_THRESHOLD';
|
||||
public const VISITS_THRESHOLD = 'VISITS_THRESHOLD';
|
||||
private const EXPECTED_KEYS = [
|
||||
self::SECRET,
|
||||
self::DISABLE_TRACK_PARAM,
|
||||
self::CHECK_VISITS_THRESHOLD,
|
||||
self::VISITS_THRESHOLD,
|
||||
];
|
||||
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void
|
||||
{
|
||||
$app = $appConfig->getApp();
|
||||
$keysToAskFor = $appConfig->hasApp() ? array_diff(self::EXPECTED_KEYS, array_keys($app)) : self::EXPECTED_KEYS;
|
||||
|
||||
if (empty($keysToAskFor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$io->title('APPLICATION');
|
||||
foreach ($keysToAskFor as $key) {
|
||||
// Skip visits threshold when the user decided not to check visits on deletions
|
||||
if ($key === self::VISITS_THRESHOLD && ! $app[self::CHECK_VISITS_THRESHOLD]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$app[$key] = $this->ask($io, $key);
|
||||
}
|
||||
$appConfig->setApp($app);
|
||||
}
|
||||
|
||||
private function ask(SymfonyStyle $io, string $key)
|
||||
{
|
||||
switch ($key) {
|
||||
case self::SECRET:
|
||||
return $io->ask(
|
||||
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one) '
|
||||
. '<fg=red>[DEPRECATED. TO BE REMOVED]</>'
|
||||
) ?: $this->generateRandomString(32);
|
||||
case self::DISABLE_TRACK_PARAM:
|
||||
return $io->ask(
|
||||
'Provide a parameter name that you will be able to use to disable tracking on specific request to '
|
||||
. 'short URLs (leave empty and this feature won\'t be enabled)'
|
||||
);
|
||||
case self::CHECK_VISITS_THRESHOLD:
|
||||
return $io->confirm(
|
||||
'Do you want to enable a safety check which will not allow short URLs to be deleted when they '
|
||||
. 'have more than a specific amount of visits?'
|
||||
);
|
||||
case self::VISITS_THRESHOLD:
|
||||
return $io->ask(
|
||||
'What is the amount of visits from which the system will not allow short URLs to be deleted?',
|
||||
15,
|
||||
[$this, 'validateVisitsThreshold']
|
||||
);
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
public function validateVisitsThreshold($value): int
|
||||
{
|
||||
if (! is_numeric($value) || $value < 1) {
|
||||
throw new InvalidConfigOptionException(
|
||||
sprintf('Provided value "%s" is invalid. Expected a number greater than 1', $value)
|
||||
);
|
||||
}
|
||||
|
||||
return (int) $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
interface ConfigCustomizerInterface
|
||||
{
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void;
|
||||
}
|
||||
135
module/Installer/src/Config/Plugin/DatabaseConfigCustomizer.php
Normal file
135
module/Installer/src/Config/Plugin/DatabaseConfigCustomizer.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Installer\Util\AskUtilsTrait;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use function array_diff;
|
||||
use function array_keys;
|
||||
use function Shlinkio\Shlink\Common\contains;
|
||||
|
||||
class DatabaseConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
use AskUtilsTrait;
|
||||
|
||||
public const DRIVER = 'DRIVER';
|
||||
public const NAME = 'NAME';
|
||||
public const USER = 'USER';
|
||||
public const PASSWORD = 'PASSWORD';
|
||||
public const HOST = 'HOST';
|
||||
public const PORT = 'PORT';
|
||||
private const DRIVER_DEPENDANT_OPTIONS = [
|
||||
self::DRIVER,
|
||||
self::NAME,
|
||||
self::USER,
|
||||
self::PASSWORD,
|
||||
self::HOST,
|
||||
self::PORT,
|
||||
];
|
||||
private const EXPECTED_KEYS = self::DRIVER_DEPENDANT_OPTIONS; // Same now, but could change in the future
|
||||
|
||||
private const DATABASE_DRIVERS = [
|
||||
'MySQL' => 'pdo_mysql',
|
||||
'PostgreSQL' => 'pdo_pgsql',
|
||||
'SQLite' => 'pdo_sqlite',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
private $filesystem;
|
||||
|
||||
public function __construct(Filesystem $filesystem)
|
||||
{
|
||||
$this->filesystem = $filesystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IOException
|
||||
*/
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void
|
||||
{
|
||||
$titlePrinted = false;
|
||||
$db = $appConfig->getDatabase();
|
||||
$doImport = $appConfig->hasDatabase();
|
||||
$keysToAskFor = $doImport ? array_diff(self::EXPECTED_KEYS, array_keys($db)) : self::EXPECTED_KEYS;
|
||||
|
||||
// If the user selected to keep DB, try to import SQLite database
|
||||
if ($doImport) {
|
||||
$this->importSqliteDbFile($io, $appConfig);
|
||||
}
|
||||
|
||||
if (empty($keysToAskFor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the driver is one of the params to ask for, ask for it first
|
||||
if (contains(self::DRIVER, $keysToAskFor)) {
|
||||
$io->title('DATABASE');
|
||||
$titlePrinted = true;
|
||||
$db[self::DRIVER] = $this->ask($io, self::DRIVER);
|
||||
$keysToAskFor = array_diff($keysToAskFor, [self::DRIVER]);
|
||||
}
|
||||
|
||||
// If driver is SQLite, do not ask any driver-dependant option
|
||||
if ($db[self::DRIVER] === self::DATABASE_DRIVERS['SQLite']) {
|
||||
$keysToAskFor = array_diff($keysToAskFor, self::DRIVER_DEPENDANT_OPTIONS);
|
||||
}
|
||||
|
||||
if (! $titlePrinted && ! empty($keysToAskFor)) {
|
||||
$io->title('DATABASE');
|
||||
}
|
||||
foreach ($keysToAskFor as $key) {
|
||||
$db[$key] = $this->ask($io, $key, $db);
|
||||
}
|
||||
$appConfig->setDatabase($db);
|
||||
}
|
||||
|
||||
private function importSqliteDbFile(SymfonyStyle $io, CustomizableAppConfig $appConfig): void
|
||||
{
|
||||
if ($appConfig->getDatabase()[self::DRIVER] !== self::DATABASE_DRIVERS['SQLite']) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->filesystem->copy(
|
||||
$appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH,
|
||||
CustomizableAppConfig::SQLITE_DB_PATH
|
||||
);
|
||||
} catch (IOException $e) {
|
||||
$io->error('It wasn\'t possible to import the SQLite database');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
private function ask(SymfonyStyle $io, string $key, array $params = [])
|
||||
{
|
||||
switch ($key) {
|
||||
case self::DRIVER:
|
||||
$databases = array_keys(self::DATABASE_DRIVERS);
|
||||
$dbType = $io->choice('Select database type', $databases, $databases[0]);
|
||||
return self::DATABASE_DRIVERS[$dbType];
|
||||
case self::NAME:
|
||||
return $io->ask('Database name', 'shlink');
|
||||
case self::USER:
|
||||
return $this->askRequired($io, 'username', 'Database username');
|
||||
case self::PASSWORD:
|
||||
return $this->askRequired($io, 'password', 'Database password');
|
||||
case self::HOST:
|
||||
return $io->ask('Database host', 'localhost');
|
||||
case self::PORT:
|
||||
return $io->ask('Database port', $this->getDefaultDbPort($params[self::DRIVER]));
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function getDefaultDbPort(string $driver): string
|
||||
{
|
||||
return $driver === 'pdo_mysql' ? '3306' : '5432';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function array_diff;
|
||||
use function array_keys;
|
||||
|
||||
class LanguageConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
public const DEFAULT_LANG = 'DEFAULT';
|
||||
public const CLI_LANG = 'CLI';
|
||||
private const EXPECTED_KEYS = [
|
||||
self::DEFAULT_LANG,
|
||||
self::CLI_LANG,
|
||||
];
|
||||
|
||||
private const SUPPORTED_LANGUAGES = ['en', 'es'];
|
||||
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void
|
||||
{
|
||||
$lang = $appConfig->getLanguage();
|
||||
$keysToAskFor = $appConfig->hasLanguage()
|
||||
? array_diff(self::EXPECTED_KEYS, array_keys($lang))
|
||||
: self::EXPECTED_KEYS;
|
||||
|
||||
if (empty($keysToAskFor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$io->title('LANGUAGE');
|
||||
foreach ($keysToAskFor as $key) {
|
||||
$lang[$key] = $this->ask($io, $key);
|
||||
}
|
||||
$appConfig->setLanguage($lang);
|
||||
}
|
||||
|
||||
private function ask(SymfonyStyle $io, string $key)
|
||||
{
|
||||
switch ($key) {
|
||||
case self::DEFAULT_LANG:
|
||||
return $this->chooseLanguage($io, 'Select default language for the application in general');
|
||||
case self::CLI_LANG:
|
||||
return $this->chooseLanguage($io, 'Select default language for CLI executions');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private function chooseLanguage(SymfonyStyle $io, string $message): string
|
||||
{
|
||||
return $io->choice($message, self::SUPPORTED_LANGUAGES, self::SUPPORTED_LANGUAGES[0]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Installer\Util\AskUtilsTrait;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function array_diff;
|
||||
use function array_keys;
|
||||
use function str_shuffle;
|
||||
|
||||
class UrlShortenerConfigCustomizer implements ConfigCustomizerInterface
|
||||
{
|
||||
use AskUtilsTrait;
|
||||
|
||||
public const SCHEMA = 'SCHEMA';
|
||||
public const HOSTNAME = 'HOSTNAME';
|
||||
public const CHARS = 'CHARS';
|
||||
public const VALIDATE_URL = 'VALIDATE_URL';
|
||||
private const EXPECTED_KEYS = [
|
||||
self::SCHEMA,
|
||||
self::HOSTNAME,
|
||||
self::CHARS,
|
||||
self::VALIDATE_URL,
|
||||
];
|
||||
|
||||
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig): void
|
||||
{
|
||||
$urlShortener = $appConfig->getUrlShortener();
|
||||
$doImport = $appConfig->hasUrlShortener();
|
||||
$keysToAskFor = $doImport ? array_diff(self::EXPECTED_KEYS, array_keys($urlShortener)) : self::EXPECTED_KEYS;
|
||||
|
||||
if (empty($keysToAskFor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$io->title('URL SHORTENER');
|
||||
foreach ($keysToAskFor as $key) {
|
||||
$urlShortener[$key] = $this->ask($io, $key);
|
||||
}
|
||||
$appConfig->setUrlShortener($urlShortener);
|
||||
}
|
||||
|
||||
private function ask(SymfonyStyle $io, string $key)
|
||||
{
|
||||
switch ($key) {
|
||||
case self::SCHEMA:
|
||||
return $io->choice(
|
||||
'Select schema for generated short URLs',
|
||||
['http', 'https'],
|
||||
'http'
|
||||
);
|
||||
case self::HOSTNAME:
|
||||
return $this->askRequired($io, 'hostname', 'Hostname for generated URLs');
|
||||
case self::CHARS:
|
||||
return $io->ask(
|
||||
'Character set for generated short codes (leave empty to autogenerate one)'
|
||||
) ?: str_shuffle(UrlShortener::DEFAULT_CHARS);
|
||||
case self::VALIDATE_URL:
|
||||
return $io->confirm('Do you want to validate long urls by 200 HTTP status code on response');
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
15
module/Installer/src/ConfigProvider.php
Normal file
15
module/Installer/src/ConfigProvider.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer;
|
||||
|
||||
use Zend\Config\Factory;
|
||||
use Zend\Stdlib\Glob;
|
||||
|
||||
class ConfigProvider
|
||||
{
|
||||
public function __invoke()
|
||||
{
|
||||
return Factory::fromFiles(Glob::glob(__DIR__ . '/../config/{,*.}config.php', Glob::GLOB_BRACE));
|
||||
}
|
||||
}
|
||||
10
module/Installer/src/Exception/ExceptionInterface.php
Normal file
10
module/Installer/src/Exception/ExceptionInterface.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface ExceptionInterface extends Throwable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class InvalidConfigOptionException extends RuntimeException implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use function sprintf;
|
||||
|
||||
class MissingRequiredOptionException extends RuntimeException implements ExceptionInterface
|
||||
{
|
||||
public static function fromOption(string $optionName): self
|
||||
{
|
||||
return new self(sprintf('The "%s" is required and can\'t be empty', $optionName));
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Factory;
|
||||
namespace Shlinkio\Shlink\Installer\Factory;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerManager;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
use Shlinkio\Shlink\Installer\Command\InstallCommand;
|
||||
use Shlinkio\Shlink\Installer\Config\ConfigCustomizerManager;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
@@ -50,7 +50,7 @@ class InstallApplicationFactory implements FactoryInterface
|
||||
$isUpdate
|
||||
);
|
||||
$app->add($command);
|
||||
$app->setDefaultCommand($command->getName());
|
||||
$app->setDefaultCommand($command->getName(), true);
|
||||
|
||||
return $app;
|
||||
}
|
||||
215
module/Installer/src/Model/CustomizableAppConfig.php
Normal file
215
module/Installer/src/Model/CustomizableAppConfig.php
Normal file
@@ -0,0 +1,215 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Model;
|
||||
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\ApplicationConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\DatabaseConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\LanguageConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\UrlShortenerConfigCustomizer;
|
||||
use Zend\Stdlib\ArraySerializableInterface;
|
||||
use function Shlinkio\Shlink\Common\array_get_path;
|
||||
use function Shlinkio\Shlink\Common\array_path_exists;
|
||||
|
||||
final class CustomizableAppConfig implements ArraySerializableInterface
|
||||
{
|
||||
public const SQLITE_DB_PATH = 'data/database.sqlite';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $database = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $urlShortener = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $language = [];
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $app = [];
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
private $importedInstallationPath;
|
||||
|
||||
public function getDatabase(): array
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
public function setDatabase(array $database): self
|
||||
{
|
||||
$this->database = $database;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasDatabase(): bool
|
||||
{
|
||||
return ! empty($this->database);
|
||||
}
|
||||
|
||||
public function getUrlShortener(): array
|
||||
{
|
||||
return $this->urlShortener;
|
||||
}
|
||||
|
||||
public function setUrlShortener(array $urlShortener): self
|
||||
{
|
||||
$this->urlShortener = $urlShortener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasUrlShortener(): bool
|
||||
{
|
||||
return ! empty($this->urlShortener);
|
||||
}
|
||||
|
||||
public function getLanguage(): array
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
public function setLanguage(array $language): self
|
||||
{
|
||||
$this->language = $language;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasLanguage(): bool
|
||||
{
|
||||
return ! empty($this->language);
|
||||
}
|
||||
|
||||
public function getApp(): array
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
public function setApp(array $app): self
|
||||
{
|
||||
$this->app = $app;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasApp(): bool
|
||||
{
|
||||
return ! empty($this->app);
|
||||
}
|
||||
|
||||
public function getImportedInstallationPath(): ?string
|
||||
{
|
||||
return $this->importedInstallationPath;
|
||||
}
|
||||
|
||||
public function setImportedInstallationPath(string $importedInstallationPath): self
|
||||
{
|
||||
$this->importedInstallationPath = $importedInstallationPath;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasImportedInstallationPath(): bool
|
||||
{
|
||||
return $this->importedInstallationPath !== null;
|
||||
}
|
||||
|
||||
public function exchangeArray(array $array): void
|
||||
{
|
||||
$this->setApp($this->mapExistingPathsToKeys([
|
||||
ApplicationConfigCustomizer::SECRET => ['app_options', 'secret_key'],
|
||||
ApplicationConfigCustomizer::DISABLE_TRACK_PARAM => ['app_options', 'disable_track_param'],
|
||||
ApplicationConfigCustomizer::CHECK_VISITS_THRESHOLD => ['delete_short_urls', 'check_visits_threshold'],
|
||||
ApplicationConfigCustomizer::VISITS_THRESHOLD => ['delete_short_urls', 'visits_threshold'],
|
||||
], $array));
|
||||
|
||||
$this->setDatabase($this->mapExistingPathsToKeys([
|
||||
DatabaseConfigCustomizer::DRIVER => ['entity_manager', 'connection', 'driver'],
|
||||
DatabaseConfigCustomizer::USER => ['entity_manager', 'connection', 'user'],
|
||||
DatabaseConfigCustomizer::PASSWORD => ['entity_manager', 'connection', 'password'],
|
||||
DatabaseConfigCustomizer::NAME => ['entity_manager', 'connection', 'dbname'],
|
||||
DatabaseConfigCustomizer::HOST => ['entity_manager', 'connection', 'host'],
|
||||
DatabaseConfigCustomizer::PORT => ['entity_manager', 'connection', 'port'],
|
||||
], $array));
|
||||
|
||||
$this->setLanguage($this->mapExistingPathsToKeys([
|
||||
LanguageConfigCustomizer::DEFAULT_LANG => ['translator', 'locale'],
|
||||
LanguageConfigCustomizer::CLI_LANG => ['cli', 'locale'],
|
||||
], $array));
|
||||
|
||||
$this->setUrlShortener($this->mapExistingPathsToKeys([
|
||||
UrlShortenerConfigCustomizer::SCHEMA => ['url_shortener', 'domain', 'schema'],
|
||||
UrlShortenerConfigCustomizer::HOSTNAME => ['url_shortener', 'domain', 'hostname'],
|
||||
UrlShortenerConfigCustomizer::CHARS => ['url_shortener', 'shortcode_chars'],
|
||||
UrlShortenerConfigCustomizer::VALIDATE_URL => ['url_shortener', 'validate_url'],
|
||||
], $array));
|
||||
}
|
||||
|
||||
private function mapExistingPathsToKeys(array $map, array $config): array
|
||||
{
|
||||
$result = [];
|
||||
foreach ($map as $key => $path) {
|
||||
if (array_path_exists($path, $config)) {
|
||||
$result[$key] = array_get_path($path, $config);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getArrayCopy(): array
|
||||
{
|
||||
$dbDriver = $this->database[DatabaseConfigCustomizer::DRIVER] ?? '';
|
||||
$config = [
|
||||
'app_options' => [
|
||||
'secret_key' => $this->app[ApplicationConfigCustomizer::SECRET] ?? '',
|
||||
'disable_track_param' => $this->app[ApplicationConfigCustomizer::DISABLE_TRACK_PARAM] ?? null,
|
||||
],
|
||||
'delete_short_urls' => [
|
||||
'check_visits_threshold' => $this->app[ApplicationConfigCustomizer::CHECK_VISITS_THRESHOLD] ?? true,
|
||||
'visits_threshold' => $this->app[ApplicationConfigCustomizer::VISITS_THRESHOLD] ?? 15,
|
||||
],
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => $dbDriver,
|
||||
],
|
||||
],
|
||||
'translator' => [
|
||||
'locale' => $this->language[LanguageConfigCustomizer::DEFAULT_LANG] ?? 'en',
|
||||
],
|
||||
'cli' => [
|
||||
'locale' => $this->language[LanguageConfigCustomizer::CLI_LANG] ?? 'en',
|
||||
],
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => $this->urlShortener[UrlShortenerConfigCustomizer::SCHEMA] ?? 'http',
|
||||
'hostname' => $this->urlShortener[UrlShortenerConfigCustomizer::HOSTNAME] ?? '',
|
||||
],
|
||||
'shortcode_chars' => $this->urlShortener[UrlShortenerConfigCustomizer::CHARS] ?? '',
|
||||
'validate_url' => $this->urlShortener[UrlShortenerConfigCustomizer::VALIDATE_URL] ?? true,
|
||||
],
|
||||
];
|
||||
|
||||
// Build dynamic database config based on selected driver
|
||||
if ($dbDriver === 'pdo_sqlite') {
|
||||
$config['entity_manager']['connection']['path'] = self::SQLITE_DB_PATH;
|
||||
} else {
|
||||
$config['entity_manager']['connection']['user'] = $this->database[DatabaseConfigCustomizer::USER] ?? '';
|
||||
$config['entity_manager']['connection']['password'] =
|
||||
$this->database[DatabaseConfigCustomizer::PASSWORD] ?? '';
|
||||
$config['entity_manager']['connection']['dbname'] = $this->database[DatabaseConfigCustomizer::NAME] ?? '';
|
||||
$config['entity_manager']['connection']['host'] = $this->database[DatabaseConfigCustomizer::HOST] ?? '';
|
||||
$config['entity_manager']['connection']['port'] = $this->database[DatabaseConfigCustomizer::PORT] ?? '';
|
||||
|
||||
if ($dbDriver === 'pdo_mysql') {
|
||||
$config['entity_manager']['connection']['driverOptions'] = [
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
||||
24
module/Installer/src/Util/AskUtilsTrait.php
Normal file
24
module/Installer/src/Util/AskUtilsTrait.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Installer\Util;
|
||||
|
||||
use Shlinkio\Shlink\Installer\Exception\MissingRequiredOptionException;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
trait AskUtilsTrait
|
||||
{
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
private function askRequired(SymfonyStyle $io, string $optionName, string $question)
|
||||
{
|
||||
return $io->ask($question, null, function ($value) use ($optionName) {
|
||||
if (empty($value)) {
|
||||
throw MissingRequiredOptionException::fromOption($optionName);
|
||||
};
|
||||
|
||||
return $value;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,15 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Install;
|
||||
namespace ShlinkioTest\Shlink\Installer\Command;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerManagerInterface;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerInterface;
|
||||
use Shlinkio\Shlink\Installer\Command\InstallCommand;
|
||||
use Shlinkio\Shlink\Installer\Config\ConfigCustomizerManagerInterface;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\ConfigCustomizerInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
@@ -131,14 +131,14 @@ class InstallCommandTest extends TestCase
|
||||
|
||||
/** @var MethodProphecy $importedConfigExists */
|
||||
$importedConfigExists = $this->filesystem->exists(
|
||||
__DIR__ . '/../../../test-resources/' . InstallCommand::GENERATED_CONFIG_PATH
|
||||
__DIR__ . '/../../test-resources/' . InstallCommand::GENERATED_CONFIG_PATH
|
||||
)->willReturn(true);
|
||||
|
||||
$this->commandTester->setInputs([
|
||||
'',
|
||||
'/foo/bar/wrong_previous_shlink',
|
||||
'',
|
||||
__DIR__ . '/../../../test-resources',
|
||||
__DIR__ . '/../../test-resources',
|
||||
]);
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Installer\Config\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\ApplicationConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Exception\InvalidConfigOptionException;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use function array_shift;
|
||||
use function strpos;
|
||||
|
||||
class ApplicationConfigCustomizerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ApplicationConfigCustomizer
|
||||
*/
|
||||
private $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $io;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->io = $this->prophesize(SymfonyStyle::class);
|
||||
$this->io->title(Argument::any())->willReturn(null);
|
||||
|
||||
$this->plugin = new ApplicationConfigCustomizer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('asked');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertTrue($config->hasApp());
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'asked',
|
||||
'DISABLE_TRACK_PARAM' => 'asked',
|
||||
'CHECK_VISITS_THRESHOLD' => false,
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function visitsThresholdIsRequestedIfCheckIsEnabled()
|
||||
{
|
||||
$ask = $this->io->ask(Argument::cetera())->will(function (array $args) {
|
||||
$message = array_shift($args);
|
||||
return strpos($message, 'What is the amount of visits') === 0 ? 20 : 'asked';
|
||||
});
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertTrue($config->hasApp());
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'asked',
|
||||
'DISABLE_TRACK_PARAM' => 'asked',
|
||||
'CHECK_VISITS_THRESHOLD' => true,
|
||||
'VISITS_THRESHOLD' => 20,
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(3);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function onlyMissingOptionsAreAsked()
|
||||
{
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('disable_param');
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
'CHECK_VISITS_THRESHOLD' => true,
|
||||
'VISITS_THRESHOLD' => 20,
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'foo',
|
||||
'DISABLE_TRACK_PARAM' => 'disable_param',
|
||||
'CHECK_VISITS_THRESHOLD' => true,
|
||||
'VISITS_THRESHOLD' => 20,
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function noQuestionsAskedIfImportedConfigContainsEverything()
|
||||
{
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('the_new_secret');
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
'DISABLE_TRACK_PARAM' => 'the_new_secret',
|
||||
'CHECK_VISITS_THRESHOLD' => true,
|
||||
'VISITS_THRESHOLD' => 20,
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'foo',
|
||||
'DISABLE_TRACK_PARAM' => 'the_new_secret',
|
||||
'CHECK_VISITS_THRESHOLD' => true,
|
||||
'VISITS_THRESHOLD' => 20,
|
||||
], $config->getApp());
|
||||
$ask->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideInvalidValues
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function validateVisitsThresholdThrowsExceptionWhenProvidedValueIsInvalid($value)
|
||||
{
|
||||
$this->expectException(InvalidConfigOptionException::class);
|
||||
$this->plugin->validateVisitsThreshold($value);
|
||||
}
|
||||
|
||||
public function provideInvalidValues(): array
|
||||
{
|
||||
return [
|
||||
'string' => ['foo'],
|
||||
'empty string' => [''],
|
||||
'negative number' => [-5],
|
||||
'negative number as string' => ['-5'],
|
||||
'zero' => [0],
|
||||
'zero as string' => ['0'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideValidValues
|
||||
* @param mixed $value
|
||||
*/
|
||||
public function validateVisitsThresholdCastsToIntWhenProvidedValueIsValid($value, int $expected)
|
||||
{
|
||||
$this->assertEquals($expected, $this->plugin->validateVisitsThreshold($value));
|
||||
}
|
||||
|
||||
public function provideValidValues(): array
|
||||
{
|
||||
return [
|
||||
'positive as string' => ['20', 20],
|
||||
'positive as integer' => [5, 5],
|
||||
'one as string' => ['1', 1],
|
||||
'one as integer' => [1, 1],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
namespace ShlinkioTest\Shlink\Installer\Config\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizer;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\DatabaseConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
@@ -62,64 +62,62 @@ class DatabaseConfigCustomizerTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
public function onlyMissingOptionsAreAsked()
|
||||
{
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('MySQL');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('MySQL');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('asked');
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setDatabase([
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
'NAME' => 'foo',
|
||||
'PASSWORD' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_mysql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
'NAME' => 'foo',
|
||||
'USER' => 'asked',
|
||||
'PASSWORD' => 'foo',
|
||||
'HOST' => 'asked',
|
||||
'PORT' => 'asked',
|
||||
], $config->getDatabase());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$ask->shouldHaveBeenCalledTimes(5);
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
$ask->shouldHaveBeenCalledTimes(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
public function noQuestionsAskedIfImportedConfigContainsEverything()
|
||||
{
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('MySQL');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('asked');
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setDatabase([
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
'NAME' => 'foo',
|
||||
'USER' => 'foo',
|
||||
'PASSWORD' => 'foo',
|
||||
'HOST' => 'foo',
|
||||
'PORT' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
'NAME' => 'foo',
|
||||
'USER' => 'foo',
|
||||
'PASSWORD' => 'foo',
|
||||
'HOST' => 'foo',
|
||||
'PORT' => 'foo',
|
||||
], $config->getDatabase());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
$ask->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +125,6 @@ class DatabaseConfigCustomizerTest extends TestCase
|
||||
*/
|
||||
public function sqliteDatabaseIsImportedWhenRequested()
|
||||
{
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
$copy = $this->filesystem->copy(Argument::cetera())->willReturn(null);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
@@ -140,7 +137,6 @@ class DatabaseConfigCustomizerTest extends TestCase
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_sqlite',
|
||||
], $config->getDatabase());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$copy->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
namespace ShlinkioTest\Shlink\Installer\Config\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\LanguageConfigCustomizer;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\LanguageConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class LanguageConfigCustomizerTest extends TestCase
|
||||
@@ -33,7 +33,7 @@ class LanguageConfigCustomizerTest extends TestCase
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
$ask = $this->io->choice(Argument::cetera())->willReturn('en');
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('en');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
@@ -43,38 +43,35 @@ class LanguageConfigCustomizerTest extends TestCase
|
||||
'DEFAULT' => 'en',
|
||||
'CLI' => 'en',
|
||||
], $config->getLanguage());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$choice->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
public function onlyMissingOptionsAreAsked()
|
||||
{
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('es');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setLanguage([
|
||||
'DEFAULT' => 'en',
|
||||
'CLI' => 'en',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DEFAULT' => 'es',
|
||||
'DEFAULT' => 'en',
|
||||
'CLI' => 'es',
|
||||
], $config->getLanguage());
|
||||
$choice->shouldHaveBeenCalledTimes(2);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
public function noQuestionsAskedIfImportedConfigContainsEverything()
|
||||
{
|
||||
$ask = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('en');
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setLanguage([
|
||||
@@ -88,6 +85,6 @@ class LanguageConfigCustomizerTest extends TestCase
|
||||
'DEFAULT' => 'es',
|
||||
'CLI' => 'es',
|
||||
], $config->getLanguage());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
namespace ShlinkioTest\Shlink\Installer\Config\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\UrlShortenerConfigCustomizer;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\UrlShortenerConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
class UrlShortenerConfigCustomizerTest extends TestCase
|
||||
@@ -33,8 +33,8 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('something');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('something');
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('chosen');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('asked');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
@@ -42,9 +42,9 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
||||
|
||||
$this->assertTrue($config->hasUrlShortener());
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'something',
|
||||
'HOSTNAME' => 'something',
|
||||
'CHARS' => 'something',
|
||||
'SCHEMA' => 'chosen',
|
||||
'HOSTNAME' => 'asked',
|
||||
'CHARS' => 'asked',
|
||||
'VALIDATE_URL' => true,
|
||||
], $config->getUrlShortener());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
@@ -55,16 +55,44 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
public function onlyMissingOptionsAreAsked()
|
||||
{
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('foo');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('foo');
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('chosen');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('asked');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setUrlShortener([
|
||||
'SCHEMA' => 'bar',
|
||||
'HOSTNAME' => 'bar',
|
||||
'CHARS' => 'bar',
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'asked',
|
||||
'VALIDATE_URL' => false,
|
||||
], $config->getUrlShortener());
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function noQuestionsAskedIfImportedConfigContainsEverything()
|
||||
{
|
||||
$choice = $this->io->choice(Argument::cetera())->willReturn('chosen');
|
||||
$ask = $this->io->ask(Argument::cetera())->willReturn('asked');
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(false);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setUrlShortener([
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'foo',
|
||||
'VALIDATE_URL' => true,
|
||||
]);
|
||||
|
||||
@@ -74,36 +102,10 @@ class UrlShortenerConfigCustomizerTest extends TestCase
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'foo',
|
||||
'VALIDATE_URL' => false,
|
||||
'VALIDATE_URL' => true,
|
||||
], $config->getUrlShortener());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
$choice->shouldHaveBeenCalledTimes(1);
|
||||
$confirm->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
$confirm = $this->io->confirm(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setUrlShortener([
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'foo',
|
||||
'VALIDATE_URL' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process($this->io->reveal(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'foo',
|
||||
'VALIDATE_URL' => 'foo',
|
||||
], $config->getUrlShortener());
|
||||
$confirm->shouldHaveBeenCalledTimes(1);
|
||||
$choice->shouldNotHaveBeenCalled();
|
||||
$ask->shouldNotHaveBeenCalled();
|
||||
$confirm->shouldNotHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
29
module/Installer/test/ConfigProviderTest.php
Normal file
29
module/Installer/test/ConfigProviderTest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Installer;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Installer\ConfigProvider;
|
||||
|
||||
class ConfigProviderTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ConfigProvider
|
||||
*/
|
||||
protected $configProvider;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->configProvider = new ConfigProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function configIsReturned()
|
||||
{
|
||||
$config = $this->configProvider->__invoke();
|
||||
$this->assertEmpty($config);
|
||||
}
|
||||
}
|
||||
40
module/Installer/test/CustomizableAppConfigTest.php
Normal file
40
module/Installer/test/CustomizableAppConfigTest.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Installer;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\ApplicationConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Config\Plugin\LanguageConfigCustomizer;
|
||||
use Shlinkio\Shlink\Installer\Model\CustomizableAppConfig;
|
||||
|
||||
class CustomizableAppConfigTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function exchangeArrayIgnoresAnyNonProvidedKey()
|
||||
{
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$config->exchangeArray([
|
||||
'app_options' => [
|
||||
'disable_track_param' => null,
|
||||
],
|
||||
'translator' => [
|
||||
'locale' => 'es',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertFalse($config->hasDatabase());
|
||||
$this->assertFalse($config->hasUrlShortener());
|
||||
$this->assertTrue($config->hasApp());
|
||||
$this->assertTrue($config->hasLanguage());
|
||||
$this->assertEquals([
|
||||
ApplicationConfigCustomizer::DISABLE_TRACK_PARAM => null,
|
||||
], $config->getApp());
|
||||
$this->assertEquals([
|
||||
LanguageConfigCustomizer::DEFAULT_LANG => 'es',
|
||||
], $config->getLanguage());
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user