mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 20:23:12 +08:00
Compare commits
206 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6eef694315 | ||
|
|
fe4a4aef34 | ||
|
|
b13c95cf1a | ||
|
|
c1c325588e | ||
|
|
faad60f79e | ||
|
|
cbb5a02b95 | ||
|
|
96faafd31e | ||
|
|
7aa42ada54 | ||
|
|
3b1545a761 | ||
|
|
536309afb6 | ||
|
|
ad6ef22b72 | ||
|
|
4185015bef | ||
|
|
3376725152 | ||
|
|
d7b18776f1 | ||
|
|
1da285a63a | ||
|
|
372488cbb4 | ||
|
|
b6fee0ebaf | ||
|
|
322180bde4 | ||
|
|
1cf6c93007 | ||
|
|
2b89556c09 | ||
|
|
e3021120e3 | ||
|
|
2ca7ab4ccf | ||
|
|
a2c2bc166c | ||
|
|
5f0baab2ac | ||
|
|
ea083e30b6 | ||
|
|
7d49c1760c | ||
|
|
7c42835cc1 | ||
|
|
f77273ef93 | ||
|
|
e52983c5d9 | ||
|
|
4615bbaaf7 | ||
|
|
b432ed2c1d | ||
|
|
34174b2fbe | ||
|
|
fac519699a | ||
|
|
2c91ded514 | ||
|
|
15247e832e | ||
|
|
60c68c914b | ||
|
|
277406c3b8 | ||
|
|
26adf48b48 | ||
|
|
20e43aac90 | ||
|
|
56c9abcfd0 | ||
|
|
5ca4bc928d | ||
|
|
ffa6c0d2ca | ||
|
|
395311eaad | ||
|
|
5bbc7de4af | ||
|
|
d5516c7269 | ||
|
|
f852f9d398 | ||
|
|
0d9f964687 | ||
|
|
00c56ca594 | ||
|
|
9e4fb68265 | ||
|
|
804d99ebf7 | ||
|
|
4bbdccf981 | ||
|
|
1f3e31d100 | ||
|
|
2617ef1547 | ||
|
|
25380e4727 | ||
|
|
12322b7368 | ||
|
|
a608f7d0f4 | ||
|
|
9a42d70604 | ||
|
|
fe708333b1 | ||
|
|
566ee7ef6f | ||
|
|
56af58fcb8 | ||
|
|
cffa43a155 | ||
|
|
2b2c0b7c13 | ||
|
|
faa8019fc5 | ||
|
|
a8ea458649 | ||
|
|
8f4d305982 | ||
|
|
065cddc4b1 | ||
|
|
3a3a16f46f | ||
|
|
ba5bd6d98c | ||
|
|
a1aa9c2031 | ||
|
|
43c6b56e42 | ||
|
|
39d2f5a38f | ||
|
|
69cc30bce7 | ||
|
|
5913550eec | ||
|
|
3140ab2ad7 | ||
|
|
090479fa62 | ||
|
|
9ee2064ba1 | ||
|
|
90cef7d4d9 | ||
|
|
12410e82d8 | ||
|
|
18084433c7 | ||
|
|
8eb279fd28 | ||
|
|
99b7c77997 | ||
|
|
166c94cac7 | ||
|
|
7c5d8cf244 | ||
|
|
73a236b3d0 | ||
|
|
34753ca7d3 | ||
|
|
fff058f44b | ||
|
|
b7f3c332e4 | ||
|
|
cff9b7c0b5 | ||
|
|
63e867cf4b | ||
|
|
f49e9064cd | ||
|
|
3bd4f506e0 | ||
|
|
93713689d7 | ||
|
|
ecd2e6e759 | ||
|
|
a65003803b | ||
|
|
0a4f8c3b0a | ||
|
|
80d8c32881 | ||
|
|
57bc681b9e | ||
|
|
2a089f05b1 | ||
|
|
258f954a38 | ||
|
|
7b0beb3b8c | ||
|
|
9573e9f4ef | ||
|
|
a60080b1ce | ||
|
|
1d92e87d50 | ||
|
|
289db45f27 | ||
|
|
c5382b2a7f | ||
|
|
dd1bc49b79 | ||
|
|
74777c2234 | ||
|
|
99d7e6dd7d | ||
|
|
7b746f76b0 | ||
|
|
2767a14101 | ||
|
|
270dbc6028 | ||
|
|
7b1b00901a | ||
|
|
9ab4b9ab43 | ||
|
|
5e493b435a | ||
|
|
21dc9ac5a0 | ||
|
|
40a3ba6203 | ||
|
|
307dfc64b4 | ||
|
|
9b9b1415fe | ||
|
|
ce2b28a0b4 | ||
|
|
7b98527f2e | ||
|
|
30988b10d1 | ||
|
|
d73d3049b7 | ||
|
|
7b1e855e7f | ||
|
|
a957f66ed0 | ||
|
|
ce00874dd1 | ||
|
|
3d5e5d5df9 | ||
|
|
08f6d2de78 | ||
|
|
c6b7515285 | ||
|
|
04e0a192ad | ||
|
|
878518ced7 | ||
|
|
6f7e4f7e7f | ||
|
|
f701e65f75 | ||
|
|
ef6f4fba66 | ||
|
|
f904f79c18 | ||
|
|
41939b790d | ||
|
|
8c446f0f3b | ||
|
|
9c6420fe26 | ||
|
|
ce4877d4ac | ||
|
|
ebeaa3c64a | ||
|
|
fcdcfde04f | ||
|
|
2ce6c1f44b | ||
|
|
00db8a7ea5 | ||
|
|
4c6cc9cd11 | ||
|
|
50f1549457 | ||
|
|
3923bf0604 | ||
|
|
bb8404040a | ||
|
|
5cdd782ce1 | ||
|
|
e345c2bbfe | ||
|
|
2a018f5415 | ||
|
|
7394424a43 | ||
|
|
2f5119d0b3 | ||
|
|
c569cef239 | ||
|
|
ab6aa99a6d | ||
|
|
f4532c3015 | ||
|
|
af9193f721 | ||
|
|
36259588db | ||
|
|
a3554db1c3 | ||
|
|
75e744838c | ||
|
|
f3d2cf5e15 | ||
|
|
a81dba58bd | ||
|
|
83f29080c6 | ||
|
|
0ef1e416c6 | ||
|
|
57d81115de | ||
|
|
5c8353da02 | ||
|
|
95346ebb7e | ||
|
|
cd5bbcd60a | ||
|
|
fb9c7f8eec | ||
|
|
e42469b090 | ||
|
|
06868f782b | ||
|
|
73a35a8f44 | ||
|
|
6a05265a48 | ||
|
|
8e51b51cae | ||
|
|
545fe7da70 | ||
|
|
cb99130c1e | ||
|
|
b413b16c86 | ||
|
|
3ba51c5390 | ||
|
|
0ef9db0bdf | ||
|
|
bdd2d6f8b2 | ||
|
|
45d194aced | ||
|
|
0a57f52309 | ||
|
|
d97287ab0c | ||
|
|
84c4021b24 | ||
|
|
d39a949992 | ||
|
|
dbe1281d2a | ||
|
|
d3c2f4ed2a | ||
|
|
06fa33877b | ||
|
|
c290bed354 | ||
|
|
31af0eea04 | ||
|
|
aaf4f1dfe5 | ||
|
|
e28e984278 | ||
|
|
839329d627 | ||
|
|
0daa24739d | ||
|
|
7ca52ecff9 | ||
|
|
39598d8608 | ||
|
|
923abdf4d2 | ||
|
|
f917697b8e | ||
|
|
fe8ef2030f | ||
|
|
ab9c2f728a | ||
|
|
ba06ad44bd | ||
|
|
8fc88171ee | ||
|
|
7efb3b3a86 | ||
|
|
170c96b8ef | ||
|
|
55f954f50f | ||
|
|
95d0beea3c | ||
|
|
5eefaf3071 | ||
|
|
5536d05e99 |
@@ -1,14 +1,15 @@
|
||||
# Application
|
||||
APP_ENV=
|
||||
SECRET_KEY=
|
||||
SHORTENED_URL_SCHEMA=
|
||||
SHORTENED_URL_HOSTNAME=
|
||||
SHORTCODE_CHARS=
|
||||
|
||||
# Language
|
||||
DEFAULT_LOCALE=
|
||||
CLI_LOCALE=
|
||||
|
||||
# Database
|
||||
DB_USER=
|
||||
DB_PASSWORD=
|
||||
DB_NAME=
|
||||
|
||||
# Rest authentication
|
||||
REST_USER=
|
||||
REST_PASSWORD=
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
.idea
|
||||
build
|
||||
composer.lock
|
||||
vendor/
|
||||
.env
|
||||
data/database.sqlite
|
||||
|
||||
19
.phpstorm.meta.php
Normal file
19
.phpstorm.meta.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
namespace PHPSTORM_META;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* PhpStorm Container Interop code completion
|
||||
*
|
||||
* Add code completion for container-interop.
|
||||
*
|
||||
* \App\ClassName::class will automatically resolve to it's own name.
|
||||
*
|
||||
* Custom strings like ``"cache"`` or ``"logger"`` need to be added manually.
|
||||
*/
|
||||
$STATIC_METHOD_TYPES = [
|
||||
ContainerInterface::get('') => [
|
||||
'' == '@',
|
||||
],
|
||||
];
|
||||
1
.travis-php.ini
Normal file
1
.travis-php.ini
Normal file
@@ -0,0 +1 @@
|
||||
extension="memcached.so"
|
||||
@@ -8,7 +8,9 @@ branches:
|
||||
php:
|
||||
- 5.6
|
||||
- 7
|
||||
- hhvm
|
||||
- 7.1
|
||||
|
||||
before_install: phpenv config-add .travis-php.ini
|
||||
|
||||
before_script:
|
||||
- composer self-update
|
||||
|
||||
81
CHANGELOG.md
81
CHANGELOG.md
@@ -1,5 +1,86 @@
|
||||
## CHANGELOG
|
||||
|
||||
### 1.2.1
|
||||
|
||||
**Bugs**
|
||||
|
||||
* [62: Fix cross-domain requests in REST API](https://github.com/acelaya/url-shortener/issues/62)
|
||||
|
||||
### 1.2.0
|
||||
|
||||
**Features**
|
||||
|
||||
* [45: Allow to define tags on short codes, to improve filtering and classification](https://github.com/acelaya/url-shortener/issues/45)
|
||||
* [7: Add website previews while listing available URLs](https://github.com/acelaya/url-shortener/issues/7)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
* [57: Add database migrations system to improve updating between versions](https://github.com/acelaya/url-shortener/issues/57)
|
||||
* [31: Add support for other database management systems by improving the EntityManager factory](https://github.com/acelaya/url-shortener/issues/31)
|
||||
* [51: Generate build process to paquetize the app and ease distribution](https://github.com/acelaya/url-shortener/issues/51)
|
||||
* [38: Define installation script. It will request dynamic data on the fly so that there is no need to define env vars](https://github.com/acelaya/url-shortener/issues/38)
|
||||
|
||||
**Tasks**
|
||||
|
||||
* [55: Create update script which does not try to create a new database](https://github.com/acelaya/url-shortener/issues/55)
|
||||
* [54: Add cache namespace to prevent name collisions with other apps in the same environment](https://github.com/acelaya/url-shortener/issues/54)
|
||||
* [29: Use the acelaya/ze-content-based-error-handler package instead of custom error handler implementation](https://github.com/acelaya/url-shortener/issues/29)
|
||||
|
||||
**Bugs**
|
||||
|
||||
* [53: Fix entities database interoperability](https://github.com/acelaya/url-shortener/issues/53)
|
||||
* [52: Add missing htaccess file for apache environments](https://github.com/acelaya/url-shortener/issues/52)
|
||||
|
||||
### 1.1.0
|
||||
|
||||
**Features**
|
||||
|
||||
* [46: Define a route that returns a QR code representing the shortened URL](https://github.com/acelaya/url-shortener/issues/46)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
* [32: Add support for other cache adapters by improving the Cache factory](https://github.com/acelaya/url-shortener/issues/32)
|
||||
* [14: https://github.com/shlinkio/shlink/issues/14](https://github.com/acelaya/url-shortener/issues/14)
|
||||
* [41: Cache the "short code" => "URL" map to prevent extra DB hits](https://github.com/acelaya/url-shortener/issues/41)
|
||||
* [13: Improve REST authentication](https://github.com/acelaya/url-shortener/issues/13)
|
||||
|
||||
**Tasks**
|
||||
|
||||
* [39: Change copyright from "Alejandro Celaya" to "Shlink" in error pages](https://github.com/acelaya/url-shortener/issues/39)
|
||||
* [42: Make REST endpoints that need to find something return a 404 when "something" is not found](https://github.com/acelaya/url-shortener/issues/42)
|
||||
* [35: Make CLI commands to use the same PHP namespace as the one used for the command name](https://github.com/acelaya/url-shortener/issues/35)
|
||||
|
||||
**Bugs**
|
||||
|
||||
* [40: Take into account the X-Forwarded-For header in order to get the visitor information, in case the server is behind a load balancer or proxy](https://github.com/acelaya/url-shortener/issues/40)
|
||||
|
||||
### 1.0.0
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
* [33: Create a command to generate a short code charset by randomizing the default one](https://github.com/acelaya/url-shortener/issues/33)
|
||||
* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/acelaya/url-shortener/issues/15)
|
||||
* [23: Translate application literals](https://github.com/acelaya/url-shortener/issues/23)
|
||||
* [21: Allow to filter visits by date range](https://github.com/acelaya/url-shortener/issues/21)
|
||||
* [22: Save visits locations data on a visit_locations table](https://github.com/acelaya/url-shortener/issues/22)
|
||||
* [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/acelaya/url-shortener/issues/20)
|
||||
* [11: Separate code into multiple modules](https://github.com/acelaya/url-shortener/issues/11)
|
||||
* [18: Group routable middleware in an Action namespace](https://github.com/acelaya/url-shortener/issues/18)
|
||||
|
||||
**Tasks**
|
||||
|
||||
* [36: Remove hhvm from the CI matrix since it doesn't support array constants and will fail](https://github.com/acelaya/url-shortener/issues/36)
|
||||
* [4: Installation steps](https://github.com/acelaya/url-shortener/issues/4)
|
||||
* [6: Remove dependency on expressive helpers package](https://github.com/acelaya/url-shortener/issues/6)
|
||||
* [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/acelaya/url-shortener/issues/30)
|
||||
* [12: Improve code coverage](https://github.com/acelaya/url-shortener/issues/12)
|
||||
* [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/acelaya/url-shortener/issues/25)
|
||||
* [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/acelaya/url-shortener/issues/19)
|
||||
|
||||
**Bugs**
|
||||
|
||||
* [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/acelaya/url-shortener/issues/24)
|
||||
|
||||
### 0.2.0
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
@@ -1,2 +1,9 @@
|
||||
# url-shortener
|
||||
# Shlink
|
||||
|
||||
[](https://travis-ci.org/shlinkio/shlink)
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
|
||||
[](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
|
||||
[](https://packagist.org/packages/shlinkio/shlink)
|
||||
[](https://packagist.org/packages/shlinkio/shlink)
|
||||
|
||||
A PHP-based URL shortener application with analytics and management
|
||||
|
||||
1
bin/cli
1
bin/cli
@@ -6,5 +6,6 @@ use Symfony\Component\Console\Application as CliApp;
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/../config/container.php';
|
||||
|
||||
/** @var CliApp $app */
|
||||
$app = $container->get(CliApp::class);
|
||||
$app->run();
|
||||
|
||||
14
bin/install
Executable file
14
bin/install
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Zend\Config\Writer\PhpArray;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new Application();
|
||||
$app->add(new InstallCommand(new PhpArray()));
|
||||
$app->setDefaultCommand('shlink:install');
|
||||
$app->run();
|
||||
14
bin/update
Executable file
14
bin/update
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
use Shlinkio\Shlink\CLI\Command\Install\UpdateCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Zend\Config\Writer\PhpArray;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new Application();
|
||||
$app->add(new UpdateCommand(new PhpArray()));
|
||||
$app->setDefaultCommand('shlink:install');
|
||||
$app->run();
|
||||
BIN
bin/wkhtmltoimage
Executable file
BIN
bin/wkhtmltoimage
Executable file
Binary file not shown.
44
build.sh
Executable file
44
build.sh
Executable file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage:" >&2
|
||||
echo " $0 {version}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
version=$1
|
||||
builtcontent=$(readlink -f '../shlink_build_tmp')
|
||||
projectdir=$(pwd)
|
||||
|
||||
# Copy project content to temp dir
|
||||
echo 'Copying project files...'
|
||||
rm -rf "${builtcontent}"
|
||||
mkdir "${builtcontent}"
|
||||
cp -R "${projectdir}"/* "${builtcontent}"
|
||||
cd "${builtcontent}"
|
||||
|
||||
# Install dependencies
|
||||
rm -r vendor
|
||||
rm composer.lock
|
||||
composer self-update
|
||||
composer install --no-dev --optimize-autoloader
|
||||
|
||||
# Delete development files
|
||||
echo 'Deleting dev files...'
|
||||
rm build.sh
|
||||
rm CHANGELOG.md
|
||||
rm composer.*
|
||||
rm LICENSE
|
||||
rm php*
|
||||
rm README.md
|
||||
rm -r build
|
||||
rm -f data/database.sqlite
|
||||
rm -rf data/{cache,log,proxies}/{*,.gitignore}
|
||||
rm -rf config/params/{*,.gitignore}
|
||||
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}
|
||||
|
||||
# Compressing file
|
||||
rm -f "${projectdir}"/build/shlink_${version}_dist.zip
|
||||
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip .
|
||||
rm -rf "${builtcontent}"
|
||||
@@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "acelaya/url-shortener",
|
||||
"name": "shlinkio/shlink",
|
||||
"type": "project",
|
||||
"homepage": "https://github.com/acelaya/url-shortener",
|
||||
"homepage": "http://shlink.io",
|
||||
"description": "A self-hosted and PHP-based URL shortener application with CLI and REST interfaces",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Alejandro Celaya ALastrué",
|
||||
"name": "Alejandro Celaya Alastrué",
|
||||
"homepage": "http://www.alejandrocelaya.com",
|
||||
"email": "alejandro@alejandrocelaya.com"
|
||||
}
|
||||
@@ -13,16 +14,27 @@
|
||||
"require": {
|
||||
"php": "^5.6 || ^7.0",
|
||||
"zendframework/zend-expressive": "^1.0",
|
||||
"zendframework/zend-expressive-helpers": "^2.0",
|
||||
"zendframework/zend-expressive-fastroute": "^1.1",
|
||||
"zendframework/zend-expressive-twigrenderer": "^1.0",
|
||||
"zendframework/zend-stdlib": "^2.7",
|
||||
"zendframework/zend-servicemanager": "^3.0",
|
||||
"zendframework/zend-paginator": "^2.6",
|
||||
"zendframework/zend-config": "^2.6",
|
||||
"zendframework/zend-i18n": "^2.7",
|
||||
"mtymek/expressive-config-manager": "^0.4",
|
||||
"acelaya/zsm-annotated-services": "^0.2.0",
|
||||
"acelaya/ze-content-based-error-handler": "^1.0",
|
||||
"doctrine/orm": "^2.5",
|
||||
"guzzlehttp/guzzle": "^6.2",
|
||||
"acelaya/zsm-annotated-services": "^0.2.0",
|
||||
"symfony/console": "^3.0"
|
||||
"symfony/console": "^3.0",
|
||||
"symfony/process": "^3.0",
|
||||
"symfony/filesystem": "^3.0",
|
||||
"firebase/php-jwt": "^4.0",
|
||||
"monolog/monolog": "^1.21",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"endroid/qrcode": "^1.7",
|
||||
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||
"doctrine/migrations": "^1.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5.0",
|
||||
@@ -34,12 +46,21 @@
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Acelaya\\UrlShortener\\": "src"
|
||||
}
|
||||
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
|
||||
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
|
||||
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
|
||||
"Shlinkio\\Shlink\\Common\\": "module/Common/src"
|
||||
},
|
||||
"files": [
|
||||
"module/Common/functions/functions.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"AcelayaTest\\UrlShortener\\": "tests"
|
||||
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
|
||||
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
|
||||
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
|
||||
"ShlinkioTest\\Shlink\\Common\\": "module/Common/test"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
10
config/autoload/app_options.global.php
Normal file
10
config/autoload/app_options.global.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
'app_options' => [
|
||||
'name' => 'Shlink',
|
||||
'version' => '1.2.0',
|
||||
'secret_key' => env('SECRET_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
use Acelaya\UrlShortener\CLI\Command;
|
||||
|
||||
return [
|
||||
|
||||
'cli' => [
|
||||
'commands' => [
|
||||
Command\GenerateShortcodeCommand::class,
|
||||
Command\ResolveUrlCommand::class,
|
||||
Command\ListShortcodesCommand::class,
|
||||
Command\GetVisitsCommand::class,
|
||||
]
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
'database' => [
|
||||
'driver' => 'pdo_mysql',
|
||||
'user' => getenv('DB_USER'),
|
||||
'password' => getenv('DB_PASSWORD'),
|
||||
'dbname' => getenv('DB_NAME') ?: 'acelaya_url_shortener',
|
||||
'charset' => 'utf8',
|
||||
'driverOptions' => [
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
22
config/autoload/dependencies.global.php
Normal file
22
config/autoload/dependencies.global.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
use Zend\Expressive;
|
||||
use Zend\Expressive\Container;
|
||||
use Zend\Expressive\Router;
|
||||
use Zend\Expressive\Template;
|
||||
use Zend\Expressive\Twig;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Expressive\Application::class => Container\ApplicationFactory::class,
|
||||
Router\FastRouteRouter::class => InvokableFactory::class,
|
||||
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
Router\RouterInterface::class => Router\FastRouteRouter::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
20
config/autoload/entity-manager.global.php
Normal file
20
config/autoload/entity-manager.global.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
'entity_manager' => [
|
||||
'orm' => [
|
||||
'proxies_dir' => 'data/proxies',
|
||||
],
|
||||
'connection' => [
|
||||
'driver' => 'pdo_mysql',
|
||||
'user' => env('DB_USER'),
|
||||
'password' => env('DB_PASSWORD'),
|
||||
'dbname' => env('DB_NAME', 'shlink'),
|
||||
'charset' => 'utf8',
|
||||
'driverOptions' => [
|
||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,14 +1,13 @@
|
||||
<?php
|
||||
use Acelaya\ExpressiveErrorHandler\ErrorHandler\ContentBasedErrorHandler;
|
||||
use Zend\Expressive\Container\WhoopsErrorHandlerFactory;
|
||||
|
||||
return [
|
||||
'services' => [
|
||||
'dependencies' => [
|
||||
'invokables' => [
|
||||
'Zend\Expressive\Whoops' => Whoops\Run::class,
|
||||
'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class,
|
||||
],
|
||||
'factories' => [
|
||||
'Zend\Expressive\FinalHandler' => Zend\Expressive\Container\WhoopsErrorHandlerFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
'whoops' => [
|
||||
@@ -18,4 +17,12 @@ return [
|
||||
'ajax_only' => true,
|
||||
],
|
||||
],
|
||||
|
||||
'error_handler' => [
|
||||
'plugins' => [
|
||||
'factories' => [
|
||||
ContentBasedErrorHandler::DEFAULT_CONTENT => WhoopsErrorHandlerFactory::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'debug' => true,
|
||||
|
||||
'debug' => true,
|
||||
'config_cache_enabled' => false,
|
||||
|
||||
];
|
||||
|
||||
32
config/autoload/logger.global.php
Normal file
32
config/autoload/logger.global.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
use Monolog\Handler\RotatingFileHandler;
|
||||
use Monolog\Logger;
|
||||
|
||||
return [
|
||||
|
||||
'logger' => [
|
||||
'formatters' => [
|
||||
'dashed' => [
|
||||
'format' => '[%datetime%] %channel%.%level_name% - %message% %context%' . PHP_EOL,
|
||||
'include_stacktraces' => true,
|
||||
],
|
||||
],
|
||||
|
||||
'handlers' => [
|
||||
'rotating_file_handler' => [
|
||||
'class' => RotatingFileHandler::class,
|
||||
'level' => Logger::INFO,
|
||||
'filename' => 'data/log/shlink_log.log',
|
||||
'max_files' => 30,
|
||||
'formatter' => 'dashed',
|
||||
],
|
||||
],
|
||||
|
||||
'loggers' => [
|
||||
'Shlink' => [
|
||||
'handlers' => ['rotating_file_handler'],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
14
config/autoload/logger.local.php.dist
Normal file
14
config/autoload/logger.local.php.dist
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
use Monolog\Logger;
|
||||
|
||||
return [
|
||||
|
||||
'logger' => [
|
||||
'handlers' => [
|
||||
'rotating_file_handler' => [
|
||||
'level' => Logger::DEBUG,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,18 +1,9 @@
|
||||
<?php
|
||||
use Acelaya\UrlShortener\Middleware;
|
||||
use Zend\Expressive\Container\ApplicationFactory;
|
||||
use Zend\Expressive\Helper;
|
||||
|
||||
return [
|
||||
|
||||
'middleware_pipeline' => [
|
||||
'always' => [
|
||||
'middleware' => [
|
||||
Helper\ServerUrlMiddleware::class,
|
||||
],
|
||||
'priority' => 10000,
|
||||
],
|
||||
|
||||
'routing' => [
|
||||
'middleware' => [
|
||||
ApplicationFactory::ROUTING_MIDDLEWARE,
|
||||
@@ -20,27 +11,11 @@ return [
|
||||
'priority' => 10,
|
||||
],
|
||||
|
||||
'rest' => [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
Middleware\CheckAuthenticationMiddleware::class,
|
||||
Middleware\CrossDomainMiddleware::class,
|
||||
],
|
||||
'priority' => 5,
|
||||
],
|
||||
|
||||
'post-routing' => [
|
||||
'middleware' => [
|
||||
Helper\UrlHelperMiddleware::class,
|
||||
ApplicationFactory::DISPATCH_MIDDLEWARE,
|
||||
],
|
||||
'priority' => 1,
|
||||
],
|
||||
|
||||
'error' => [
|
||||
'middleware' => [],
|
||||
'error' => true,
|
||||
'priority' => -10000,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
11
config/autoload/phpwkhtmltopdf.global.php
Normal file
11
config/autoload/phpwkhtmltopdf.global.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
'phpwkhtmltopdf' => [
|
||||
'images' => [
|
||||
'binary' => 'bin/wkhtmltoimage',
|
||||
'type' => 'jpg',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
8
config/autoload/preview-generation.global.php
Normal file
8
config/autoload/preview-generation.global.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
'preview_generation' => [
|
||||
'files_location' => 'data/cache',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,9 +0,0 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
'rest' => [
|
||||
'username' => getenv('REST_USER'),
|
||||
'password' => getenv('REST_PASSWORD'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,70 +0,0 @@
|
||||
<?php
|
||||
use Acelaya\UrlShortener\CLI;
|
||||
use Acelaya\UrlShortener\Factory\CacheFactory;
|
||||
use Acelaya\UrlShortener\Factory\EntityManagerFactory;
|
||||
use Acelaya\UrlShortener\Middleware;
|
||||
use Acelaya\UrlShortener\Service;
|
||||
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Symfony\Component\Console;
|
||||
use Zend\Expressive;
|
||||
use Zend\Expressive\Container;
|
||||
use Zend\Expressive\Helper;
|
||||
use Zend\Expressive\Router;
|
||||
use Zend\Expressive\Template;
|
||||
use Zend\Expressive\Twig;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
'services' => [
|
||||
'factories' => [
|
||||
Expressive\Application::class => Container\ApplicationFactory::class,
|
||||
Console\Application::class => CLI\Factory\ApplicationFactory::class,
|
||||
|
||||
// Url helpers
|
||||
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
|
||||
Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class,
|
||||
Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
|
||||
Helper\ServerUrlHelper::class => InvokableFactory::class,
|
||||
Router\FastRouteRouter::class => InvokableFactory::class,
|
||||
|
||||
// View
|
||||
'Zend\Expressive\FinalHandler' => Container\TemplatedErrorHandlerFactory::class,
|
||||
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
|
||||
|
||||
// Services
|
||||
EntityManager::class => EntityManagerFactory::class,
|
||||
GuzzleHttp\Client::class => InvokableFactory::class,
|
||||
Service\UrlShortener::class => AnnotatedFactory::class,
|
||||
Service\VisitsTracker::class => AnnotatedFactory::class,
|
||||
Service\ShortUrlService::class => AnnotatedFactory::class,
|
||||
Service\RestTokenService::class => AnnotatedFactory::class,
|
||||
Cache::class => CacheFactory::class,
|
||||
|
||||
// Cli commands
|
||||
CLI\Command\GenerateShortcodeCommand::class => AnnotatedFactory::class,
|
||||
CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class,
|
||||
CLI\Command\ListShortcodesCommand::class => AnnotatedFactory::class,
|
||||
CLI\Command\GetVisitsCommand::class => AnnotatedFactory::class,
|
||||
|
||||
// Middleware
|
||||
Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Rest\AuthenticateMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Rest\CreateShortcodeMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\Rest\ListShortcodesMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
|
||||
Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'em' => EntityManager::class,
|
||||
'httpClient' => GuzzleHttp\Client::class,
|
||||
Router\RouterInterface::class => Router\FastRouteRouter::class,
|
||||
AnnotatedFactory::CACHE_SERVICE => Cache::class,
|
||||
]
|
||||
],
|
||||
|
||||
];
|
||||
@@ -2,12 +2,6 @@
|
||||
|
||||
return [
|
||||
|
||||
'templates' => [
|
||||
'paths' => [
|
||||
'templates'
|
||||
],
|
||||
],
|
||||
|
||||
'twig' => [
|
||||
'cache_dir' => 'data/cache/twig',
|
||||
'extensions' => [
|
||||
|
||||
8
config/autoload/translator.global.php
Normal file
8
config/autoload/translator.global.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
'translator' => [
|
||||
'locale' => env('DEFAULT_LOCALE', 'en'),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,12 +1,14 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => getenv('SHORTENED_URL_SCHEMA') ?: 'http',
|
||||
'hostname' => getenv('SHORTENED_URL_HOSTNAME'),
|
||||
'schema' => env('SHORTENED_URL_SCHEMA', 'http'),
|
||||
'hostname' => env('SHORTENED_URL_HOSTNAME'),
|
||||
],
|
||||
'shortcode_chars' => getenv('SHORTCODE_CHARS'),
|
||||
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'debug' => false,
|
||||
'config_cache_enabled' => true,
|
||||
|
||||
'config_cache_enabled' => false,
|
||||
|
||||
'zend-expressive' => [
|
||||
'error_handler' => [
|
||||
'template_404' => 'error/404.html.twig',
|
||||
'template_error' => 'error/error.html.twig',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -4,7 +4,7 @@ use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Interop\Container\ContainerInterface;
|
||||
|
||||
/** @var ContainerInterface $container */
|
||||
$container = include __DIR__ . '/config/container.php';
|
||||
$container = include __DIR__ . '/container.php';
|
||||
/** @var EntityManager $em */
|
||||
$em = $container->get(EntityManager::class);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
<?php
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
use Zend\Stdlib\Glob;
|
||||
use Acelaya\ExpressiveErrorHandler;
|
||||
use Shlinkio\Shlink\CLI;
|
||||
use Shlinkio\Shlink\Common;
|
||||
use Shlinkio\Shlink\Core;
|
||||
use Shlinkio\Shlink\Rest;
|
||||
use Zend\Expressive\ConfigManager;
|
||||
|
||||
/**
|
||||
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
|
||||
@@ -11,22 +15,11 @@ use Zend\Stdlib\Glob;
|
||||
* Obviously, if you use closures in your config you can't cache it.
|
||||
*/
|
||||
|
||||
$cachedConfigFile = 'data/cache/app_config.php';
|
||||
|
||||
$config = [];
|
||||
if (is_file($cachedConfigFile)) {
|
||||
// Try to load the cached config
|
||||
$config = include $cachedConfigFile;
|
||||
} else {
|
||||
// Load configuration from autoload path
|
||||
foreach (Glob::glob('config/autoload/{{,*.}global,{,*.}local}.php', Glob::GLOB_BRACE) as $file) {
|
||||
$config = ArrayUtils::merge($config, include $file);
|
||||
}
|
||||
|
||||
// Cache config if enabled
|
||||
if (isset($config['config_cache_enabled']) && $config['config_cache_enabled'] === true) {
|
||||
file_put_contents($cachedConfigFile, '<?php return ' . var_export($config, true) . ';');
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
return (new ConfigManager\ConfigManager([
|
||||
ExpressiveErrorHandler\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
CLI\ConfigProvider::class,
|
||||
Rest\ConfigProvider::class,
|
||||
new ConfigManager\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
|
||||
], 'data/cache/app_config.php'))->getMergedConfig();
|
||||
|
||||
@@ -16,6 +16,6 @@ if (class_exists(Dotenv::class)) {
|
||||
|
||||
// Build container
|
||||
$config = require __DIR__ . '/config.php';
|
||||
$container = new ServiceManager($config['services']);
|
||||
$container = new ServiceManager($config['dependencies']);
|
||||
$container->setService('config', $config);
|
||||
return $container;
|
||||
|
||||
2
config/params/.gitignore
vendored
Normal file
2
config/params/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
@@ -1,278 +0,0 @@
|
||||
|
||||
# REST API documentation
|
||||
|
||||
## Error management
|
||||
|
||||
Statuses:
|
||||
|
||||
* 400 -> controlled error
|
||||
* 401 -> authentication error
|
||||
* 500 -> unexpected error
|
||||
|
||||
[TODO]
|
||||
|
||||
## Authentication
|
||||
|
||||
[TODO]
|
||||
|
||||
## Endpoints
|
||||
|
||||
#### Authenticate
|
||||
|
||||
**REQUEST**
|
||||
|
||||
* `POST` -> `/rest/authenticate`
|
||||
* Params:
|
||||
* username: `string`
|
||||
* password: `string`
|
||||
|
||||
**SUCCESS RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "9f741eb0-33d7-4c56-b8f7-3719e9929946"
|
||||
}
|
||||
```
|
||||
|
||||
**ERROR RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "INVALID_ARGUMENT",
|
||||
"message": "You have to provide both \"username\" and \"password\""
|
||||
}
|
||||
```
|
||||
|
||||
Posible errors:
|
||||
|
||||
* **INVALID_ARGUMENT**: Username or password were not provided.
|
||||
* **INVALID_CREDENTIALS**: Username or password are incorrect.
|
||||
|
||||
|
||||
#### Create shortcode
|
||||
|
||||
**REQUEST**
|
||||
|
||||
* `POST` -> `/rest/short-codes`
|
||||
* Params:
|
||||
* longUrl: `string` -> The URL to shorten
|
||||
* Headers:
|
||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
||||
|
||||
**SUCCESS RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"longUrl": "https://www.facebook.com/something/something",
|
||||
"shortUrl": "https://doma.in/rY9Kr",
|
||||
"shortCode": "rY9Kr"
|
||||
}
|
||||
```
|
||||
|
||||
**ERROR RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "INVALID_URL",
|
||||
"message": "Provided URL \"wfwef\" is invalid. Try with a different one."
|
||||
}
|
||||
```
|
||||
|
||||
Posible errors:
|
||||
|
||||
* **INVALID_ARGUMENT**: The longUrl was not provided.
|
||||
* **INVALID_URL**: Provided longUrl has an invalid format or does not resolve.
|
||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
||||
|
||||
|
||||
#### Resolve URL
|
||||
|
||||
**REQUEST**
|
||||
|
||||
* `GET` -> `/rest/short-codes/{shortCode}`
|
||||
* Route params:
|
||||
* shortCode: `string` -> The short code we want to resolve
|
||||
* Headers:
|
||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
||||
|
||||
**SUCCESS RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"longUrl": "https://www.facebook.com/something/something"
|
||||
}
|
||||
```
|
||||
|
||||
**ERROR RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "INVALID_SHORTCODE",
|
||||
"message": "Provided short code \"abc123\" has an invalid format"
|
||||
}
|
||||
```
|
||||
|
||||
Posible errors:
|
||||
|
||||
* **INVALID_ARGUMENT**: No longUrl was found for provided shortCode.
|
||||
* **INVALID_SHORTCODE**: Provided shortCode does not match the character set used by the app to generate short codes.
|
||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
||||
|
||||
|
||||
#### List shortened URLs
|
||||
|
||||
**REQUEST**
|
||||
|
||||
* `GET` -> `/rest/short-codes`
|
||||
* Query params:
|
||||
* page: `integer` -> The page to list. Defaults to 1 if not provided.
|
||||
* Headers:
|
||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
||||
|
||||
**SUCCESS RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"shortUrls": {
|
||||
"data": [
|
||||
{
|
||||
"shortCode": "abc123",
|
||||
"originalUrl": "http://www.alejandrocelaya.com",
|
||||
"dateCreated": "2016-04-30T18:01:47+0200",
|
||||
"visitsCount": 4
|
||||
},
|
||||
{
|
||||
"shortCode": "def456",
|
||||
"originalUrl": "http://www.alejandrocelaya.com/en",
|
||||
"dateCreated": "2016-04-30T18:03:43+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "ghi789",
|
||||
"originalUrl": "http://www.alejandrocelaya.com/es",
|
||||
"dateCreated": "2016-04-30T18:10:38+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "jkl987",
|
||||
"originalUrl": "http://www.alejandrocelaya.com/es/",
|
||||
"dateCreated": "2016-04-30T18:10:57+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "mno654",
|
||||
"originalUrl": "http://blog.alejandrocelaya.com/2016/04/09/improving-zend-service-manager-workflow-with-annotations/",
|
||||
"dateCreated": "2016-04-30T19:21:05+0200",
|
||||
"visitsCount": 1
|
||||
},
|
||||
{
|
||||
"shortCode": "pqr321",
|
||||
"originalUrl": "http://www.google.com",
|
||||
"dateCreated": "2016-05-01T11:19:53+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "stv159",
|
||||
"originalUrl": "http://www.acelaya.com",
|
||||
"dateCreated": "2016-06-12T17:49:21+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "wxy753",
|
||||
"originalUrl": "http://www.atomic-reader.com",
|
||||
"dateCreated": "2016-06-12T17:50:27+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "zab852",
|
||||
"originalUrl": "http://foo.com",
|
||||
"dateCreated": "2016-07-03T09:07:36+0200",
|
||||
"visitsCount": 0
|
||||
},
|
||||
{
|
||||
"shortCode": "cde963",
|
||||
"originalUrl": "https://www.facebook.com.com",
|
||||
"dateCreated": "2016-07-03T09:12:35+0200",
|
||||
"visitsCount": 0
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"currentPage": 4,
|
||||
"pagesCount": 15
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ERROR RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "UNKNOWN_ERROR",
|
||||
"message": "Unexpected error occured"
|
||||
}
|
||||
```
|
||||
|
||||
Posible errors:
|
||||
|
||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
||||
|
||||
|
||||
#### Get visits
|
||||
|
||||
**REQUEST**
|
||||
|
||||
* `GET` -> `/rest/visits/{shortCode}`
|
||||
* Route params:
|
||||
* shortCode: `string` -> The shortCode from which we eant to get the visits.
|
||||
* Headers:
|
||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
||||
|
||||
**SUCCESS RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"shortUrls": {
|
||||
"data": [
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2016-06-18T09:32:22+0200",
|
||||
"remoteAddr": "127.0.0.1",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
||||
},
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2016-04-30T19:20:06+0200",
|
||||
"remoteAddr": "127.0.0.1",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
||||
},
|
||||
{
|
||||
"referer": "google.com",
|
||||
"date": "2016-04-30T19:19:57+0200",
|
||||
"remoteAddr": "1.2.3.4",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
||||
},
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2016-04-30T19:17:35+0200",
|
||||
"remoteAddr": "127.0.0.1",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**ERROR RESPONSE**
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "INVALID_ARGUMENT",
|
||||
"message": "Provided short code \"abc123\" is invalid"
|
||||
}
|
||||
```
|
||||
|
||||
Posible errors:
|
||||
|
||||
* **INVALID_ARGUMENT**: The shortcode does not belong to any short URL
|
||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
||||
2
data/log/.gitignore
vendored
Normal file
2
data/log/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
||||
39
data/migrations/Version20160819142757.php
Normal file
39
data/migrations/Version20160819142757.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20160819142757 extends AbstractMigration
|
||||
{
|
||||
const MYSQL = 'mysql';
|
||||
const SQLITE = 'sqlite';
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
{
|
||||
$db = $this->connection->getDatabasePlatform()->getName();
|
||||
$table = $schema->getTable('short_urls');
|
||||
$column = $table->getColumn('short_code');
|
||||
|
||||
if ($db === self::MYSQL) {
|
||||
$column->setPlatformOption('collation', 'utf8_bin');
|
||||
} elseif ($db === self::SQLITE) {
|
||||
$column->setPlatformOption('collate', 'BINARY');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
{
|
||||
$db = $this->connection->getDatabasePlatform()->getName();
|
||||
}
|
||||
}
|
||||
80
data/migrations/Version20160820191203.php
Normal file
80
data/migrations/Version20160820191203.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace ShlinkMigrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
class Version20160820191203 extends AbstractMigration
|
||||
{
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function up(Schema $schema)
|
||||
{
|
||||
// Check if the tables already exist
|
||||
$tables = $schema->getTables();
|
||||
foreach ($tables as $table) {
|
||||
if ($table->getName() === 'tags') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$this->createTagsTable($schema);
|
||||
$this->createShortUrlsInTagsTable($schema);
|
||||
}
|
||||
|
||||
protected function createTagsTable(Schema $schema)
|
||||
{
|
||||
$table = $schema->createTable('tags');
|
||||
$table->addColumn('id', Type::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addColumn('name', Type::STRING, [
|
||||
'length' => 255,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addUniqueIndex(['name']);
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
protected function createShortUrlsInTagsTable(Schema $schema)
|
||||
{
|
||||
$table = $schema->createTable('short_urls_in_tags');
|
||||
$table->addColumn('short_url_id', Type::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addColumn('tag_id', Type::BIGINT, [
|
||||
'unsigned' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
|
||||
$table->addForeignKeyConstraint('tags', ['tag_id'], ['id'], [
|
||||
'onDelete' => 'CASCADE',
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]);
|
||||
$table->addForeignKeyConstraint('short_urls', ['short_url_id'], ['id'], [
|
||||
'onDelete' => 'CASCADE',
|
||||
'onUpdate' => 'RESTRICT',
|
||||
]);
|
||||
|
||||
$table->setPrimaryKey(['short_url_id', 'tag_id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
*/
|
||||
public function down(Schema $schema)
|
||||
{
|
||||
$schema->dropTable('short_urls_in_tags');
|
||||
$schema->dropTable('tags');
|
||||
}
|
||||
}
|
||||
4
migrations.yml
Normal file
4
migrations.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: ShlinkMigrations
|
||||
migrations_namespace: ShlinkMigrations
|
||||
table_name: migrations
|
||||
migrations_directory: data/migrations
|
||||
23
module/CLI/config/cli.config.php
Normal file
23
module/CLI/config/cli.config.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\CLI\Command;
|
||||
|
||||
return [
|
||||
|
||||
'cli' => [
|
||||
'locale' => env('CLI_LOCALE', 'en'),
|
||||
'commands' => [
|
||||
Command\Shortcode\GenerateShortcodeCommand::class,
|
||||
Command\Shortcode\ResolveUrlCommand::class,
|
||||
Command\Shortcode\ListShortcodesCommand::class,
|
||||
Command\Shortcode\GetVisitsCommand::class,
|
||||
Command\Shortcode\GeneratePreviewCommand::class,
|
||||
Command\Visit\ProcessVisitsCommand::class,
|
||||
Command\Config\GenerateCharsetCommand::class,
|
||||
Command\Config\GenerateSecretCommand::class,
|
||||
Command\Api\GenerateKeyCommand::class,
|
||||
Command\Api\DisableKeyCommand::class,
|
||||
Command\Api\ListKeysCommand::class,
|
||||
]
|
||||
],
|
||||
|
||||
];
|
||||
27
module/CLI/config/dependencies.config.php
Normal file
27
module/CLI/config/dependencies.config.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
||||
use Shlinkio\Shlink\CLI\Command;
|
||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||
use Symfony\Component\Console\Application;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
Application::class => ApplicationFactory::class,
|
||||
|
||||
Command\Shortcode\GenerateShortcodeCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\ResolveUrlCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\ListShortcodesCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\GetVisitsCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\GeneratePreviewCommand::class => AnnotatedFactory::class,
|
||||
Command\Visit\ProcessVisitsCommand::class => AnnotatedFactory::class,
|
||||
Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class,
|
||||
Command\Config\GenerateSecretCommand::class => AnnotatedFactory::class,
|
||||
Command\Api\GenerateKeyCommand::class => AnnotatedFactory::class,
|
||||
Command\Api\DisableKeyCommand::class => AnnotatedFactory::class,
|
||||
Command\Api\ListKeysCommand::class => AnnotatedFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
14
module/CLI/config/translator.config.php
Normal file
14
module/CLI/config/translator.config.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
return [
|
||||
|
||||
'translator' => [
|
||||
'translation_file_patterns' => [
|
||||
[
|
||||
'type' => 'gettext',
|
||||
'base_dir' => __DIR__ . '/../lang',
|
||||
'pattern' => '%s.mo',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
BIN
module/CLI/lang/es.mo
Normal file
BIN
module/CLI/lang/es.mo
Normal file
Binary file not shown.
224
module/CLI/lang/es.po
Normal file
224
module/CLI/lang/es.po
Normal file
@@ -0,0 +1,224 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2016-08-21 18:16+0200\n"
|
||||
"PO-Revision-Date: 2016-08-21 18:16+0200\n"
|
||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es_ES\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Generator: Poedit 1.8.7.1\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
"X-Poedit-KeywordsList: translate;translatePlural\n"
|
||||
"X-Poedit-SearchPath-0: src\n"
|
||||
"X-Poedit-SearchPath-1: config\n"
|
||||
|
||||
msgid "Disables an API key."
|
||||
msgstr "Desahbilita una clave de API."
|
||||
|
||||
msgid "The API key to disable"
|
||||
msgstr "La clave de API a deshabilitar"
|
||||
|
||||
#, php-format
|
||||
msgid "API key %s properly disabled"
|
||||
msgstr "Clave de API %s deshabilitada correctamente"
|
||||
|
||||
#, php-format
|
||||
msgid "API key \"%s\" does not exist."
|
||||
msgstr "La clave de API \"%s\" no existe."
|
||||
|
||||
msgid "Generates a new valid API key."
|
||||
msgstr "Genera una nueva clave de API válida."
|
||||
|
||||
msgid "The date in which the API key should expire. Use any valid PHP format."
|
||||
msgstr ""
|
||||
"La fecha en la que la clave de API debe expirar. Utiliza cualquier valor "
|
||||
"válido en PHP."
|
||||
|
||||
msgid "Generated API key"
|
||||
msgstr "Generada clave de API"
|
||||
|
||||
msgid "Lists all the available API keys."
|
||||
msgstr "Lista todas las claves de API disponibles."
|
||||
|
||||
msgid "Tells if only enabled API keys should be returned."
|
||||
msgstr "Define si sólo las claves de API habilitadas deben ser devueltas."
|
||||
|
||||
msgid "Key"
|
||||
msgstr "Clave"
|
||||
|
||||
msgid "Expiration date"
|
||||
msgstr "Fecha de caducidad"
|
||||
|
||||
msgid "Is enabled"
|
||||
msgstr "Está habilitada"
|
||||
|
||||
#, php-format
|
||||
msgid ""
|
||||
"Generates a character set sample just by shuffling the default one, \"%s\". "
|
||||
"Then it can be set in the SHORTCODE_CHARS environment variable"
|
||||
msgstr ""
|
||||
"Genera un grupo de caracteres simplemente mexclando el grupo por defecto \"%s"
|
||||
"\". Después puede ser utilizado en la variable de entrono SHORTCODE_CHARS"
|
||||
|
||||
msgid "Character set:"
|
||||
msgstr "Grupo de caracteres:"
|
||||
|
||||
msgid ""
|
||||
"Generates a random secret string that can be used for JWT token encryption"
|
||||
msgstr ""
|
||||
"Genera una cadena de caracteres aleatoria que puede ser usada para cifrar "
|
||||
"tokens JWT"
|
||||
|
||||
msgid "Secret key:"
|
||||
msgstr "Clave secreta:"
|
||||
|
||||
msgid ""
|
||||
"Processes and generates the previews for every URL, improving performance "
|
||||
"for later web requests."
|
||||
msgstr ""
|
||||
"Procesa y genera las vistas previas para cada URL, mejorando el rendimiento "
|
||||
"para peticiones web posteriores."
|
||||
|
||||
msgid "Finished processing all URLs"
|
||||
msgstr "Finalizado el procesado de todas las URLs"
|
||||
|
||||
#, php-format
|
||||
msgid "Processing URL %s..."
|
||||
msgstr "Procesando URL %s..."
|
||||
|
||||
msgid " <info>Success!</info>"
|
||||
msgstr "<info>¡Correcto!</info>"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
msgid "Generates a short 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 "The long URL to parse"
|
||||
msgstr "La URL larga a procesar"
|
||||
|
||||
msgid "Tags to apply to the new short URL"
|
||||
msgstr "Etiquetas a aplicar a la nueva URL acortada"
|
||||
|
||||
msgid "A long URL was not provided. Which URL do you want to shorten?:"
|
||||
msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?"
|
||||
|
||||
msgid "A URL was not provided!"
|
||||
msgstr "¡No se ha proporcionado una URL!"
|
||||
|
||||
msgid "Processed URL:"
|
||||
msgstr "URL procesada:"
|
||||
|
||||
msgid "Generated URL:"
|
||||
msgstr "URL generada:"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided URL \"%s\" is invalid. Try with a different one."
|
||||
msgstr "La URL proporcionada \"%s\" e inválida. Prueba con una diferente."
|
||||
|
||||
msgid "Returns the detailed visits information for provided short code"
|
||||
msgstr ""
|
||||
"Devuelve la información detallada de visitas para el código corto "
|
||||
"proporcionado"
|
||||
|
||||
msgid "The short code which visits we want to get"
|
||||
msgstr "El código corto del cual queremos obtener las visitas"
|
||||
|
||||
msgid "Allows to filter visits, returning only those older than start date"
|
||||
msgstr ""
|
||||
"Permite filtrar las visitas, devolviendo sólo aquellas más antiguas que "
|
||||
"startDate"
|
||||
|
||||
msgid "Allows to filter visits, returning only those newer than end date"
|
||||
msgstr ""
|
||||
"Permite filtrar las visitas, devolviendo sólo aquellas más nuevas que endDate"
|
||||
|
||||
msgid "A short code was not provided. Which short code do you want to use?:"
|
||||
msgstr "No se prporcionó un código corto. ¿Qué código corto deseas usar?"
|
||||
|
||||
msgid "Referer"
|
||||
msgstr "Origen"
|
||||
|
||||
msgid "Date"
|
||||
msgstr "Fecha"
|
||||
|
||||
msgid "Remote Address"
|
||||
msgstr "Dirección remota"
|
||||
|
||||
msgid "User agent"
|
||||
msgstr "Agente de usuario"
|
||||
|
||||
msgid "List all short URLs"
|
||||
msgstr "Listar todas las URLs cortas"
|
||||
|
||||
#, php-format
|
||||
msgid "The first page to list (%s items per page)"
|
||||
msgstr "La primera página a listar (%s elementos por página)"
|
||||
|
||||
msgid "Whether to display the tags or not"
|
||||
msgstr "Si se desea mostrar las etiquetas o no"
|
||||
|
||||
msgid "Short code"
|
||||
msgstr "Código corto"
|
||||
|
||||
msgid "Original URL"
|
||||
msgstr "URL original"
|
||||
|
||||
msgid "Date created"
|
||||
msgstr "Fecha de creación"
|
||||
|
||||
msgid "Visits count"
|
||||
msgstr "Número de visitas"
|
||||
|
||||
msgid "Tags"
|
||||
msgstr "Etiquetas"
|
||||
|
||||
msgid "You have reached last page"
|
||||
msgstr "Has alcanzado la última página"
|
||||
|
||||
msgid "Continue with page"
|
||||
msgstr "Continuar con la página"
|
||||
|
||||
msgid "Returns the long URL behind a short code"
|
||||
msgstr "Devuelve la URL larga detrás de un código corto"
|
||||
|
||||
msgid "The short code to parse"
|
||||
msgstr "El código corto a convertir"
|
||||
|
||||
msgid "A short code was not provided. Which short code do you want to parse?:"
|
||||
msgstr ""
|
||||
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?"
|
||||
|
||||
#, php-format
|
||||
msgid "No URL found for short code \"%s\""
|
||||
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
|
||||
|
||||
msgid "Long URL:"
|
||||
msgstr "URL larga:"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided short code \"%s\" has an invalid format."
|
||||
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
|
||||
|
||||
msgid "Processes visits where location is not set yet"
|
||||
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
|
||||
|
||||
msgid "Processing IP"
|
||||
msgstr "Procesando IP"
|
||||
|
||||
msgid "Ignored localhost address"
|
||||
msgstr "Ignorada IP de localhost"
|
||||
|
||||
#, php-format
|
||||
msgid "Address located at \"%s\""
|
||||
msgstr "Dirección localizada en \"%s\""
|
||||
|
||||
msgid "Finished processing all IPs"
|
||||
msgstr "Finalizado el procesado de todas las IPs"
|
||||
62
module/CLI/src/Command/Api/DisableKeyCommand.php
Normal file
62
module/CLI/src/Command/Api/DisableKeyCommand.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class DisableKeyCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
private $apiKeyService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* DisableKeyCommand constructor.
|
||||
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({ApiKeyService::class, "translator"})
|
||||
*/
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('api-key:disable')
|
||||
->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)
|
||||
{
|
||||
$apiKey = $input->getArgument('apiKey');
|
||||
|
||||
try {
|
||||
$this->apiKeyService->disable($apiKey);
|
||||
$output->writeln(sprintf(
|
||||
$this->translator->translate('API key %s properly disabled'),
|
||||
'<info>' . $apiKey . '</info>'
|
||||
));
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>' . $this->translator->translate('API key "%s" does not exist.') . '</error>',
|
||||
$apiKey
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
56
module/CLI/src/Command/Api/GenerateKeyCommand.php
Normal file
56
module/CLI/src/Command/Api/GenerateKeyCommand.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateKeyCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
private $apiKeyService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* GenerateKeyCommand constructor.
|
||||
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({ApiKeyService::class, "translator"})
|
||||
*/
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('api-key:generate')
|
||||
->setDescription($this->translator->translate('Generates a new valid API key.'))
|
||||
->addOption(
|
||||
'expirationDate',
|
||||
'e',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('The date in which the API key should expire. Use any valid PHP format.')
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$expirationDate = $input->getOption('expirationDate');
|
||||
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? new \DateTime($expirationDate) : null);
|
||||
$output->writeln($this->translator->translate('Generated API key') . sprintf(': <info>%s</info>', $apiKey));
|
||||
}
|
||||
}
|
||||
108
module/CLI/src/Command/Api/ListKeysCommand.php
Normal file
108
module/CLI/src/Command/Api/ListKeysCommand.php
Normal file
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ListKeysCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var ApiKeyServiceInterface
|
||||
*/
|
||||
private $apiKeyService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* ListKeysCommand constructor.
|
||||
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({ApiKeyService::class, "translator"})
|
||||
*/
|
||||
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('api-key:list')
|
||||
->setDescription($this->translator->translate('Lists all the available API keys.'))
|
||||
->addOption(
|
||||
'enabledOnly',
|
||||
null,
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate('Tells if only enabled API keys should be returned.')
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$enabledOnly = $input->getOption('enabledOnly');
|
||||
$list = $this->apiKeyService->listKeys($enabledOnly);
|
||||
|
||||
$table = new Table($output);
|
||||
if ($enabledOnly) {
|
||||
$table->setHeaders([
|
||||
$this->translator->translate('Key'),
|
||||
$this->translator->translate('Expiration date'),
|
||||
]);
|
||||
} else {
|
||||
$table->setHeaders([
|
||||
$this->translator->translate('Key'),
|
||||
$this->translator->translate('Is enabled'),
|
||||
$this->translator->translate('Expiration date'),
|
||||
]);
|
||||
}
|
||||
|
||||
/** @var ApiKey $row */
|
||||
foreach ($list as $row) {
|
||||
$key = $row->getKey();
|
||||
$expiration = $row->getExpirationDate();
|
||||
$rowData = [];
|
||||
|
||||
if ($enabledOnly) {
|
||||
$rowData[] = $key;
|
||||
} else {
|
||||
$rowData[] = $row->isEnabled() ? $this->getSuccessString($key) : $this->getErrorString($key);
|
||||
$rowData[] = $row->isEnabled() ? $this->getSuccessString('+++') : $this->getErrorString('---');
|
||||
}
|
||||
|
||||
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ISO8601) : '-';
|
||||
$table->addRow($rowData);
|
||||
}
|
||||
|
||||
$table->render();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
protected function getErrorString($string)
|
||||
{
|
||||
return sprintf('<fg=red>%s</>', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
*/
|
||||
protected function getSuccessString($string)
|
||||
{
|
||||
return sprintf('<info>%s</info>', $string);
|
||||
}
|
||||
}
|
||||
44
module/CLI/src/Command/Config/GenerateCharsetCommand.php
Normal file
44
module/CLI/src/Command/Config/GenerateCharsetCommand.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateCharsetCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* GenerateCharsetCommand constructor.
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({"translator"})
|
||||
*/
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('config:generate-charset')
|
||||
->setDescription(sprintf($this->translator->translate(
|
||||
'Generates a character set sample just by shuffling the default one, "%s". '
|
||||
. 'Then it can be set in the SHORTCODE_CHARS environment variable'
|
||||
), UrlShortener::DEFAULT_CHARS));
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
|
||||
$output->writeln($this->translator->translate('Character set:') . sprintf(' <info>%s</info>', $charSet));
|
||||
}
|
||||
}
|
||||
45
module/CLI/src/Command/Config/GenerateSecretCommand.php
Normal file
45
module/CLI/src/Command/Config/GenerateSecretCommand.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateSecretCommand extends Command
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* GenerateCharsetCommand constructor.
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({"translator"})
|
||||
*/
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('config:generate-secret')
|
||||
->setDescription($this->translator->translate(
|
||||
'Generates a random secret string that can be used for JWT token encryption'
|
||||
));
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$secret = $this->generateRandomString(32);
|
||||
$output->writeln($this->translator->translate('Secret key:') . sprintf(' <info>%s</info>', $secret));
|
||||
}
|
||||
}
|
||||
307
module/CLI/src/Command/Install/InstallCommand.php
Normal file
307
module/CLI/src/Command/Install/InstallCommand.php
Normal file
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Install;
|
||||
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class InstallCommand extends Command
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
const DATABASE_DRIVERS = [
|
||||
'MySQL' => 'pdo_mysql',
|
||||
'PostgreSQL' => 'pdo_pgsql',
|
||||
'SQLite' => 'pdo_sqlite',
|
||||
];
|
||||
const SUPPORTED_LANGUAGES = ['en', 'es'];
|
||||
|
||||
/**
|
||||
* @var InputInterface
|
||||
*/
|
||||
private $input;
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
private $output;
|
||||
/**
|
||||
* @var QuestionHelper
|
||||
*/
|
||||
private $questionHelper;
|
||||
/**
|
||||
* @var ProcessHelper
|
||||
*/
|
||||
private $processHelper;
|
||||
/**
|
||||
* @var WriterInterface
|
||||
*/
|
||||
private $configWriter;
|
||||
|
||||
/**
|
||||
* InstallCommand constructor.
|
||||
* @param WriterInterface $configWriter
|
||||
* @param callable|null $databaseCreationLogic
|
||||
*/
|
||||
public function __construct(WriterInterface $configWriter)
|
||||
{
|
||||
parent::__construct(null);
|
||||
$this->configWriter = $configWriter;
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shlink:install')
|
||||
->setDescription('Installs Shlink');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$this->input = $input;
|
||||
$this->output = $output;
|
||||
$this->questionHelper = $this->getHelper('question');
|
||||
$this->processHelper = $this->getHelper('process');
|
||||
$params = [];
|
||||
|
||||
$output->writeln([
|
||||
'<info>Welcome to Shlink!!</info>',
|
||||
'This process will guide you through the installation.',
|
||||
]);
|
||||
|
||||
// Check if a cached config file exists and drop it if so
|
||||
if (file_exists('data/cache/app_config.php')) {
|
||||
$output->write('Deleting old cached config...');
|
||||
if (unlink('data/cache/app_config.php')) {
|
||||
$output->writeln(' <info>Success</info>');
|
||||
} else {
|
||||
$output->writeln(
|
||||
' <error>Failed!</error> You will have to manually delete the data/cache/app_config.php file to get'
|
||||
. ' new config applied.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Ask for custom config params
|
||||
$params['DATABASE'] = $this->askDatabase();
|
||||
$params['URL_SHORTENER'] = $this->askUrlShortener();
|
||||
$params['LANGUAGE'] = $this->askLanguage();
|
||||
$params['APP'] = $this->askApplication();
|
||||
|
||||
// Generate config params files
|
||||
$config = $this->buildAppConfig($params);
|
||||
$this->configWriter->toFile('config/params/generated_config.php', $config, false);
|
||||
$output->writeln(['<info>Custom configuration properly generated!</info>', '']);
|
||||
|
||||
// Generate database
|
||||
if (! $this->createDatabase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Run database migrations
|
||||
$output->writeln('Updating database...');
|
||||
if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate proxies
|
||||
$output->writeln('Generating proxies...');
|
||||
if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
protected function askDatabase()
|
||||
{
|
||||
$params = [];
|
||||
$this->printTitle('DATABASE');
|
||||
|
||||
// Select database type
|
||||
$databases = array_keys(self::DATABASE_DRIVERS);
|
||||
$dbType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||
'<question>Select database type (defaults to ' . $databases[0] . '):</question>',
|
||||
$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'] = $this->ask('Database name', 'shlink');
|
||||
$params['USER'] = $this->ask('Database username');
|
||||
$params['PASSWORD'] = $this->ask('Database password');
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
protected function askUrlShortener()
|
||||
{
|
||||
$this->printTitle('URL SHORTENER');
|
||||
|
||||
// Ask for URL shortener params
|
||||
return [
|
||||
'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||
'<question>Select schema for generated short URLs (defaults to http):</question>',
|
||||
['http', 'https'],
|
||||
0
|
||||
)),
|
||||
'HOSTNAME' => $this->ask('Hostname for generated URLs'),
|
||||
'CHARS' => $this->ask(
|
||||
'Character set for generated short codes (leave empty to autogenerate one)',
|
||||
null,
|
||||
true
|
||||
) ?: str_shuffle(UrlShortener::DEFAULT_CHARS)
|
||||
];
|
||||
}
|
||||
|
||||
protected function askLanguage()
|
||||
{
|
||||
$this->printTitle('LANGUAGE');
|
||||
|
||||
return [
|
||||
'DEFAULT' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||
'<question>Select default language for the application in general (defaults to '
|
||||
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
|
||||
self::SUPPORTED_LANGUAGES,
|
||||
0
|
||||
)),
|
||||
'CLI' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||
'<question>Select default language for CLI executions (defaults to '
|
||||
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
|
||||
self::SUPPORTED_LANGUAGES,
|
||||
0
|
||||
)),
|
||||
];
|
||||
}
|
||||
|
||||
protected function askApplication()
|
||||
{
|
||||
$this->printTitle('APPLICATION');
|
||||
|
||||
return [
|
||||
'SECRET' => $this->ask(
|
||||
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
|
||||
null,
|
||||
true
|
||||
) ?: $this->generateRandomString(32),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
*/
|
||||
protected function printTitle($text)
|
||||
{
|
||||
$text = trim($text);
|
||||
$length = strlen($text) + 4;
|
||||
$header = str_repeat('*', $length);
|
||||
|
||||
$this->output->writeln([
|
||||
'',
|
||||
'<info>' . $header . '</info>',
|
||||
'<info>* ' . strtoupper($text) . ' *</info>',
|
||||
'<info>' . $header . '</info>',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $text
|
||||
* @param string|null $default
|
||||
* @param bool $allowEmpty
|
||||
* @return string
|
||||
*/
|
||||
protected function ask($text, $default = null, $allowEmpty = false)
|
||||
{
|
||||
if (isset($default)) {
|
||||
$text .= ' (defaults to ' . $default . ')';
|
||||
}
|
||||
do {
|
||||
$value = $this->questionHelper->ask($this->input, $this->output, new Question(
|
||||
'<question>' . $text . ':</question> ',
|
||||
$default
|
||||
));
|
||||
if (empty($value) && ! $allowEmpty) {
|
||||
$this->output->writeln('<error>Value can\'t be empty</error>');
|
||||
}
|
||||
} while (empty($value) && empty($default) && ! $allowEmpty);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $params
|
||||
* @return array
|
||||
*/
|
||||
protected function buildAppConfig(array $params)
|
||||
{
|
||||
// Build simple config
|
||||
$config = [
|
||||
'app_options' => [
|
||||
'secret_key' => $params['APP']['SECRET'],
|
||||
],
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => $params['DATABASE']['DRIVER'],
|
||||
],
|
||||
],
|
||||
'translator' => [
|
||||
'locale' => $params['LANGUAGE']['DEFAULT'],
|
||||
],
|
||||
'cli' => [
|
||||
'locale' => $params['LANGUAGE']['CLI'],
|
||||
],
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => $params['URL_SHORTENER']['SCHEMA'],
|
||||
'hostname' => $params['URL_SHORTENER']['HOSTNAME'],
|
||||
],
|
||||
'shortcode_chars' => $params['URL_SHORTENER']['CHARS'],
|
||||
],
|
||||
];
|
||||
|
||||
// Build dynamic database config
|
||||
if ($params['DATABASE']['DRIVER'] === 'pdo_sqlite') {
|
||||
$config['entity_manager']['connection']['path'] = 'data/database.sqlite';
|
||||
} else {
|
||||
$config['entity_manager']['connection']['user'] = $params['DATABASE']['USER'];
|
||||
$config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD'];
|
||||
$config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME'];
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
|
||||
protected function createDatabase()
|
||||
{
|
||||
$this->output->writeln('Initializing database...');
|
||||
return $this->runCommand('php vendor/bin/doctrine.php orm:schema-tool:create', 'Error generating database.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $command
|
||||
* @param string $errorMessage
|
||||
* @return bool
|
||||
*/
|
||||
protected function runCommand($command, $errorMessage)
|
||||
{
|
||||
$process = $this->processHelper->run($this->output, $command);
|
||||
if ($process->isSuccessful()) {
|
||||
$this->output->writeln(' <info>Success!</info>');
|
||||
return true;
|
||||
} else {
|
||||
if ($this->output->isVerbose()) {
|
||||
return false;
|
||||
}
|
||||
$this->output->writeln(
|
||||
' <error>' . $errorMessage . '</error> Run this command with -vvv to see specific error info.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
12
module/CLI/src/Command/Install/UpdateCommand.php
Normal file
12
module/CLI/src/Command/Install/UpdateCommand.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Install;
|
||||
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class UpdateCommand extends InstallCommand
|
||||
{
|
||||
public function createDatabase()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
89
module/CLI/src/Command/Shortcode/GeneratePreviewCommand.php
Normal file
89
module/CLI/src/Command/Shortcode/GeneratePreviewCommand.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GeneratePreviewCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var PreviewGeneratorInterface
|
||||
*/
|
||||
private $previewGenerator;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
*/
|
||||
private $shortUrlService;
|
||||
|
||||
/**
|
||||
* GeneratePreviewCommand constructor.
|
||||
* @param ShortUrlServiceInterface $shortUrlService
|
||||
* @param PreviewGeneratorInterface $previewGenerator
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({ShortUrlService::class, PreviewGenerator::class, "translator"})
|
||||
*/
|
||||
public function __construct(
|
||||
ShortUrlServiceInterface $shortUrlService,
|
||||
PreviewGeneratorInterface $previewGenerator,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->previewGenerator = $previewGenerator;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:process-previews')
|
||||
->setDescription(
|
||||
$this->translator->translate(
|
||||
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$page = 1;
|
||||
do {
|
||||
$shortUrls = $this->shortUrlService->listShortUrls($page);
|
||||
$page += 1;
|
||||
|
||||
foreach ($shortUrls as $shortUrl) {
|
||||
$this->processUrl($shortUrl->getOriginalUrl(), $output);
|
||||
}
|
||||
} while ($page <= $shortUrls->count());
|
||||
|
||||
$output->writeln('<info>' . $this->translator->translate('Finished processing all URLs') . '</info>');
|
||||
}
|
||||
|
||||
protected function processUrl($url, OutputInterface $output)
|
||||
{
|
||||
try {
|
||||
$output->write(sprintf($this->translator->translate('Processing URL %s...'), $url));
|
||||
$this->previewGenerator->generatePreview($url);
|
||||
$output->writeln($this->translator->translate(' <info>Success!</info>'));
|
||||
} catch (PreviewGenerationException $e) {
|
||||
$messages = [' <error>' . $this->translator->translate('Error') . '</error>'];
|
||||
if ($output->isVerbose()) {
|
||||
$messages[] = '<error>' . $e->__toString() . '</error>';
|
||||
}
|
||||
|
||||
$output->writeln($messages);
|
||||
}
|
||||
}
|
||||
}
|
||||
122
module/CLI/src/Command/Shortcode/GenerateShortcodeCommand.php
Normal file
122
module/CLI/src/Command/Shortcode/GenerateShortcodeCommand.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Zend\Diactoros\Uri;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GenerateShortcodeCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $domainConfig;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* GenerateShortcodeCommand constructor.
|
||||
* @param UrlShortenerInterface $urlShortener
|
||||
* @param TranslatorInterface $translator
|
||||
* @param array $domainConfig
|
||||
*
|
||||
* @Inject({UrlShortener::class, "translator", "config.url_shortener.domain"})
|
||||
*/
|
||||
public function __construct(
|
||||
UrlShortenerInterface $urlShortener,
|
||||
TranslatorInterface $translator,
|
||||
array $domainConfig
|
||||
) {
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->translator = $translator;
|
||||
$this->domainConfig = $domainConfig;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:generate')
|
||||
->setDescription(
|
||||
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
|
||||
)
|
||||
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('Tags to apply to the new short URL')
|
||||
);
|
||||
}
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
if (! empty($longUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new Question(sprintf(
|
||||
'<question>%s</question> ',
|
||||
$this->translator->translate('A long URL was not provided. Which URL do you want to shorten?:')
|
||||
));
|
||||
|
||||
$longUrl = $helper->ask($input, $output, $question);
|
||||
if (! empty($longUrl)) {
|
||||
$input->setArgument('longUrl', $longUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$longUrl = $input->getArgument('longUrl');
|
||||
$tags = $input->getOption('tags');
|
||||
$processedTags = [];
|
||||
foreach ($tags as $key => $tag) {
|
||||
$explodedTags = explode(',', $tag);
|
||||
$processedTags = array_merge($processedTags, $explodedTags);
|
||||
}
|
||||
$tags = $processedTags;
|
||||
|
||||
try {
|
||||
if (! isset($longUrl)) {
|
||||
$output->writeln(sprintf('<error>%s</error>', $this->translator->translate('A URL was not provided!')));
|
||||
return;
|
||||
}
|
||||
|
||||
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl), $tags);
|
||||
$shortUrl = (new Uri())->withPath($shortCode)
|
||||
->withScheme($this->domainConfig['schema'])
|
||||
->withHost($this->domainConfig['hostname']);
|
||||
|
||||
$output->writeln([
|
||||
sprintf('%s <info>%s</info>', $this->translator->translate('Processed URL:'), $longUrl),
|
||||
sprintf('%s <info>%s</info>', $this->translator->translate('Generated URL:'), $shortUrl),
|
||||
]);
|
||||
} catch (InvalidUrlException $e) {
|
||||
$output->writeln(sprintf(
|
||||
'<error>' . $this->translator->translate(
|
||||
'Provided URL "%s" is invalid. Try with a different one.'
|
||||
) . '</error>',
|
||||
$longUrl
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
122
module/CLI/src/Command/Shortcode/GetVisitsCommand.php
Normal file
122
module/CLI/src/Command/Shortcode/GetVisitsCommand.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class GetVisitsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var VisitsTrackerInterface
|
||||
*/
|
||||
private $visitsTracker;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* GetVisitsCommand constructor.
|
||||
* @param VisitsTrackerInterface $visitsTracker
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({VisitsTracker::class, "translator"})
|
||||
*/
|
||||
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
|
||||
{
|
||||
$this->visitsTracker = $visitsTracker;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:visits')
|
||||
->setDescription(
|
||||
$this->translator->translate('Returns the detailed visits information for provided short code')
|
||||
)
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code which visits we want to get')
|
||||
)
|
||||
->addOption(
|
||||
'startDate',
|
||||
's',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('Allows to filter visits, returning only those older than start date')
|
||||
)
|
||||
->addOption(
|
||||
'endDate',
|
||||
'e',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
$this->translator->translate('Allows to filter visits, returning only those newer than end date')
|
||||
);
|
||||
}
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
if (! empty($shortCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new Question(sprintf(
|
||||
'<question>%s</question> ',
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to use?:')
|
||||
));
|
||||
|
||||
$shortCode = $helper->ask($input, $output, $question);
|
||||
if (! empty($shortCode)) {
|
||||
$input->setArgument('shortCode', $shortCode);
|
||||
}
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$shortCode = $input->getArgument('shortCode');
|
||||
$startDate = $this->getDateOption($input, 'startDate');
|
||||
$endDate = $this->getDateOption($input, 'endDate');
|
||||
|
||||
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
|
||||
$table = new Table($output);
|
||||
$table->setHeaders([
|
||||
$this->translator->translate('Referer'),
|
||||
$this->translator->translate('Date'),
|
||||
$this->translator->translate('Remote Address'),
|
||||
$this->translator->translate('User agent'),
|
||||
]);
|
||||
|
||||
foreach ($visits as $row) {
|
||||
$rowData = $row->jsonSerialize();
|
||||
// Unset location info
|
||||
unset($rowData['visitLocation']);
|
||||
|
||||
$table->addRow(array_values($rowData));
|
||||
}
|
||||
$table->render();
|
||||
}
|
||||
|
||||
protected function getDateOption(InputInterface $input, $key)
|
||||
{
|
||||
$value = $input->getOption($key);
|
||||
if (isset($value)) {
|
||||
$value = new \DateTime($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
122
module/CLI/src/Command/Shortcode/ListShortcodesCommand.php
Normal file
122
module/CLI/src/Command/Shortcode/ListShortcodesCommand.php
Normal file
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ListShortcodesCommand extends Command
|
||||
{
|
||||
use PaginatorUtilsTrait;
|
||||
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
*/
|
||||
private $shortUrlService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* ListShortcodesCommand constructor.
|
||||
* @param ShortUrlServiceInterface $shortUrlService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({ShortUrlService::class, "translator"})
|
||||
*/
|
||||
public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->shortUrlService = $shortUrlService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:list')
|
||||
->setDescription($this->translator->translate('List all short URLs'))
|
||||
->addOption(
|
||||
'page',
|
||||
'p',
|
||||
InputOption::VALUE_OPTIONAL,
|
||||
sprintf(
|
||||
$this->translator->translate('The first page to list (%s items per page)'),
|
||||
PaginableRepositoryAdapter::ITEMS_PER_PAGE
|
||||
),
|
||||
1
|
||||
)
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
InputOption::VALUE_NONE,
|
||||
$this->translator->translate('Whether to display the tags or not')
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$page = intval($input->getOption('page'));
|
||||
$showTags = $input->getOption('tags');
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
|
||||
do {
|
||||
$result = $this->shortUrlService->listShortUrls($page);
|
||||
$page++;
|
||||
$table = new Table($output);
|
||||
|
||||
$headers = [
|
||||
$this->translator->translate('Short code'),
|
||||
$this->translator->translate('Original URL'),
|
||||
$this->translator->translate('Date created'),
|
||||
$this->translator->translate('Visits count'),
|
||||
];
|
||||
if ($showTags) {
|
||||
$headers[] = $this->translator->translate('Tags');
|
||||
}
|
||||
$table->setHeaders($headers);
|
||||
|
||||
foreach ($result as $row) {
|
||||
$shortUrl = $row->jsonSerialize();
|
||||
if ($showTags) {
|
||||
$shortUrl['tags'] = [];
|
||||
foreach ($row->getTags() as $tag) {
|
||||
$shortUrl['tags'][] = $tag->getName();
|
||||
}
|
||||
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
|
||||
} else {
|
||||
unset($shortUrl['tags']);
|
||||
}
|
||||
|
||||
$table->addRow(array_values($shortUrl));
|
||||
}
|
||||
$table->render();
|
||||
|
||||
if ($this->isLastPage($result)) {
|
||||
$continue = false;
|
||||
$output->writeln(
|
||||
sprintf('<info>%s</info>', $this->translator->translate('You have reached last page'))
|
||||
);
|
||||
} else {
|
||||
$continue = $helper->ask($input, $output, new ConfirmationQuestion(
|
||||
sprintf('<question>' . $this->translator->translate(
|
||||
'Continue with page'
|
||||
) . ' <bg=cyan;options=bold>%s</>? (y/N)</question> ', $page),
|
||||
false
|
||||
));
|
||||
}
|
||||
} while ($continue);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
<?php
|
||||
namespace Acelaya\UrlShortener\CLI\Command;
|
||||
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
|
||||
use Acelaya\UrlShortener\Service\UrlShortener;
|
||||
use Acelaya\UrlShortener\Service\UrlShortenerInterface;
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ResolveUrlCommand extends Command
|
||||
{
|
||||
@@ -18,24 +19,34 @@ class ResolveUrlCommand extends Command
|
||||
* @var UrlShortenerInterface
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* ResolveUrlCommand constructor.
|
||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
||||
* @param UrlShortenerInterface $urlShortener
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({UrlShortener::class})
|
||||
* @Inject({UrlShortener::class, "translator"})
|
||||
*/
|
||||
public function __construct(UrlShortenerInterface $urlShortener)
|
||||
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
|
||||
{
|
||||
parent::__construct(null);
|
||||
$this->urlShortener = $urlShortener;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shortcode:parse')
|
||||
->setDescription('Returns the long URL behind a short code')
|
||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse');
|
||||
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
|
||||
->addArgument(
|
||||
'shortCode',
|
||||
InputArgument::REQUIRED,
|
||||
$this->translator->translate('The short code to parse')
|
||||
);
|
||||
}
|
||||
|
||||
public function interact(InputInterface $input, OutputInterface $output)
|
||||
@@ -47,9 +58,10 @@ class ResolveUrlCommand extends Command
|
||||
|
||||
/** @var QuestionHelper $helper */
|
||||
$helper = $this->getHelper('question');
|
||||
$question = new Question(
|
||||
'<question>A short code was not provided. Which short code do you want to parse?:</question> '
|
||||
);
|
||||
$question = new Question(sprintf(
|
||||
'<question>%s</question> ',
|
||||
$this->translator->translate('A short code was not provided. Which short code do you want to parse?:')
|
||||
));
|
||||
|
||||
$shortCode = $helper->ask($input, $output, $question);
|
||||
if (! empty($shortCode)) {
|
||||
@@ -64,15 +76,18 @@ class ResolveUrlCommand extends Command
|
||||
try {
|
||||
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||
if (! isset($longUrl)) {
|
||||
$output->writeln(sprintf('<error>No URL found for short code "%s"</error>', $shortCode));
|
||||
$output->writeln(sprintf(
|
||||
'<error>' . $this->translator->translate('No URL found for short code "%s"') . '</error>',
|
||||
$shortCode
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
$output->writeln(sprintf('Long URL <info>%s</info>', $longUrl));
|
||||
$output->writeln(sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $longUrl));
|
||||
} catch (InvalidShortCodeException $e) {
|
||||
$output->writeln(
|
||||
sprintf('<error>Provided short code "%s" has an invalid format.</error>', $shortCode)
|
||||
);
|
||||
$output->writeln(sprintf('<error>' . $this->translator->translate(
|
||||
'Provided short code "%s" has an invalid format.'
|
||||
) . '</error>', $shortCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
91
module/CLI/src/Command/Visit/ProcessVisitsCommand.php
Normal file
91
module/CLI/src/Command/Visit/ProcessVisitsCommand.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ProcessVisitsCommand extends Command
|
||||
{
|
||||
const LOCALHOST = '127.0.0.1';
|
||||
|
||||
/**
|
||||
* @var VisitServiceInterface
|
||||
*/
|
||||
private $visitService;
|
||||
/**
|
||||
* @var IpLocationResolverInterface
|
||||
*/
|
||||
private $ipLocationResolver;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* ProcessVisitsCommand constructor.
|
||||
* @param VisitServiceInterface $visitService
|
||||
* @param IpLocationResolverInterface $ipLocationResolver
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @Inject({VisitService::class, IpLocationResolver::class, "translator"})
|
||||
*/
|
||||
public function __construct(
|
||||
VisitServiceInterface $visitService,
|
||||
IpLocationResolverInterface $ipLocationResolver,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
$this->visitService = $visitService;
|
||||
$this->ipLocationResolver = $ipLocationResolver;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('visit:process')
|
||||
->setDescription(
|
||||
$this->translator->translate('Processes visits where location is not set yet')
|
||||
);
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$visits = $this->visitService->getUnlocatedVisits();
|
||||
|
||||
foreach ($visits as $visit) {
|
||||
$ipAddr = $visit->getRemoteAddr();
|
||||
$output->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
|
||||
if ($ipAddr === self::LOCALHOST) {
|
||||
$output->writeln(
|
||||
sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->ipLocationResolver->resolveIpLocation($ipAddr);
|
||||
$location = new VisitLocation();
|
||||
$location->exchangeArray($result);
|
||||
$visit->setVisitLocation($location);
|
||||
$this->visitService->saveVisit($visit);
|
||||
$output->writeln(sprintf(
|
||||
' (' . $this->translator->translate('Address located at "%s"') . ')',
|
||||
$location->getCityName()
|
||||
));
|
||||
} catch (WrongIpException $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$output->writeln($this->translator->translate('Finished processing all IPs'));
|
||||
}
|
||||
}
|
||||
13
module/CLI/src/ConfigProvider.php
Normal file
13
module/CLI/src/ConfigProvider.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
<?php
|
||||
namespace Acelaya\UrlShortener\CLI\Factory;
|
||||
namespace Shlinkio\Shlink\CLI\Factory;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
@@ -25,9 +27,12 @@ class ApplicationFactory implements FactoryInterface
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->get('config')['cli'];
|
||||
$app = new CliApp();
|
||||
$appOptions = $container->get(AppOptions::class);
|
||||
$translator = $container->get(Translator::class);
|
||||
$translator->setLocale($config['locale']);
|
||||
|
||||
$commands = isset($config['commands']) ? $config['commands'] : [];
|
||||
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());
|
||||
foreach ($commands as $command) {
|
||||
if (! $container->has($command)) {
|
||||
continue;
|
||||
62
module/CLI/test/Command/Api/DisableKeyCommandTest.php
Normal file
62
module/CLI/test/Command/Api/DisableKeyCommandTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class DisableKeyCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $apiKeyService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||
$command = new DisableKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function providedApiKeyIsDisabled()
|
||||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$this->apiKeyService->disable($apiKey)->shouldBeCalledTimes(1);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:disable',
|
||||
'apiKey' => $apiKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function errorIsReturnedIfServiceThrowsException()
|
||||
{
|
||||
$apiKey = 'abcd1234';
|
||||
$this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class)
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:disable',
|
||||
'apiKey' => $apiKey,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals('API key "abcd1234" does not exist.' . PHP_EOL, $output);
|
||||
}
|
||||
}
|
||||
55
module/CLI/test/Command/Api/GenerateKeyCommandTest.php
Normal file
55
module/CLI/test/Command/Api/GenerateKeyCommandTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class GenerateKeyCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $apiKeyService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||
$command = new GenerateKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function noExpirationDateIsDefinedIfNotProvided()
|
||||
{
|
||||
$this->apiKeyService->create(null)->shouldBeCalledTimes(1);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function expirationDateIsDefinedIfWhenProvided()
|
||||
{
|
||||
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:generate',
|
||||
'--expirationDate' => '2016-01-01',
|
||||
]);
|
||||
}
|
||||
}
|
||||
62
module/CLI/test/Command/Api/ListKeysCommandTest.php
Normal file
62
module/CLI/test/Command/Api/ListKeysCommandTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class ListKeysCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $apiKeyService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||
$command = new ListKeysCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function ifEnabledOnlyIsNotProvidedEverythingIsListed()
|
||||
{
|
||||
$this->apiKeyService->listKeys(false)->willReturn([
|
||||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
])->shouldBeCalledTimes(1);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:list',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function ifEnabledOnlyIsProvidedOnlyThoseKeysAreListed()
|
||||
{
|
||||
$this->apiKeyService->listKeys(true)->willReturn([
|
||||
new ApiKey(),
|
||||
new ApiKey(),
|
||||
])->shouldBeCalledTimes(1);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'api-key:list',
|
||||
'--enabledOnly' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Config;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class GenerateCharsetCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$command = new GenerateCharsetCommand(Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function charactersAreGeneratedFromDefault()
|
||||
{
|
||||
$prefix = 'Character set: ';
|
||||
$prefixLength = strlen($prefix);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'config:generate-charset',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
// Both default character set and the new one should have the same length
|
||||
$this->assertEquals($prefixLength + strlen(UrlShortener::DEFAULT_CHARS) + 1, strlen($output));
|
||||
|
||||
// Both default character set and the new one should have the same characters
|
||||
$charset = substr($output, $prefixLength, strlen(UrlShortener::DEFAULT_CHARS));
|
||||
$orderedDefault = $this->orderStringLetters(UrlShortener::DEFAULT_CHARS);
|
||||
$orderedCharset = $this->orderStringLetters($charset);
|
||||
$this->assertEquals($orderedDefault, $orderedCharset);
|
||||
}
|
||||
|
||||
protected function orderStringLetters($string)
|
||||
{
|
||||
$letters = str_split($string);
|
||||
sort($letters);
|
||||
return implode('', $letters);
|
||||
}
|
||||
}
|
||||
104
module/CLI/test/Command/Install/InstallCommandTest.php
Normal file
104
module/CLI/test/Command/Install/InstallCommandTest.php
Normal file
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Install;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class InstallCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $configWriter;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$processMock = $this->prophesize(Process::class);
|
||||
$processMock->isSuccessful()->willReturn(true);
|
||||
$processHelper = $this->prophesize(ProcessHelper::class);
|
||||
$processHelper->getName()->willReturn('process');
|
||||
$processHelper->setHelperSet(Argument::any())->willReturn(null);
|
||||
$processHelper->run(Argument::cetera())->willReturn($processMock->reveal());
|
||||
|
||||
$app = new Application();
|
||||
$helperSet = $app->getHelperSet();
|
||||
$helperSet->set($processHelper->reveal());
|
||||
$app->setHelperSet($helperSet);
|
||||
|
||||
$this->configWriter = $this->prophesize(WriterInterface::class);
|
||||
$command = new InstallCommand($this->configWriter->reveal());
|
||||
$app->add($command);
|
||||
|
||||
$questionHelper = $command->getHelper('question');
|
||||
$questionHelper->setInputStream($this->createInputStream());
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
protected function createInputStream()
|
||||
{
|
||||
$stream = fopen('php://memory', 'r+', false);
|
||||
fputs($stream, <<<CLI_INPUT
|
||||
|
||||
shlink_db
|
||||
alejandro
|
||||
1234
|
||||
0
|
||||
doma.in
|
||||
abc123BCA
|
||||
|
||||
1
|
||||
my_secret
|
||||
CLI_INPUT
|
||||
);
|
||||
rewind($stream);
|
||||
|
||||
return $stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function testInputIsProperlyParsed()
|
||||
{
|
||||
$this->configWriter->toFile(Argument::any(), [
|
||||
'app_options' => [
|
||||
'secret_key' => 'my_secret',
|
||||
],
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => 'pdo_mysql',
|
||||
'dbname' => 'shlink_db',
|
||||
'user' => 'alejandro',
|
||||
'password' => '1234',
|
||||
],
|
||||
],
|
||||
'translator' => [
|
||||
'locale' => 'en',
|
||||
],
|
||||
'cli' => [
|
||||
'locale' => 'es',
|
||||
],
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => 'http',
|
||||
'hostname' => 'doma.in',
|
||||
],
|
||||
'shortcode_chars' => 'abc123BCA',
|
||||
],
|
||||
], false)->shouldBeCalledTimes(1);
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shlink:install',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\GeneratePreviewCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class GeneratePreviewCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $previewGenerator;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $shortUrlService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
|
||||
|
||||
$command = new GeneratePreviewCommand(
|
||||
$this->shortUrlService->reveal(),
|
||||
$this->previewGenerator->reveal(),
|
||||
Translator::factory([])
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function previewsForEveryUrlAreGenerated()
|
||||
{
|
||||
$paginator = $this->createPaginator([
|
||||
(new ShortUrl())->setOriginalUrl('http://foo.com'),
|
||||
(new ShortUrl())->setOriginalUrl('https://bar.com'),
|
||||
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
|
||||
]);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
|
||||
|
||||
$this->previewGenerator->generatePreview('http://foo.com')->shouldBeCalledTimes(1);
|
||||
$this->previewGenerator->generatePreview('https://bar.com')->shouldBeCalledTimes(1);
|
||||
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:process-previews'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function exceptionWillOutputError()
|
||||
{
|
||||
$items = [
|
||||
(new ShortUrl())->setOriginalUrl('http://foo.com'),
|
||||
(new ShortUrl())->setOriginalUrl('https://bar.com'),
|
||||
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
|
||||
];
|
||||
$paginator = $this->createPaginator($items);
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
|
||||
$this->previewGenerator->generatePreview(Argument::any())->willThrow(PreviewGenerationException::class)
|
||||
->shouldBeCalledTimes(count($items));
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:process-previews'
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals(count($items), substr_count($output, 'Error'));
|
||||
}
|
||||
|
||||
protected function createPaginator(array $items)
|
||||
{
|
||||
$paginator = new Paginator(new ArrayAdapter($items));
|
||||
$paginator->setItemCountPerPage(count($items));
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\GenerateShortcodeCommand;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class GenerateShortcodeCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $urlShortener;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||
$command = new GenerateShortcodeCommand($this->urlShortener->reveal(), Translator::factory([]), [
|
||||
'schema' => 'http',
|
||||
'hostname' => 'foo.com'
|
||||
]);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function properShortCodeIsCreatedIfLongUrlIsCorrect()
|
||||
{
|
||||
$this->urlShortener->urlToShortCode(Argument::cetera())->willReturn('abc123')
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:generate',
|
||||
'longUrl' => 'http://domain.com/foo/bar'
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(strpos($output, 'http://foo.com/abc123') > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function exceptionWhileParsingLongUrlOutputsError()
|
||||
{
|
||||
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:generate',
|
||||
'longUrl' => 'http://domain.com/invalid'
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(
|
||||
strpos($output, 'Provided URL "http://domain.com/invalid" is invalid. Try with a different one.') === 0
|
||||
);
|
||||
}
|
||||
}
|
||||
91
module/CLI/test/Command/Shortcode/GetVisitsCommandTest.php
Normal file
91
module/CLI/test/Command/Shortcode/GetVisitsCommandTest.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\GetVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class GetVisitsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $visitsTracker;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->visitsTracker = $this->prophesize(VisitsTrackerInterface::class);
|
||||
$command = new GetVisitsCommand($this->visitsTracker->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function noDateFlagsTriesToListWithoutDateRange()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function providingDateFlagsTheListGetsFiltered()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$startDate = '2016-01-01';
|
||||
$endDate = '2016-02-01';
|
||||
$this->visitsTracker->info($shortCode, new DateRange(new \DateTime($startDate), new \DateTime($endDate)))
|
||||
->willReturn([])
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
'shortCode' => $shortCode,
|
||||
'--startDate' => $startDate,
|
||||
'--endDate' => $endDate,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function outputIsProperlyGenerated()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->visitsTracker->info($shortCode, Argument::any())->willReturn([
|
||||
(new Visit())->setReferer('foo')
|
||||
->setRemoteAddr('1.2.3.4')
|
||||
->setUserAgent('bar'),
|
||||
])->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:visits',
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(strpos($output, 'foo') > 0);
|
||||
$this->assertTrue(strpos($output, '1.2.3.4') > 0);
|
||||
$this->assertTrue(strpos($output, 'bar') > 0);
|
||||
}
|
||||
}
|
||||
136
module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php
Normal file
136
module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\ListShortcodesCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||
use Zend\Paginator\Paginator;
|
||||
|
||||
class ListShortcodesCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var QuestionHelper
|
||||
*/
|
||||
protected $questionHelper;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $shortUrlService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
|
||||
$app = new Application();
|
||||
$command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([]));
|
||||
$app->add($command);
|
||||
|
||||
$this->questionHelper = $command->getHelper('question');
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function noInputCallsListJustOnce()
|
||||
{
|
||||
$this->questionHelper->setInputStream($this->getInputStream('\n'));
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function loadingMorePagesCallsListMoreTimes()
|
||||
{
|
||||
// The paginator will return more than one page for the first 3 times
|
||||
$data = [];
|
||||
for ($i = 0; $i < 30; $i++) {
|
||||
$data[] = new ShortUrl();
|
||||
}
|
||||
$data = array_chunk($data, 11);
|
||||
|
||||
$questionHelper = $this->questionHelper;
|
||||
$that = $this;
|
||||
$this->shortUrlService->listShortUrls(Argument::any())->will(function () use (&$data, $questionHelper, $that) {
|
||||
$questionHelper->setInputStream($that->getInputStream('y'));
|
||||
return new Paginator(new ArrayAdapter(array_shift($data)));
|
||||
})->shouldBeCalledTimes(3);
|
||||
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function havingMorePagesButAnsweringNoCallsListJustOnce()
|
||||
{
|
||||
// The paginator will return more than one page
|
||||
$data = [];
|
||||
for ($i = 0; $i < 30; $i++) {
|
||||
$data[] = new ShortUrl();
|
||||
}
|
||||
|
||||
$this->questionHelper->setInputStream($this->getInputStream('n'));
|
||||
$this->shortUrlService->listShortUrls(Argument::any())->willReturn(new Paginator(new ArrayAdapter($data)))
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute(['command' => 'shortcode:list']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function passingPageWillMakeListStartOnThatPage()
|
||||
{
|
||||
$page = 5;
|
||||
$this->questionHelper->setInputStream($this->getInputStream('\n'));
|
||||
$this->shortUrlService->listShortUrls($page)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:list',
|
||||
'--page' => $page,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
|
||||
{
|
||||
$this->questionHelper->setInputStream($this->getInputStream('\n'));
|
||||
$this->shortUrlService->listShortUrls(1)->willReturn(new Paginator(new ArrayAdapter()))
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:list',
|
||||
'--tags' => true,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(strpos($output, 'Tags') > 0);
|
||||
}
|
||||
|
||||
protected function getInputStream($inputData)
|
||||
{
|
||||
$stream = fopen('php://memory', 'r+', false);
|
||||
fputs($stream, $inputData);
|
||||
rewind($stream);
|
||||
|
||||
return $stream;
|
||||
}
|
||||
}
|
||||
85
module/CLI/test/Command/Shortcode/ResolveUrlCommandTest.php
Normal file
85
module/CLI/test/Command/Shortcode/ResolveUrlCommandTest.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class ResolveUrlCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $urlShortener;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||
$command = new ResolveUrlCommand($this->urlShortener->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function correctShortCodeResolvesUrl()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$expectedUrl = 'http://domain.com/foo/bar';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl)
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:parse',
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function incorrectShortCodeOutputsErrorMessage()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:parse',
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals('No URL found for short code "' . $shortCode . '"' . PHP_EOL, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function wrongShortCodeFormatOutputsErrorMessage()
|
||||
{
|
||||
$shortCode = 'abc123';
|
||||
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(new InvalidShortCodeException())
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'shortcode:parse',
|
||||
'shortCode' => $shortCode,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertEquals('Provided short code "' . $shortCode . '" has an invalid format.' . PHP_EOL, $output);
|
||||
}
|
||||
}
|
||||
96
module/CLI/test/Command/Visit/ProcessVisitsCommandTest.php
Normal file
96
module/CLI/test/Command/Visit/ProcessVisitsCommandTest.php
Normal file
@@ -0,0 +1,96 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class ProcessVisitsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
protected $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $visitService;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $ipResolver;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->visitService = $this->prophesize(VisitService::class);
|
||||
$this->ipResolver = $this->prophesize(IpLocationResolver::class);
|
||||
$command = new ProcessVisitsCommand(
|
||||
$this->visitService->reveal(),
|
||||
$this->ipResolver->reveal(),
|
||||
Translator::factory([])
|
||||
);
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function allReturnedVisitsIpsAreProcessed()
|
||||
{
|
||||
$visits = [
|
||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
||||
];
|
||||
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits));
|
||||
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||
->shouldBeCalledTimes(count($visits));
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(strpos($output, 'Processing IP 1.2.3.4') === 0);
|
||||
$this->assertTrue(strpos($output, 'Processing IP 4.3.2.1') > 0);
|
||||
$this->assertTrue(strpos($output, 'Processing IP 12.34.56.78') > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function localhostAddressIsIgnored()
|
||||
{
|
||||
$visits = [
|
||||
(new Visit())->setRemoteAddr('1.2.3.4'),
|
||||
(new Visit())->setRemoteAddr('4.3.2.1'),
|
||||
(new Visit())->setRemoteAddr('12.34.56.78'),
|
||||
(new Visit())->setRemoteAddr('127.0.0.1'),
|
||||
(new Visit())->setRemoteAddr('127.0.0.1'),
|
||||
];
|
||||
$this->visitService->getUnlocatedVisits()->willReturn($visits)
|
||||
->shouldBeCalledTimes(1);
|
||||
|
||||
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits) - 2);
|
||||
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
|
||||
->shouldBeCalledTimes(count($visits) - 2);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'command' => 'visit:process',
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertTrue(strpos($output, 'Ignored localhost address') > 0);
|
||||
}
|
||||
}
|
||||
30
module/CLI/test/ConfigProviderTest.php
Normal file
30
module/CLI/test/ConfigProviderTest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Shlinkio\Shlink\CLI\ConfigProvider;
|
||||
|
||||
class ConfigProviderTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ConfigProvider
|
||||
*/
|
||||
protected $configProvider;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->configProvider = new ConfigProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function confiIsProperlyReturned()
|
||||
{
|
||||
$config = $this->configProvider->__invoke();
|
||||
|
||||
$this->assertArrayHasKey('cli', $config);
|
||||
$this->assertArrayHasKey('dependencies', $config);
|
||||
$this->assertArrayHasKey('translator', $config);
|
||||
}
|
||||
}
|
||||
64
module/CLI/test/Factory/ApplicationFactoryTest.php
Normal file
64
module/CLI/test/Factory/ApplicationFactoryTest.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Factory;
|
||||
|
||||
use PHPUnit_Framework_TestCase as TestCase;
|
||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class ApplicationFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ApplicationFactory
|
||||
*/
|
||||
protected $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->factory = new ApplicationFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function serviceIsCreated()
|
||||
{
|
||||
$instance = $this->factory->__invoke($this->createServiceManager(), '');
|
||||
$this->assertInstanceOf(Application::class, $instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function allCommandsWhichAreServicesAreAdded()
|
||||
{
|
||||
$sm = $this->createServiceManager([
|
||||
'commands' => [
|
||||
'foo',
|
||||
'bar',
|
||||
'baz',
|
||||
],
|
||||
]);
|
||||
$sm->setService('foo', $this->prophesize(Command::class)->reveal());
|
||||
$sm->setService('baz', $this->prophesize(Command::class)->reveal());
|
||||
|
||||
/** @var Application $instance */
|
||||
$instance = $this->factory->__invoke($sm, '');
|
||||
$this->assertInstanceOf(Application::class, $instance);
|
||||
$this->assertCount(2, $instance->all());
|
||||
}
|
||||
|
||||
protected function createServiceManager($config = [])
|
||||
{
|
||||
return new ServiceManager(['services' => [
|
||||
'config' => [
|
||||
'cli' => array_merge($config, ['locale' => 'en']),
|
||||
],
|
||||
AppOptions::class => new AppOptions(),
|
||||
Translator::class => Translator::factory([]),
|
||||
]]);
|
||||
}
|
||||
}
|
||||
48
module/Common/config/dependencies.config.php
Normal file
48
module/Common/config/dependencies.config.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\Factory;
|
||||
use Shlinkio\Shlink\Common\Image;
|
||||
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
||||
use Shlinkio\Shlink\Common\Service;
|
||||
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'invokables' => [
|
||||
Filesystem::class => Filesystem::class,
|
||||
],
|
||||
'factories' => [
|
||||
EntityManager::class => Factory\EntityManagerFactory::class,
|
||||
GuzzleHttp\Client::class => InvokableFactory::class,
|
||||
Cache::class => Factory\CacheFactory::class,
|
||||
'Logger_Shlink' => Factory\LoggerFactory::class,
|
||||
|
||||
Translator::class => Factory\TranslatorFactory::class,
|
||||
TranslatorExtension::class => AnnotatedFactory::class,
|
||||
LocaleMiddleware::class => AnnotatedFactory::class,
|
||||
|
||||
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
||||
|
||||
Service\IpLocationResolver::class => AnnotatedFactory::class,
|
||||
Service\PreviewGenerator::class => AnnotatedFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'em' => EntityManager::class,
|
||||
'httpClient' => GuzzleHttp\Client::class,
|
||||
'translator' => Translator::class,
|
||||
'logger' => LoggerInterface::class,
|
||||
AnnotatedFactory::CACHE_SERVICE => Cache::class,
|
||||
Logger::class => 'Logger_Shlink',
|
||||
LoggerInterface::class => 'Logger_Shlink',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
15
module/Common/config/middleware-pipeline.config.php
Normal file
15
module/Common/config/middleware-pipeline.config.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\Common\Middleware;
|
||||
|
||||
return [
|
||||
|
||||
'middleware_pipeline' => [
|
||||
'pre-routing' => [
|
||||
'middleware' => [
|
||||
Middleware\LocaleMiddleware::class,
|
||||
],
|
||||
'priority' => 5,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
12
module/Common/config/templates.config.php
Normal file
12
module/Common/config/templates.config.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
|
||||
|
||||
return [
|
||||
|
||||
'twig' => [
|
||||
'extensions' => [
|
||||
TranslatorExtension::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
36
module/Common/functions/functions.php
Normal file
36
module/Common/functions/functions.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
if (! function_exists('env')) {
|
||||
/**
|
||||
* Gets the value of an environment variable. Supports boolean, empty and null.
|
||||
* This is basically Laravel's env helper
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
* @link https://github.com/laravel/framework/blob/5.2/src/Illuminate/Foundation/helpers.php#L369
|
||||
*/
|
||||
function env($key, $default = null)
|
||||
{
|
||||
$value = getenv($key);
|
||||
if ($value === false) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
switch (strtolower($value)) {
|
||||
case 'true':
|
||||
case '(true)':
|
||||
return true;
|
||||
case 'false':
|
||||
case '(false)':
|
||||
return false;
|
||||
case 'empty':
|
||||
case '(empty)':
|
||||
return '';
|
||||
case 'null':
|
||||
case '(null)':
|
||||
return null;
|
||||
}
|
||||
|
||||
return trim($value);
|
||||
}
|
||||
}
|
||||
13
module/Common/src/ConfigProvider.php
Normal file
13
module/Common/src/ConfigProvider.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Acelaya\UrlShortener\Entity;
|
||||
namespace Shlinkio\Shlink\Common\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
6
module/Common/src/Exception/ExceptionInterface.php
Normal file
6
module/Common/src/Exception/ExceptionInterface.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Exception;
|
||||
|
||||
interface ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Acelaya\UrlShortener\Exception;
|
||||
namespace Shlinkio\Shlink\Common\Exception;
|
||||
|
||||
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||
{
|
||||
10
module/Common/src/Exception/PreviewGenerationException.php
Normal file
10
module/Common/src/Exception/PreviewGenerationException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Exception;
|
||||
|
||||
class PreviewGenerationException extends RuntimeException
|
||||
{
|
||||
public static function fromImageError($error)
|
||||
{
|
||||
return new self(sprintf('Error generating a preview image with error: %s', $error));
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Acelaya\UrlShortener\Exception;
|
||||
namespace Shlinkio\Shlink\Common\Exception;
|
||||
|
||||
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
10
module/Common/src/Exception/WrongIpException.php
Normal file
10
module/Common/src/Exception/WrongIpException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Exception;
|
||||
|
||||
class WrongIpException extends RuntimeException
|
||||
{
|
||||
public static function fromIpAddress($ipAddress, \Exception $prev = null)
|
||||
{
|
||||
return new self(sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
|
||||
}
|
||||
}
|
||||
95
module/Common/src/Factory/CacheFactory.php
Normal file
95
module/Common/src/Factory/CacheFactory.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Factory;
|
||||
|
||||
use Doctrine\Common\Cache;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class CacheFactory implements FactoryInterface
|
||||
{
|
||||
const VALID_CACHE_ADAPTERS = [
|
||||
Cache\ApcuCache::class,
|
||||
Cache\ArrayCache::class,
|
||||
Cache\FilesystemCache::class,
|
||||
Cache\PhpFileCache::class,
|
||||
Cache\MemcachedCache::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$appOptions = $container->get(AppOptions::class);
|
||||
$adapter = $this->getAdapter($container);
|
||||
$adapter->setNamespace($appOptions->__toString());
|
||||
|
||||
return $adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ContainerInterface $container
|
||||
* @return Cache\CacheProvider
|
||||
*/
|
||||
protected function getAdapter(ContainerInterface $container)
|
||||
{
|
||||
// Try to get the adapter from config
|
||||
$config = $container->get('config');
|
||||
if (isset($config['cache'])
|
||||
&& isset($config['cache']['adapter'])
|
||||
&& in_array($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 env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $cacheConfig
|
||||
* @return Cache\CacheProvider
|
||||
*/
|
||||
protected function resolveCacheAdapter(array $cacheConfig)
|
||||
{
|
||||
switch ($cacheConfig['adapter']) {
|
||||
case Cache\ArrayCache::class:
|
||||
case Cache\ApcuCache::class:
|
||||
return new $cacheConfig['adapter']();
|
||||
case Cache\FilesystemCache::class:
|
||||
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'] : [];
|
||||
|
||||
foreach ($servers as $server) {
|
||||
if (! isset($server['host'])) {
|
||||
continue;
|
||||
}
|
||||
$port = isset($server['port']) ? intval($server['port']) : 11211;
|
||||
|
||||
$memcached->addServer($server['host'], $port);
|
||||
}
|
||||
|
||||
$cache = new Cache\MemcachedCache();
|
||||
$cache->setMemcached($memcached);
|
||||
return $cache;
|
||||
default:
|
||||
return new Cache\ArrayCache();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Acelaya\UrlShortener\Factory;
|
||||
namespace Shlinkio\Shlink\Common\Factory;
|
||||
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use Doctrine\Common\Cache\Cache;
|
||||
@@ -30,12 +30,14 @@ class EntityManagerFactory implements FactoryInterface
|
||||
$globalConfig = $container->get('config');
|
||||
$isDevMode = isset($globalConfig['debug']) ? ((bool) $globalConfig['debug']) : false;
|
||||
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
|
||||
$dbConfig = isset($globalConfig['database']) ? $globalConfig['database'] : [];
|
||||
$emConfig = isset($globalConfig['entity_manager']) ? $globalConfig['entity_manager'] : [];
|
||||
$connecitonConfig = isset($emConfig['connection']) ? $emConfig['connection'] : [];
|
||||
$ormConfig = isset($emConfig['orm']) ? $emConfig['orm'] : [];
|
||||
|
||||
return EntityManager::create($dbConfig, Setup::createAnnotationMetadataConfiguration(
|
||||
['src/Entity'],
|
||||
return EntityManager::create($connecitonConfig, Setup::createAnnotationMetadataConfiguration(
|
||||
isset($ormConfig['entities_paths']) ? $ormConfig['entities_paths'] : [],
|
||||
$isDevMode,
|
||||
'data/proxies',
|
||||
isset($ormConfig['proxies_dir']) ? $ormConfig['proxies_dir'] : null,
|
||||
$cache,
|
||||
false
|
||||
));
|
||||
39
module/Common/src/Factory/LoggerFactory.php
Normal file
39
module/Common/src/Factory/LoggerFactory.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Factory;
|
||||
|
||||
use Cascade\Cascade;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class LoggerFactory 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
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->has('config') ? $container->get('config') : [];
|
||||
Cascade::fileConfig(isset($config['logger']) ? $config['logger'] : ['loggers' => []]);
|
||||
|
||||
// Compose requested logger name
|
||||
$loggerName = isset($options) & isset($options['logger_name']) ? $options['logger_name'] : 'Logger';
|
||||
$nameParts = explode('_', $requestedName);
|
||||
if (count($nameParts) > 1) {
|
||||
$loggerName = $nameParts[1];
|
||||
}
|
||||
|
||||
return Cascade::getLogger($loggerName);
|
||||
}
|
||||
}
|
||||
30
module/Common/src/Factory/TranslatorFactory.php
Normal file
30
module/Common/src/Factory/TranslatorFactory.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Factory;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class TranslatorFactory 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
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->get('config');
|
||||
return Translator::factory(isset($config['translator']) ? $config['translator'] : []);
|
||||
}
|
||||
}
|
||||
10
module/Common/src/Image/ImageBuilder.php
Normal file
10
module/Common/src/Image/ImageBuilder.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Image;
|
||||
|
||||
use mikehaertl\wkhtmlto\Image;
|
||||
use Zend\ServiceManager\AbstractPluginManager;
|
||||
|
||||
class ImageBuilder extends AbstractPluginManager implements ImageBuilderInterface
|
||||
{
|
||||
protected $instanceOf = Image::class;
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
<?php
|
||||
namespace Acelaya\UrlShortener\Factory;
|
||||
namespace Shlinkio\Shlink\Common\Image;
|
||||
|
||||
use Doctrine\Common\Cache\ApcuCache;
|
||||
use Doctrine\Common\Cache\ArrayCache;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use mikehaertl\wkhtmlto\Image;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class CacheFactory implements FactoryInterface
|
||||
class ImageBuilderFactory implements FactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
@@ -25,6 +24,8 @@ class CacheFactory implements FactoryInterface
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
return getenv('APP_ENV') === 'pro' ? new ApcuCache() : new ArrayCache();
|
||||
return new ImageBuilder($container, ['factories' => [
|
||||
Image::class => ImageFactory::class,
|
||||
]]);
|
||||
}
|
||||
}
|
||||
8
module/Common/src/Image/ImageBuilderInterface.php
Normal file
8
module/Common/src/Image/ImageBuilderInterface.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Image;
|
||||
|
||||
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||
|
||||
interface ImageBuilderInterface extends ServiceLocatorInterface
|
||||
{
|
||||
}
|
||||
36
module/Common/src/Image/ImageFactory.php
Normal file
36
module/Common/src/Image/ImageFactory.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Image;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use mikehaertl\wkhtmlto\Image;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class ImageFactory 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
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
$config = $container->get('config')['phpwkhtmltopdf'];
|
||||
$image = new Image(isset($config['images']) ? $config['images'] : null);
|
||||
|
||||
if (isset($options) && isset($options['url'])) {
|
||||
$image->setPage($options['url']);
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
}
|
||||
82
module/Common/src/Middleware/LocaleMiddleware.php
Normal file
82
module/Common/src/Middleware/LocaleMiddleware.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Middleware;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\Stratigility\MiddlewareInterface;
|
||||
|
||||
class LocaleMiddleware implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* @var Translator
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* LocaleMiddleware constructor.
|
||||
* @param Translator $translator
|
||||
*
|
||||
* @Inject({"translator"})
|
||||
*/
|
||||
public function __construct(Translator $translator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming request and/or response.
|
||||
*
|
||||
* Accepts a server-side request and a response instance, and does
|
||||
* something with them.
|
||||
*
|
||||
* If the response is not complete and/or further processing would not
|
||||
* interfere with the work done in the middleware, or if the middleware
|
||||
* wants to delegate to another process, it can use the `$out` callable
|
||||
* if present.
|
||||
*
|
||||
* If the middleware does not return a value, execution of the current
|
||||
* request is considered complete, and the response instance provided will
|
||||
* be considered the response to return.
|
||||
*
|
||||
* Alternately, the middleware may return a response instance.
|
||||
*
|
||||
* Often, middleware will `return $out();`, with the assumption that a
|
||||
* later middleware will return a response.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Response $response
|
||||
* @param null|callable $out
|
||||
* @return null|Response
|
||||
*/
|
||||
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||
{
|
||||
if (! $request->hasHeader('Accept-Language')) {
|
||||
return $out($request, $response);
|
||||
}
|
||||
|
||||
$locale = $request->getHeaderLine('Accept-Language');
|
||||
$this->translator->setLocale($this->normalizeLocale($locale));
|
||||
return $out($request, $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $locale
|
||||
* @return string
|
||||
*/
|
||||
protected function normalizeLocale($locale)
|
||||
{
|
||||
$parts = explode('_', $locale);
|
||||
if (count($parts) > 1) {
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
$parts = explode('-', $locale);
|
||||
if (count($parts) > 1) {
|
||||
return $parts[0];
|
||||
}
|
||||
|
||||
return $locale;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php
|
||||
namespace Acelaya\UrlShortener\Paginator\Adapter;
|
||||
namespace Shlinkio\Shlink\Common\Paginator\Adapter;
|
||||
|
||||
use Acelaya\UrlShortener\Repository\PaginableRepositoryInterface;
|
||||
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;
|
||||
use Zend\Paginator\Adapter\AdapterInterface;
|
||||
|
||||
class PaginableRepositoryAdapter implements AdapterInterface
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Acelaya\UrlShortener\Paginator\Util;
|
||||
namespace Shlinkio\Shlink\Common\Paginator\Util;
|
||||
|
||||
use Zend\Paginator\Paginator;
|
||||
use Zend\Stdlib\ArrayUtils;
|
||||
@@ -1,5 +1,5 @@
|
||||
<?php
|
||||
namespace Acelaya\UrlShortener\Repository;
|
||||
namespace Shlinkio\Shlink\Common\Repository;
|
||||
|
||||
interface PaginableRepositoryInterface
|
||||
{
|
||||
35
module/Common/src/Response/QrCodeResponse.php
Normal file
35
module/Common/src/Response/QrCodeResponse.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Response;
|
||||
|
||||
use Endroid\QrCode\QrCode;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Zend\Diactoros\Response;
|
||||
use Zend\Diactoros\Stream;
|
||||
|
||||
class QrCodeResponse extends Response
|
||||
{
|
||||
use Response\InjectContentTypeTrait;
|
||||
|
||||
public function __construct(QrCode $qrCode, $status = 200, array $headers = [])
|
||||
{
|
||||
parent::__construct(
|
||||
$this->createBody($qrCode),
|
||||
$status,
|
||||
$this->injectContentType($qrCode->getContentType(), $headers)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the message body.
|
||||
*
|
||||
* @param QrCode $qrCode
|
||||
* @return StreamInterface
|
||||
*/
|
||||
private function createBody(QrCode $qrCode)
|
||||
{
|
||||
$body = new Stream('php://temp', 'wb+');
|
||||
$body->write($qrCode->get());
|
||||
$body->rewind();
|
||||
return $body;
|
||||
}
|
||||
}
|
||||
42
module/Common/src/Service/IpLocationResolver.php
Normal file
42
module/Common/src/Service/IpLocationResolver.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Service;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||
|
||||
class IpLocationResolver implements IpLocationResolverInterface
|
||||
{
|
||||
const SERVICE_PATTERN = 'http://freegeoip.net/json/%s';
|
||||
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
private $httpClient;
|
||||
|
||||
/**
|
||||
* IpLocationResolver constructor.
|
||||
* @param Client $httpClient
|
||||
*
|
||||
* @Inject({"httpClient"})
|
||||
*/
|
||||
public function __construct(Client $httpClient)
|
||||
{
|
||||
$this->httpClient = $httpClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $ipAddress
|
||||
* @return array
|
||||
*/
|
||||
public function resolveIpLocation($ipAddress)
|
||||
{
|
||||
try {
|
||||
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
|
||||
return json_decode($response->getBody(), true);
|
||||
} catch (GuzzleException $e) {
|
||||
throw WrongIpException::fromIpAddress($ipAddress, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
module/Common/src/Service/IpLocationResolverInterface.php
Normal file
11
module/Common/src/Service/IpLocationResolverInterface.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Service;
|
||||
|
||||
interface IpLocationResolverInterface
|
||||
{
|
||||
/**
|
||||
* @param $ipAddress
|
||||
* @return array
|
||||
*/
|
||||
public function resolveIpLocation($ipAddress);
|
||||
}
|
||||
70
module/Common/src/Service/PreviewGenerator.php
Normal file
70
module/Common/src/Service/PreviewGenerator.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Service;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||
use mikehaertl\wkhtmlto\Image;
|
||||
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||
use Shlinkio\Shlink\Common\Image\ImageBuilder;
|
||||
use Shlinkio\Shlink\Common\Image\ImageBuilderInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class PreviewGenerator implements PreviewGeneratorInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $location;
|
||||
/**
|
||||
* @var ImageBuilderInterface
|
||||
*/
|
||||
private $imageBuilder;
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
private $filesystem;
|
||||
|
||||
/**
|
||||
* PreviewGenerator constructor.
|
||||
* @param ImageBuilderInterface $imageBuilder
|
||||
* @param Filesystem $filesystem
|
||||
* @param string $location
|
||||
*
|
||||
* @Inject({ImageBuilder::class, Filesystem::class, "config.preview_generation.files_location"})
|
||||
*/
|
||||
public function __construct(ImageBuilderInterface $imageBuilder, Filesystem $filesystem, $location)
|
||||
{
|
||||
$this->location = $location;
|
||||
$this->imageBuilder = $imageBuilder;
|
||||
$this->filesystem = $filesystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and stores preview for provided website and returns the path to the image file
|
||||
*
|
||||
* @param string $url
|
||||
* @return string
|
||||
* @throws PreviewGenerationException
|
||||
*/
|
||||
public function generatePreview($url)
|
||||
{
|
||||
/** @var Image $image */
|
||||
$image = $this->imageBuilder->build(Image::class, ['url' => $url]);
|
||||
|
||||
// If the file already exists, return its path
|
||||
$cacheId = sprintf('preview_%s.%s', urlencode($url), $image->type);
|
||||
$path = $this->location . '/' . $cacheId;
|
||||
if ($this->filesystem->exists($path)) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
// Save and check if an error occurred
|
||||
$image->saveAs($path);
|
||||
$error = $image->getError();
|
||||
if (! empty($error)) {
|
||||
throw PreviewGenerationException::fromImageError($error);
|
||||
}
|
||||
|
||||
// Cache the path and return it
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user