mirror of
https://github.com/shlinkio/shlink.git
synced 2024-12-27 01:11:39 -06:00
commit
9c8eef12ba
@ -1,7 +1,8 @@
|
||||
<?php
|
||||
namespace PHPSTORM_META;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
/**
|
||||
* PhpStorm Container Interop code completion
|
||||
@ -16,4 +17,7 @@ $STATIC_METHOD_TYPES = [
|
||||
ContainerInterface::get('') => [
|
||||
'' == '@',
|
||||
],
|
||||
ServiceManager::build('') => [
|
||||
'' == '@',
|
||||
],
|
||||
];
|
||||
|
18
CHANGELOG.md
18
CHANGELOG.md
@ -1,5 +1,23 @@
|
||||
## CHANGELOG
|
||||
|
||||
### 1.5.0
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
* [95: Add tags CRUD to CLI](https://github.com/shlinkio/shlink/issues/95)
|
||||
* [59: Add tags CRUD to REST](https://github.com/shlinkio/shlink/issues/59)
|
||||
* [66: Allow to import certain information from older app directory when updating](https://github.com/shlinkio/shlink/issues/66)
|
||||
|
||||
**Tasks**
|
||||
|
||||
* [96: Add namespace to functions](https://github.com/shlinkio/shlink/issues/96)
|
||||
* [76: Add response examples to swagger docs](https://github.com/shlinkio/shlink/issues/76)
|
||||
* [93: Improve cross domain management by using the ImplicitOptionsMiddleware](https://github.com/shlinkio/shlink/issues/93)
|
||||
|
||||
**Bugs**
|
||||
|
||||
* [92: Fix formatted dates, using an ISO compliant format](https://github.com/shlinkio/shlink/issues/92)
|
||||
|
||||
### 1.4.0
|
||||
|
||||
**Enhancements:**
|
||||
|
5
bin/cli
5
bin/cli
@ -5,7 +5,4 @@ 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();
|
||||
$container->get(CliApp::class)->run();
|
||||
|
17
bin/install
17
bin/install
@ -1,14 +1,19 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Zend\Config\Writer\PhpArray;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new Application();
|
||||
$app->add(new InstallCommand(new PhpArray()));
|
||||
$app->setDefaultCommand('shlink:install');
|
||||
$app->run();
|
||||
$container = new ServiceManager(['factories' => [
|
||||
Application::class => InstallApplicationFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
QuestionHelper::class => InvokableFactory::class,
|
||||
]]);
|
||||
$container->build(Application::class)->run();
|
||||
|
17
bin/update
17
bin/update
@ -1,14 +1,19 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
use Shlinkio\Shlink\CLI\Command\Install\UpdateCommand;
|
||||
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Zend\Config\Writer\PhpArray;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
chdir(dirname(__DIR__));
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
$app = new Application();
|
||||
$app->add(new UpdateCommand(new PhpArray()));
|
||||
$app->setDefaultCommand('shlink:install');
|
||||
$app->run();
|
||||
$container = new ServiceManager(['factories' => [
|
||||
Application::class => InstallApplicationFactory::class,
|
||||
Filesystem::class => InvokableFactory::class,
|
||||
QuestionHelper::class => InvokableFactory::class,
|
||||
]]);
|
||||
$container->build(Application::class, ['isUpdate' => true])->run();
|
||||
|
6
build.sh
6
build.sh
@ -8,7 +8,7 @@ if [ "$#" -ne 1 ]; then
|
||||
fi
|
||||
|
||||
version=$1
|
||||
builtcontent=$(readlink -f '../shlink_build_tmp')
|
||||
builtcontent=$(readlink -f "../shlink_${version}_dist")
|
||||
projectdir=$(pwd)
|
||||
|
||||
# Copy project content to temp dir
|
||||
@ -31,6 +31,8 @@ rm build.sh
|
||||
rm CHANGELOG.md
|
||||
rm composer.*
|
||||
rm LICENSE
|
||||
rm indocker
|
||||
rm docker-compose.yml
|
||||
rm php*
|
||||
rm README.md
|
||||
rm -r build
|
||||
@ -42,5 +44,5 @@ 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 .
|
||||
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip "../shlink_${version}_dist"
|
||||
rm -rf "${builtcontent}"
|
||||
|
@ -42,7 +42,8 @@
|
||||
"roave/security-advisories": "dev-master",
|
||||
"filp/whoops": "^2.0",
|
||||
"symfony/var-dumper": "^3.0",
|
||||
"vlucas/phpdotenv": "^2.2"
|
||||
"vlucas/phpdotenv": "^2.2",
|
||||
"zendframework/zend-expressive-tooling": "^0.4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
@ -1,10 +1,12 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\Common;
|
||||
|
||||
return [
|
||||
|
||||
'app_options' => [
|
||||
'name' => 'Shlink',
|
||||
'version' => '1.2.0',
|
||||
'secret_key' => env('SECRET_KEY'),
|
||||
'secret_key' => Common\env('SECRET_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
|
||||
use Zend\Expressive;
|
||||
use Zend\Expressive\Container;
|
||||
use Zend\Expressive\Middleware;
|
||||
use Zend\Expressive\Router;
|
||||
use Zend\Expressive\Template;
|
||||
use Zend\Expressive\Twig;
|
||||
@ -15,6 +17,7 @@ return [
|
||||
\Twig_Environment::class => Twig\TwigEnvironmentFactory::class,
|
||||
Router\RouterInterface::class => Router\FastRouteRouterFactory::class,
|
||||
ErrorHandler::class => Container\ErrorHandlerFactory::class,
|
||||
Middleware\ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\Common;
|
||||
|
||||
return [
|
||||
|
||||
'entity_manager' => [
|
||||
@ -6,9 +8,9 @@ return [
|
||||
'proxies_dir' => 'data/proxies',
|
||||
],
|
||||
'connection' => [
|
||||
'user' => env('DB_USER'),
|
||||
'password' => env('DB_PASSWORD'),
|
||||
'dbname' => env('DB_NAME', 'shlink'),
|
||||
'user' => Common\env('DB_USER'),
|
||||
'password' => Common\env('DB_PASSWORD'),
|
||||
'dbname' => Common\env('DB_NAME', 'shlink'),
|
||||
'charset' => 'utf8',
|
||||
],
|
||||
],
|
||||
|
@ -4,7 +4,7 @@ use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware;
|
||||
use Zend\Expressive\Container\ApplicationFactory;
|
||||
use Zend\Expressive;
|
||||
use Zend\Stratigility\Middleware\ErrorHandler;
|
||||
|
||||
return [
|
||||
@ -27,7 +27,7 @@ return [
|
||||
|
||||
'routing' => [
|
||||
'middleware' => [
|
||||
ApplicationFactory::ROUTING_MIDDLEWARE,
|
||||
Expressive\Application::ROUTING_MIDDLEWARE,
|
||||
],
|
||||
'priority' => 10,
|
||||
],
|
||||
@ -36,6 +36,7 @@ return [
|
||||
'path' => '/rest',
|
||||
'middleware' => [
|
||||
CrossDomainMiddleware::class,
|
||||
Expressive\Middleware\ImplicitOptionsMiddleware::class,
|
||||
BodyParserMiddleware::class,
|
||||
CheckAuthenticationMiddleware::class,
|
||||
],
|
||||
@ -44,7 +45,7 @@ return [
|
||||
|
||||
'post-routing' => [
|
||||
'middleware' => [
|
||||
ApplicationFactory::DISPATCH_MIDDLEWARE,
|
||||
Expressive\Application::DISPATCH_MIDDLEWARE,
|
||||
],
|
||||
'priority' => 1,
|
||||
],
|
||||
|
@ -1,8 +1,10 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\Common;
|
||||
|
||||
return [
|
||||
|
||||
'translator' => [
|
||||
'locale' => env('DEFAULT_LOCALE', 'en'),
|
||||
'locale' => Common\env('DEFAULT_LOCALE', 'en'),
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -1,14 +1,15 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\Common;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => env('SHORTENED_URL_SCHEMA', 'http'),
|
||||
'hostname' => env('SHORTENED_URL_HOSTNAME'),
|
||||
'schema' => Common\env('SHORTENED_URL_SCHEMA', 'http'),
|
||||
'hostname' => Common\env('SHORTENED_URL_HOSTNAME'),
|
||||
],
|
||||
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
|
||||
'shortcode_chars' => Common\env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
|
||||
],
|
||||
|
||||
];
|
||||
|
@ -25,6 +25,11 @@
|
||||
"description": "The authentication token that needs to be sent in the Authorization header"
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -68,6 +68,44 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"shortUrls": {
|
||||
"data": [
|
||||
{
|
||||
"shortCode": "12C18",
|
||||
"originalUrl": "https://store.steampowered.com",
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsCount": 328,
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
]
|
||||
},
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
"originalUrl": "https://shlink.io",
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsCount": 1029,
|
||||
"tags": [
|
||||
"shlink"
|
||||
]
|
||||
},
|
||||
{
|
||||
"shortCode": "123bA",
|
||||
"originalUrl": "https://www.google.com",
|
||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||
"visitsCount": 25,
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
"currentPage": 5,
|
||||
"pagesCount": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
|
@ -28,6 +28,11 @@
|
||||
"description": "The original long URL behind the short code."
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"longUrl": "https://shlink.io"
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -41,6 +41,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"tags": [
|
||||
"games",
|
||||
"tech"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
|
@ -36,6 +36,32 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"visits": {
|
||||
"data": [
|
||||
{
|
||||
"referer": "https://twitter.com",
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"remoteAddr": "10.20.30.40",
|
||||
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0"
|
||||
},
|
||||
{
|
||||
"referer": "https://t.co",
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"remoteAddr": "11.22.33.44",
|
||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"
|
||||
},
|
||||
{
|
||||
"referer": null,
|
||||
"date": "2015-08-20T05:05:03+04:00",
|
||||
"remoteAddr": "110.220.5.6",
|
||||
"userAgent": "some_web_crawler/1.4"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
|
193
docs/swagger/paths/v1_tags.json
Normal file
193
docs/swagger/paths/v1_tags.json
Normal file
@ -0,0 +1,193 @@
|
||||
{
|
||||
"get": {
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "List existing tags",
|
||||
"description": "Returns the list of all tags used in any short URL, ordered by name",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of tags",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"tags": {
|
||||
"data": [
|
||||
"games",
|
||||
"php",
|
||||
"shlink",
|
||||
"tech"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"post": {
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Create tags",
|
||||
"description": "Provided a list of tags, creates all that do not yet exist",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
},
|
||||
{
|
||||
"name": "tags[]",
|
||||
"in": "formData",
|
||||
"description": "The list of tag names to create",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The list of tags",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"examples": {
|
||||
"application/json": {
|
||||
"tags": {
|
||||
"data": [
|
||||
"games",
|
||||
"php",
|
||||
"shlink",
|
||||
"tech"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"put": {
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Rename tag",
|
||||
"description": "Renames one existing tag",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
},
|
||||
{
|
||||
"name": "oldName",
|
||||
"in": "formData",
|
||||
"description": "Current name of the tag",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"name": "newName",
|
||||
"in": "formData",
|
||||
"description": "New name of the tag",
|
||||
"required": true,
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "The tag has been properly renamed"
|
||||
},
|
||||
"400": {
|
||||
"description": "You have not provided either the oldName or the newName params.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "There's no tag found with the name provided in oldName param.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"delete": {
|
||||
"tags": [
|
||||
"Tags"
|
||||
],
|
||||
"summary": "Delete tags",
|
||||
"description": "Deletes provided list of tags",
|
||||
"parameters": [
|
||||
{
|
||||
"$ref": "../parameters/Authorization.json"
|
||||
},
|
||||
{
|
||||
"name": "tags[]",
|
||||
"in": "query",
|
||||
"description": "The names of the tags to delete",
|
||||
"required": true,
|
||||
"type": "array"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Tags properly deleted"
|
||||
},
|
||||
"500": {
|
||||
"description": "Unexpected error.",
|
||||
"schema": {
|
||||
"$ref": "../definitions/Error.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@
|
||||
"info": {
|
||||
"title": "Shlink",
|
||||
"description": "Shlink, the self-hosted URL shortener",
|
||||
"version": "1.2.0"
|
||||
"version": "1.0"
|
||||
},
|
||||
"schemes": [
|
||||
"http",
|
||||
@ -22,17 +22,23 @@
|
||||
"/v1/authenticate": {
|
||||
"$ref": "paths/v1_authenticate.json"
|
||||
},
|
||||
|
||||
"/v1/short-codes": {
|
||||
"$ref": "paths/v1_short-codes.json"
|
||||
},
|
||||
"/v1/short-codes/{shortCode}": {
|
||||
"$ref": "paths/v1_short-codes_{shortCode}.json"
|
||||
},
|
||||
"/v1/short-codes/{shortCode}/visits": {
|
||||
"$ref": "paths/v1_short-codes_{shortCode}_visits.json"
|
||||
},
|
||||
"/v1/short-codes/{shortCode}/tags": {
|
||||
"$ref": "paths/v1_short-codes_{shortCode}_tags.json"
|
||||
},
|
||||
|
||||
"/v1/tags": {
|
||||
"$ref": "paths/v1_tags.json"
|
||||
},
|
||||
|
||||
"/v1/short-codes/{shortCode}/visits": {
|
||||
"$ref": "paths/v1_short-codes_{shortCode}_visits.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,11 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\CLI\Command;
|
||||
use Shlinkio\Shlink\Common;
|
||||
|
||||
return [
|
||||
|
||||
'cli' => [
|
||||
'locale' => env('CLI_LOCALE', 'en'),
|
||||
'locale' => Common\env('CLI_LOCALE', 'en'),
|
||||
'commands' => [
|
||||
Command\Shortcode\GenerateShortcodeCommand::class,
|
||||
Command\Shortcode\ResolveUrlCommand::class,
|
||||
@ -17,6 +18,10 @@ return [
|
||||
Command\Api\GenerateKeyCommand::class,
|
||||
Command\Api\DisableKeyCommand::class,
|
||||
Command\Api\ListKeysCommand::class,
|
||||
Command\Tag\ListTagsCommand::class,
|
||||
Command\Tag\CreateTagCommand::class,
|
||||
Command\Tag\RenameTagCommand::class,
|
||||
Command\Tag\DeleteTagsCommand::class,
|
||||
]
|
||||
],
|
||||
|
||||
|
@ -21,6 +21,10 @@ return [
|
||||
Command\Api\GenerateKeyCommand::class => AnnotatedFactory::class,
|
||||
Command\Api\DisableKeyCommand::class => AnnotatedFactory::class,
|
||||
Command\Api\ListKeysCommand::class => AnnotatedFactory::class,
|
||||
Command\Tag\ListTagsCommand::class => AnnotatedFactory::class,
|
||||
Command\Tag\CreateTagCommand::class => AnnotatedFactory::class,
|
||||
Command\Tag\RenameTagCommand::class => AnnotatedFactory::class,
|
||||
Command\Tag\DeleteTagsCommand::class => AnnotatedFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
|
Binary file not shown.
@ -1,15 +1,15 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2016-10-22 23:12+0200\n"
|
||||
"PO-Revision-Date: 2016-10-22 23:13+0200\n"
|
||||
"POT-Creation-Date: 2017-07-16 09:35+0200\n"
|
||||
"PO-Revision-Date: 2017-07-16 09:39+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-Generator: Poedit 2.0.1\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
@ -223,6 +223,53 @@ msgstr "URL larga:"
|
||||
msgid "Provided short code \"%s\" has an invalid format."
|
||||
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
|
||||
|
||||
msgid "Creates one or more tags."
|
||||
msgstr "Crea una o más etiquetas."
|
||||
|
||||
msgid "The name of the tags to create"
|
||||
msgstr "El nombre de las etiquetas a crear"
|
||||
|
||||
msgid "You have to provide at least one tag name"
|
||||
msgstr "Debes proporcionar al menos un nombre de etiqueta"
|
||||
|
||||
msgid "Created tags"
|
||||
msgstr "Etiquetas creadas"
|
||||
|
||||
msgid "Deletes one or more tags."
|
||||
msgstr "Elimina una o más etiquetas."
|
||||
|
||||
msgid "The name of the tags to delete"
|
||||
msgstr "El nombre de las etiquetas a eliminar"
|
||||
|
||||
msgid "Deleted tags"
|
||||
msgstr "Etiquetas eliminadas"
|
||||
|
||||
msgid "Lists existing tags."
|
||||
msgstr "Lista las etiquetas existentes."
|
||||
|
||||
#, fuzzy
|
||||
msgid "Name"
|
||||
msgstr "Nombre"
|
||||
|
||||
msgid "No tags yet"
|
||||
msgstr "Aún no hay etiquetas"
|
||||
|
||||
msgid "Renames one existing tag."
|
||||
msgstr "Renombra una etiqueta existente."
|
||||
|
||||
msgid "Current name of the tag."
|
||||
msgstr "Nombre actual de la etiqueta."
|
||||
|
||||
msgid "New name of the tag."
|
||||
msgstr "Nuevo nombre de la etiqueta."
|
||||
|
||||
msgid "Tag properly renamed."
|
||||
msgstr "Etiqueta correctamente renombrada."
|
||||
|
||||
#, php-format
|
||||
msgid "A tag with name \"%s\" was not found"
|
||||
msgstr "Una etiqueta con nombre \"%s\" no ha sido encontrada"
|
||||
|
||||
msgid "Processes visits where location is not set yet"
|
||||
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
|
||||
|
||||
|
@ -32,7 +32,7 @@ class DisableKeyCommand extends Command
|
||||
{
|
||||
$this->apiKeyService = $apiKeyService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct(null);
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function configure()
|
||||
|
@ -84,7 +84,7 @@ class ListKeysCommand extends Command
|
||||
$rowData[] = $this->{$formatMethod}($this->getEnabledSymbol($row));
|
||||
}
|
||||
|
||||
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ISO8601) : '-';
|
||||
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ATOM) : '-';
|
||||
$table->addRow($rowData);
|
||||
}
|
||||
|
||||
|
@ -1,27 +1,25 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Install;
|
||||
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManagerInterface;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
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\ConfirmationQuestion;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
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'];
|
||||
const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
|
||||
|
||||
/**
|
||||
* @var InputInterface
|
||||
@ -43,22 +41,44 @@ class InstallCommand extends Command
|
||||
* @var WriterInterface
|
||||
*/
|
||||
private $configWriter;
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
private $filesystem;
|
||||
/**
|
||||
* @var ConfigCustomizerPluginManagerInterface
|
||||
*/
|
||||
private $configCustomizers;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $isUpdate;
|
||||
|
||||
/**
|
||||
* InstallCommand constructor.
|
||||
* @param WriterInterface $configWriter
|
||||
* @param callable|null $databaseCreationLogic
|
||||
* @param Filesystem $filesystem
|
||||
* @param bool $isUpdate
|
||||
* @throws LogicException
|
||||
*/
|
||||
public function __construct(WriterInterface $configWriter)
|
||||
{
|
||||
parent::__construct(null);
|
||||
public function __construct(
|
||||
WriterInterface $configWriter,
|
||||
Filesystem $filesystem,
|
||||
ConfigCustomizerPluginManagerInterface $configCustomizers,
|
||||
$isUpdate = false
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->configWriter = $configWriter;
|
||||
$this->isUpdate = $isUpdate;
|
||||
$this->filesystem = $filesystem;
|
||||
$this->configCustomizers = $configCustomizers;
|
||||
}
|
||||
|
||||
public function configure()
|
||||
{
|
||||
$this->setName('shlink:install')
|
||||
->setDescription('Installs Shlink');
|
||||
$this
|
||||
->setName('shlink:install')
|
||||
->setDescription('Installs or updates Shlink');
|
||||
}
|
||||
|
||||
public function execute(InputInterface $input, OutputInterface $output)
|
||||
@ -67,40 +87,58 @@ class InstallCommand extends Command
|
||||
$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.',
|
||||
'This will guide you through the installation process.',
|
||||
]);
|
||||
|
||||
// Check if a cached config file exists and drop it if so
|
||||
if (file_exists('data/cache/app_config.php')) {
|
||||
if ($this->filesystem->exists('data/cache/app_config.php')) {
|
||||
$output->write('Deleting old cached config...');
|
||||
if (unlink('data/cache/app_config.php')) {
|
||||
try {
|
||||
$this->filesystem->remove('data/cache/app_config.php');
|
||||
$output->writeln(' <info>Success</info>');
|
||||
} else {
|
||||
} catch (IOException $e) {
|
||||
$output->writeln(
|
||||
' <error>Failed!</error> You will have to manually delete the data/cache/app_config.php file to get'
|
||||
. ' new config applied.'
|
||||
);
|
||||
if ($output->isVerbose()) {
|
||||
$this->getApplication()->renderException($e, $output);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If running update command, ask the user to import previous config
|
||||
$config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig();
|
||||
|
||||
// Ask for custom config params
|
||||
$params['DATABASE'] = $this->askDatabase();
|
||||
$params['URL_SHORTENER'] = $this->askUrlShortener();
|
||||
$params['LANGUAGE'] = $this->askLanguage();
|
||||
$params['APP'] = $this->askApplication();
|
||||
foreach ([
|
||||
Plugin\DatabaseConfigCustomizerPlugin::class,
|
||||
Plugin\UrlShortenerConfigCustomizerPlugin::class,
|
||||
Plugin\LanguageConfigCustomizerPlugin::class,
|
||||
Plugin\ApplicationConfigCustomizerPlugin::class,
|
||||
] as $pluginName) {
|
||||
/** @var Plugin\ConfigCustomizerPluginInterface $configCustomizer */
|
||||
$configCustomizer = $this->configCustomizers->get($pluginName);
|
||||
$configCustomizer->process($input, $output, $config);
|
||||
}
|
||||
|
||||
// Generate config params files
|
||||
$config = $this->buildAppConfig($params);
|
||||
$this->configWriter->toFile('config/params/generated_config.php', $config, false);
|
||||
$this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
|
||||
$output->writeln(['<info>Custom configuration properly generated!</info>', '']);
|
||||
|
||||
// Generate database
|
||||
if (! $this->createDatabase()) {
|
||||
return;
|
||||
// If current command is not update, generate database
|
||||
if (! $this->isUpdate) {
|
||||
$this->output->writeln('Initializing database...');
|
||||
if (! $this->runCommand(
|
||||
'php vendor/bin/doctrine.php orm:schema-tool:create',
|
||||
'Error generating database.'
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Run database migrations
|
||||
@ -116,105 +154,47 @@ class InstallCommand extends Command
|
||||
}
|
||||
}
|
||||
|
||||
protected function askDatabase()
|
||||
/**
|
||||
* @return CustomizableAppConfig
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
private function importConfig()
|
||||
{
|
||||
$params = [];
|
||||
$this->printTitle('DATABASE');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
// 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
|
||||
// Ask the user if he/she wants to import an older configuration
|
||||
$importConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
|
||||
'<question>Do you want to import previous configuration? (Y/n):</question> '
|
||||
));
|
||||
$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');
|
||||
$params['HOST'] = $this->ask('Database host', 'localhost');
|
||||
$params['PORT'] = $this->ask('Database port', $this->getDefaultDbPort($params['DRIVER']));
|
||||
if (! $importConfig) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
// Ask the user for the older shlink path
|
||||
$keepAsking = true;
|
||||
do {
|
||||
$config->setImportedInstallationPath($this->ask(
|
||||
'Previous shlink installation path from which to import config'
|
||||
));
|
||||
$configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
|
||||
$configExists = $this->filesystem->exists($configFile);
|
||||
|
||||
protected function getDefaultDbPort($driver)
|
||||
{
|
||||
return $driver === 'pdo_mysql' ? '3306' : '5432';
|
||||
}
|
||||
if (! $configExists) {
|
||||
$keepAsking = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
|
||||
'Provided path does not seem to be a valid shlink root path. '
|
||||
. '<question>Do you want to try another path? (Y/n):</question> '
|
||||
));
|
||||
}
|
||||
} while (! $configExists && $keepAsking);
|
||||
|
||||
protected function askUrlShortener()
|
||||
{
|
||||
$this->printTitle('URL SHORTENER');
|
||||
// If after some retries the user has chosen not to test another path, return
|
||||
if (! $configExists) {
|
||||
return $config;
|
||||
}
|
||||
|
||||
// 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>',
|
||||
]);
|
||||
// Read the config file
|
||||
$config->exchangeArray(include $configFile);
|
||||
return $config;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -222,10 +202,11 @@ class InstallCommand extends Command
|
||||
* @param string|null $default
|
||||
* @param bool $allowEmpty
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function ask($text, $default = null, $allowEmpty = false)
|
||||
private function ask($text, $default = null, $allowEmpty = false)
|
||||
{
|
||||
if (isset($default)) {
|
||||
if ($default !== null) {
|
||||
$text .= ' (defaults to ' . $default . ')';
|
||||
}
|
||||
do {
|
||||
@ -236,87 +217,31 @@ class InstallCommand extends Command
|
||||
if (empty($value) && ! $allowEmpty) {
|
||||
$this->output->writeln('<error>Value can\'t be empty</error>');
|
||||
}
|
||||
} while (empty($value) && empty($default) && ! $allowEmpty);
|
||||
} while (empty($value) && $default === null && ! $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'];
|
||||
$config['entity_manager']['connection']['host'] = $params['DATABASE']['HOST'];
|
||||
$config['entity_manager']['connection']['port'] = $params['DATABASE']['PORT'];
|
||||
|
||||
if ($params['DATABASE']['DRIVER'] === 'pdo_mysql') {
|
||||
$config['entity_manager']['connection']['driverOptions'] = [
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
private 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.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($this->output->isVerbose()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->output->writeln(
|
||||
' <error>' . $errorMessage . '</error> Run this command with -vvv to see specific error info.'
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +0,0 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Install;
|
||||
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class UpdateCommand extends InstallCommand
|
||||
{
|
||||
public function createDatabase()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
69
module/CLI/src/Command/Tag/CreateTagCommand.php
Normal file
69
module/CLI/src/Command/Tag/CreateTagCommand.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagService;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
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\Translator;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class CreateTagCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* CreateTagCommand constructor.
|
||||
* @param TagServiceInterface $tagService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @DI\Inject({TagService::class, Translator::class})
|
||||
*/
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('tag:create')
|
||||
->setDescription($this->translator->translate('Creates one or more tags.'))
|
||||
->addOption(
|
||||
'name',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
$this->translator->translate('The name of the tags to create')
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$tagNames = $input->getOption('name');
|
||||
if (empty($tagNames)) {
|
||||
$output->writeln(sprintf(
|
||||
'<comment>%s</comment>',
|
||||
$this->translator->translate('You have to provide at least one tag name')
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tagService->createTags($tagNames);
|
||||
$output->writeln($this->translator->translate('Created tags') . sprintf(': ["<info>%s</info>"]', implode(
|
||||
'</info>", "<info>',
|
||||
$tagNames
|
||||
)));
|
||||
}
|
||||
}
|
69
module/CLI/src/Command/Tag/DeleteTagsCommand.php
Normal file
69
module/CLI/src/Command/Tag/DeleteTagsCommand.php
Normal file
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagService;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
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\Translator;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class DeleteTagsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* ListTagsCommand constructor.
|
||||
* @param TagServiceInterface $tagService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @DI\Inject({TagService::class, Translator::class})
|
||||
*/
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('tag:delete')
|
||||
->setDescription($this->translator->translate('Deletes one or more tags.'))
|
||||
->addOption(
|
||||
'name',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
$this->translator->translate('The name of the tags to delete')
|
||||
);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$tagNames = $input->getOption('name');
|
||||
if (empty($tagNames)) {
|
||||
$output->writeln(sprintf(
|
||||
'<comment>%s</comment>',
|
||||
$this->translator->translate('You have to provide at least one tag name')
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
$this->tagService->deleteTags($tagNames);
|
||||
$output->writeln($this->translator->translate('Deleted tags') . sprintf(': ["<info>%s</info>"]', implode(
|
||||
'</info>", "<info>',
|
||||
$tagNames
|
||||
)));
|
||||
}
|
||||
}
|
67
module/CLI/src/Command/Tag/ListTagsCommand.php
Normal file
67
module/CLI/src/Command/Tag/ListTagsCommand.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagService;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\Table;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class ListTagsCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* ListTagsCommand constructor.
|
||||
* @param TagServiceInterface $tagService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @DI\Inject({TagService::class, Translator::class})
|
||||
*/
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('tag:list')
|
||||
->setDescription($this->translator->translate('Lists existing tags.'));
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$table = new Table($output);
|
||||
$table->setHeaders([$this->translator->translate('Name')])
|
||||
->setRows($this->getTagsRows());
|
||||
|
||||
$table->render();
|
||||
}
|
||||
|
||||
private function getTagsRows()
|
||||
{
|
||||
$tags = $this->tagService->listTags();
|
||||
if (empty($tags)) {
|
||||
return [[$this->translator->translate('No tags yet')]];
|
||||
}
|
||||
|
||||
return array_map(function (Tag $tag) {
|
||||
return [$tag->getName()];
|
||||
}, $tags);
|
||||
}
|
||||
}
|
63
module/CLI/src/Command/Tag/RenameTagCommand.php
Normal file
63
module/CLI/src/Command/Tag/RenameTagCommand.php
Normal file
@ -0,0 +1,63 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagService;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
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\Translator;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class RenameTagCommand extends Command
|
||||
{
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* RenameTagCommand constructor.
|
||||
* @param TagServiceInterface $tagService
|
||||
* @param TranslatorInterface $translator
|
||||
*
|
||||
* @DI\Inject({TagService::class, Translator::class})
|
||||
*/
|
||||
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
|
||||
{
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('tag:rename')
|
||||
->setDescription($this->translator->translate('Renames one existing tag.'))
|
||||
->addArgument('oldName', InputArgument::REQUIRED, $this->translator->translate('Current name of the tag.'))
|
||||
->addArgument('newName', InputArgument::REQUIRED, $this->translator->translate('New name of the tag.'));
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$oldName = $input->getArgument('oldName');
|
||||
$newName = $input->getArgument('newName');
|
||||
|
||||
try {
|
||||
$this->tagService->renameTag($oldName, $newName);
|
||||
$output->writeln(sprintf('<info>%s</info>', $this->translator->translate('Tag properly renamed.')));
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
$output->writeln('<error>' . sprintf($this->translator->translate(
|
||||
'A tag with name "%s" was not found'
|
||||
), $oldName) . '</error>');
|
||||
}
|
||||
}
|
||||
}
|
55
module/CLI/src/Factory/InstallApplicationFactory.php
Normal file
55
module/CLI/src/Factory/InstallApplicationFactory.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Factory;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManager;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\Factory\DefaultConfigCustomizerPluginFactory;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Exception\LogicException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\Config\Writer\PhpArray;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class InstallApplicationFactory implements FactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @throws LogicException
|
||||
* @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)
|
||||
{
|
||||
$isUpdate = $options !== null && isset($options['isUpdate']) ? (bool) $options['isUpdate'] : false;
|
||||
|
||||
$app = new Application();
|
||||
$command = new InstallCommand(
|
||||
new PhpArray(),
|
||||
$container->get(Filesystem::class),
|
||||
new ConfigCustomizerPluginManager($container, ['factories' => [
|
||||
Plugin\DatabaseConfigCustomizerPlugin::class => AnnotatedFactory::class,
|
||||
Plugin\UrlShortenerConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
|
||||
Plugin\LanguageConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
|
||||
Plugin\ApplicationConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
|
||||
]]),
|
||||
$isUpdate
|
||||
);
|
||||
$app->add($command);
|
||||
$app->setDefaultCommand($command->getName());
|
||||
|
||||
return $app;
|
||||
}
|
||||
}
|
10
module/CLI/src/Install/ConfigCustomizerPluginManager.php
Normal file
10
module/CLI/src/Install/ConfigCustomizerPluginManager.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Install;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerPluginInterface;
|
||||
use Zend\ServiceManager\AbstractPluginManager;
|
||||
|
||||
class ConfigCustomizerPluginManager extends AbstractPluginManager implements ConfigCustomizerPluginManagerInterface
|
||||
{
|
||||
protected $instanceOf = ConfigCustomizerPluginInterface::class;
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Install;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
interface ConfigCustomizerPluginManagerInterface extends ContainerInterface
|
||||
{
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\Question;
|
||||
|
||||
abstract class AbstractConfigCustomizerPlugin implements ConfigCustomizerPluginInterface
|
||||
{
|
||||
/**
|
||||
* @var QuestionHelper
|
||||
*/
|
||||
protected $questionHelper;
|
||||
|
||||
public function __construct(QuestionHelper $questionHelper)
|
||||
{
|
||||
$this->questionHelper = $questionHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param string $text
|
||||
* @param string|null $default
|
||||
* @param bool $allowEmpty
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function ask(InputInterface $input, OutputInterface $output, $text, $default = null, $allowEmpty = false)
|
||||
{
|
||||
if ($default !== null) {
|
||||
$text .= ' (defaults to ' . $default . ')';
|
||||
}
|
||||
do {
|
||||
$value = $this->questionHelper->ask($input, $output, new Question(
|
||||
'<question>' . $text . ':</question> ',
|
||||
$default
|
||||
));
|
||||
if (empty($value) && ! $allowEmpty) {
|
||||
$output->writeln('<error>Value can\'t be empty</error>');
|
||||
}
|
||||
} while (empty($value) && $default === null && ! $allowEmpty);
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param OutputInterface $output
|
||||
* @param string $text
|
||||
*/
|
||||
protected function printTitle(OutputInterface $output, $text)
|
||||
{
|
||||
$text = trim($text);
|
||||
$length = strlen($text) + 4;
|
||||
$header = str_repeat('*', $length);
|
||||
|
||||
$output->writeln([
|
||||
'',
|
||||
'<info>' . $header . '</info>',
|
||||
'<info>* ' . strtoupper($text) . ' *</info>',
|
||||
'<info>' . $header . '</info>',
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
class ApplicationConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
|
||||
{
|
||||
use StringUtilsTrait;
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
* @throws \Symfony\Component\Console\Exception\RuntimeException
|
||||
*/
|
||||
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$this->printTitle($output, 'APPLICATION');
|
||||
|
||||
if ($appConfig->hasApp() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
|
||||
'<question>Do you want to keep imported application config? (Y/n):</question> '
|
||||
))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$appConfig->setApp([
|
||||
'SECRET' => $this->ask(
|
||||
$input,
|
||||
$output,
|
||||
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
|
||||
null,
|
||||
true
|
||||
) ?: $this->generateRandomString(32),
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
interface ConfigCustomizerPluginInterface
|
||||
{
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
*/
|
||||
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig);
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
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\ConfirmationQuestion;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class DatabaseConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
|
||||
{
|
||||
const DATABASE_DRIVERS = [
|
||||
'MySQL' => 'pdo_mysql',
|
||||
'PostgreSQL' => 'pdo_pgsql',
|
||||
'SQLite' => 'pdo_sqlite',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var Filesystem
|
||||
*/
|
||||
private $filesystem;
|
||||
|
||||
/**
|
||||
* DatabaseConfigCustomizerPlugin constructor.
|
||||
* @param QuestionHelper $questionHelper
|
||||
* @param Filesystem $filesystem
|
||||
*
|
||||
* @DI\Inject({QuestionHelper::class, Filesystem::class})
|
||||
*/
|
||||
public function __construct(QuestionHelper $questionHelper, Filesystem $filesystem)
|
||||
{
|
||||
parent::__construct($questionHelper);
|
||||
$this->filesystem = $filesystem;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
* @throws IOException
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$this->printTitle($output, 'DATABASE');
|
||||
|
||||
if ($appConfig->hasDatabase() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
|
||||
'<question>Do you want to keep imported database config? (Y/n):</question> '
|
||||
))) {
|
||||
// If the user selected to keep DB config and is configured to use sqlite, copy DB file
|
||||
if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) {
|
||||
try {
|
||||
$this->filesystem->copy(
|
||||
$appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH,
|
||||
CustomizableAppConfig::SQLITE_DB_PATH
|
||||
);
|
||||
} catch (IOException $e) {
|
||||
$output->writeln('<error>It wasn\'t possible to import the SQLite database</error>');
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Select database type
|
||||
$params = [];
|
||||
$databases = array_keys(self::DATABASE_DRIVERS);
|
||||
$dbType = $this->questionHelper->ask($input, $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($input, $output, 'Database name', 'shlink');
|
||||
$params['USER'] = $this->ask($input, $output, 'Database username');
|
||||
$params['PASSWORD'] = $this->ask($input, $output, 'Database password');
|
||||
$params['HOST'] = $this->ask($input, $output, 'Database host', 'localhost');
|
||||
$params['PORT'] = $this->ask($input, $output, 'Database port', $this->getDefaultDbPort($params['DRIVER']));
|
||||
}
|
||||
|
||||
$appConfig->setDatabase($params);
|
||||
}
|
||||
|
||||
private function getDefaultDbPort($driver)
|
||||
{
|
||||
return $driver === 'pdo_mysql' ? '3306' : '5432';
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin\Factory;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class DefaultConfigCustomizerPluginFactory 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)
|
||||
{
|
||||
return new $requestedName($container->get(QuestionHelper::class));
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
class LanguageConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
|
||||
{
|
||||
const SUPPORTED_LANGUAGES = ['en', 'es'];
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$this->printTitle($output, 'LANGUAGE');
|
||||
|
||||
if ($appConfig->hasLanguage() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
|
||||
'<question>Do you want to keep imported language? (Y/n):</question> '
|
||||
))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$appConfig->setLanguage([
|
||||
'DEFAULT' => $this->questionHelper->ask($input, $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($input, $output, new ChoiceQuestion(
|
||||
'<question>Select default language for CLI executions (defaults to '
|
||||
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
|
||||
self::SUPPORTED_LANGUAGES,
|
||||
0
|
||||
)),
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
class UrlShortenerConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
|
||||
{
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @param CustomizableAppConfig $appConfig
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
|
||||
{
|
||||
$this->printTitle($output, 'URL SHORTENER');
|
||||
|
||||
if ($appConfig->hasUrlShortener() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
|
||||
'<question>Do you want to keep imported URL shortener config? (Y/n):</question> '
|
||||
))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ask for URL shortener params
|
||||
$appConfig->setUrlShortener([
|
||||
'SCHEMA' => $this->questionHelper->ask($input, $output, new ChoiceQuestion(
|
||||
'<question>Select schema for generated short URLs (defaults to http):</question>',
|
||||
['http', 'https'],
|
||||
0
|
||||
)),
|
||||
'HOSTNAME' => $this->ask($input, $output, 'Hostname for generated URLs'),
|
||||
'CHARS' => $this->ask(
|
||||
$input,
|
||||
$output,
|
||||
'Character set for generated short codes (leave empty to autogenerate one)',
|
||||
null,
|
||||
true
|
||||
) ?: str_shuffle(UrlShortener::DEFAULT_CHARS)
|
||||
]);
|
||||
}
|
||||
}
|
265
module/CLI/src/Model/CustomizableAppConfig.php
Normal file
265
module/CLI/src/Model/CustomizableAppConfig.php
Normal file
@ -0,0 +1,265 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\CLI\Model;
|
||||
|
||||
use Zend\Stdlib\ArraySerializableInterface;
|
||||
|
||||
final class CustomizableAppConfig implements ArraySerializableInterface
|
||||
{
|
||||
const SQLITE_DB_PATH = 'data/database.sqlite';
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $database;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $urlShortener;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $language;
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $app;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $importedInstallationPath;
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getDatabase()
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $database
|
||||
* @return $this
|
||||
*/
|
||||
public function setDatabase(array $database)
|
||||
{
|
||||
$this->database = $database;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasDatabase()
|
||||
{
|
||||
return ! empty($this->database);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getUrlShortener()
|
||||
{
|
||||
return $this->urlShortener;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $urlShortener
|
||||
* @return $this
|
||||
*/
|
||||
public function setUrlShortener(array $urlShortener)
|
||||
{
|
||||
$this->urlShortener = $urlShortener;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasUrlShortener()
|
||||
{
|
||||
return ! empty($this->urlShortener);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getLanguage()
|
||||
{
|
||||
return $this->language;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $language
|
||||
* @return $this
|
||||
*/
|
||||
public function setLanguage(array $language)
|
||||
{
|
||||
$this->language = $language;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasLanguage()
|
||||
{
|
||||
return ! empty($this->language);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getApp()
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $app
|
||||
* @return $this
|
||||
*/
|
||||
public function setApp(array $app)
|
||||
{
|
||||
$this->app = $app;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasApp()
|
||||
{
|
||||
return ! empty($this->app);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getImportedInstallationPath()
|
||||
{
|
||||
return $this->importedInstallationPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $importedInstallationPath
|
||||
* @return $this|self
|
||||
*/
|
||||
public function setImportedInstallationPath($importedInstallationPath)
|
||||
{
|
||||
$this->importedInstallationPath = $importedInstallationPath;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasImportedInstallationPath()
|
||||
{
|
||||
return $this->importedInstallationPath !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange internal values from provided array
|
||||
*
|
||||
* @param array $array
|
||||
* @return void
|
||||
*/
|
||||
public function exchangeArray(array $array)
|
||||
{
|
||||
if (isset($array['app_options'], $array['app_options']['secret_key'])) {
|
||||
$this->setApp([
|
||||
'SECRET' => $array['app_options']['secret_key'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (isset($array['entity_manager'], $array['entity_manager']['connection'])) {
|
||||
$this->deserializeDatabase($array['entity_manager']['connection']);
|
||||
}
|
||||
|
||||
if (isset($array['translator'], $array['translator']['locale'], $array['cli'], $array['cli']['locale'])) {
|
||||
$this->setLanguage([
|
||||
'DEFAULT' => $array['translator']['locale'],
|
||||
'CLI' => $array['cli']['locale'],
|
||||
]);
|
||||
}
|
||||
|
||||
if (isset($array['url_shortener'])) {
|
||||
$urlShortener = $array['url_shortener'];
|
||||
$this->setUrlShortener([
|
||||
'SCHEMA' => $urlShortener['domain']['schema'],
|
||||
'HOSTNAME' => $urlShortener['domain']['hostname'],
|
||||
'CHARS' => $urlShortener['shortcode_chars'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function deserializeDatabase(array $conn)
|
||||
{
|
||||
if (! isset($conn['driver'])) {
|
||||
return;
|
||||
}
|
||||
$driver = $conn['driver'];
|
||||
|
||||
$params = ['DRIVER' => $driver];
|
||||
if ($driver !== 'pdo_sqlite') {
|
||||
$params['USER'] = $conn['user'];
|
||||
$params['PASSWORD'] = $conn['password'];
|
||||
$params['NAME'] = $conn['dbname'];
|
||||
$params['HOST'] = $conn['host'];
|
||||
$params['PORT'] = $conn['port'];
|
||||
}
|
||||
|
||||
$this->setDatabase($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array representation of the object
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getArrayCopy()
|
||||
{
|
||||
$config = [
|
||||
'app_options' => [
|
||||
'secret_key' => $this->app['SECRET'],
|
||||
],
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => $this->database['DRIVER'],
|
||||
],
|
||||
],
|
||||
'translator' => [
|
||||
'locale' => $this->language['DEFAULT'],
|
||||
],
|
||||
'cli' => [
|
||||
'locale' => $this->language['CLI'],
|
||||
],
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => $this->urlShortener['SCHEMA'],
|
||||
'hostname' => $this->urlShortener['HOSTNAME'],
|
||||
],
|
||||
'shortcode_chars' => $this->urlShortener['CHARS'],
|
||||
],
|
||||
];
|
||||
|
||||
// Build dynamic database config based on selected driver
|
||||
if ($this->database['DRIVER'] === 'pdo_sqlite') {
|
||||
$config['entity_manager']['connection']['path'] = self::SQLITE_DB_PATH;
|
||||
} else {
|
||||
$config['entity_manager']['connection']['user'] = $this->database['USER'];
|
||||
$config['entity_manager']['connection']['password'] = $this->database['PASSWORD'];
|
||||
$config['entity_manager']['connection']['dbname'] = $this->database['NAME'];
|
||||
$config['entity_manager']['connection']['host'] = $this->database['HOST'];
|
||||
$config['entity_manager']['connection']['port'] = $this->database['PORT'];
|
||||
|
||||
if ($this->database['DRIVER'] === 'pdo_mysql') {
|
||||
$config['entity_manager']['connection']['driverOptions'] = [
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $config;
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
<?php
|
||||
return [];
|
@ -3,16 +3,25 @@ namespace ShlinkioTest\Shlink\CLI\Command\Install;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManagerInterface;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerPluginInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Filesystem\Exception\IOException;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Symfony\Component\Process\Process;
|
||||
use Zend\Config\Writer\WriterInterface;
|
||||
|
||||
class InstallCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var InstallCommand
|
||||
*/
|
||||
protected $command;
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
@ -21,6 +30,10 @@ class InstallCommandTest extends TestCase
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $configWriter;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $filesystem;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
@ -31,81 +44,96 @@ class InstallCommandTest extends TestCase
|
||||
$processHelper->setHelperSet(Argument::any())->willReturn(null);
|
||||
$processHelper->run(Argument::cetera())->willReturn($processMock->reveal());
|
||||
|
||||
$this->filesystem = $this->prophesize(Filesystem::class);
|
||||
$this->filesystem->exists(Argument::cetera())->willReturn(false);
|
||||
|
||||
$this->configWriter = $this->prophesize(WriterInterface::class);
|
||||
|
||||
$configCustomizer = $this->prophesize(ConfigCustomizerPluginInterface::class);
|
||||
$configCustomizers = $this->prophesize(ConfigCustomizerPluginManagerInterface::class);
|
||||
$configCustomizers->get(Argument::cetera())->willReturn($configCustomizer->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', 'rb+', false);
|
||||
fwrite($stream, <<<CLI_INPUT
|
||||
|
||||
shlink_db
|
||||
alejandro
|
||||
1234
|
||||
|
||||
|
||||
0
|
||||
doma.in
|
||||
abc123BCA
|
||||
|
||||
1
|
||||
my_secret
|
||||
CLI_INPUT
|
||||
$this->command = new InstallCommand(
|
||||
$this->configWriter->reveal(),
|
||||
$this->filesystem->reveal(),
|
||||
$configCustomizers->reveal()
|
||||
);
|
||||
rewind($stream);
|
||||
$app->add($this->command);
|
||||
|
||||
return $stream;
|
||||
$this->commandTester = new CommandTester($this->command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function inputIsProperlyParsed()
|
||||
public function generatedConfigIsProperlyPersisted()
|
||||
{
|
||||
$this->configWriter->toFile(Argument::any(), [
|
||||
'app_options' => [
|
||||
'secret_key' => 'my_secret',
|
||||
],
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
'driver' => 'pdo_mysql',
|
||||
'dbname' => 'shlink_db',
|
||||
'user' => 'alejandro',
|
||||
'password' => '1234',
|
||||
'host' => 'localhost',
|
||||
'port' => '3306',
|
||||
'driverOptions' => [
|
||||
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||
]
|
||||
],
|
||||
],
|
||||
'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',
|
||||
$this->configWriter->toFile(Argument::any(), Argument::type('array'), false)->shouldBeCalledTimes(1);
|
||||
$this->commandTester->execute([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function cachedConfigIsDeletedIfExists()
|
||||
{
|
||||
/** @var MethodProphecy $appConfigExists */
|
||||
$appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn(true);
|
||||
/** @var MethodProphecy $appConfigRemove */
|
||||
$appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willReturn(null);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$appConfigExists->shouldHaveBeenCalledTimes(1);
|
||||
$appConfigRemove->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function exceptionWhileDeletingCachedConfigCancelsProcess()
|
||||
{
|
||||
/** @var MethodProphecy $appConfigExists */
|
||||
$appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn(true);
|
||||
/** @var MethodProphecy $appConfigRemove */
|
||||
$appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willThrow(IOException::class);
|
||||
/** @var MethodProphecy $configToFile */
|
||||
$configToFile = $this->configWriter->toFile(Argument::cetera())->willReturn(true);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$appConfigExists->shouldHaveBeenCalledTimes(1);
|
||||
$appConfigRemove->shouldHaveBeenCalledTimes(1);
|
||||
$configToFile->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function whenCommandIsUpdatePreviousConfigCanBeImported()
|
||||
{
|
||||
$ref = new \ReflectionObject($this->command);
|
||||
$prop = $ref->getProperty('isUpdate');
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue($this->command, true);
|
||||
|
||||
/** @var MethodProphecy $importedConfigExists */
|
||||
$importedConfigExists = $this->filesystem->exists(
|
||||
__DIR__ . '/../../../test-resources/' . InstallCommand::GENERATED_CONFIG_PATH
|
||||
)->willReturn(true);
|
||||
|
||||
$this->commandTester->setInputs([
|
||||
'',
|
||||
'/foo/bar/wrong_previous_shlink',
|
||||
'',
|
||||
__DIR__ . '/../../../test-resources',
|
||||
]);
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$importedConfigExists->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
||||
|
67
module/CLI/test/Command/Tag/CreateTagCommandTest.php
Normal file
67
module/CLI/test/Command/Tag/CreateTagCommandTest.php
Normal file
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class CreateTagCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CreateTagCommand
|
||||
*/
|
||||
private $command;
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new CreateTagCommand($this->tagService->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function errorIsReturnedWhenNoTagsAreProvided()
|
||||
{
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertContains('You have to provide at least one tag name', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function serviceIsInvokedOnSuccess()
|
||||
{
|
||||
$tagNames = ['foo', 'bar'];
|
||||
/** @var MethodProphecy $createTags */
|
||||
$createTags = $this->tagService->createTags($tagNames)->willReturn([]);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'--name' => $tagNames,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(sprintf('Created tags: ["%s"]', implode('", "', $tagNames)), $output);
|
||||
$createTags->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
68
module/CLI/test/Command/Tag/DeleteTagsCommandTest.php
Normal file
68
module/CLI/test/Command/Tag/DeleteTagsCommandTest.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class DeleteTagsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DeleteTagsCommand
|
||||
*/
|
||||
private $command;
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new DeleteTagsCommand($this->tagService->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function errorIsReturnedWhenNoTagsAreProvided()
|
||||
{
|
||||
$this->commandTester->execute([]);
|
||||
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$this->assertContains('You have to provide at least one tag name', $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function serviceIsInvokedOnSuccess()
|
||||
{
|
||||
$tagNames = ['foo', 'bar'];
|
||||
/** @var MethodProphecy $deleteTags */
|
||||
$deleteTags = $this->tagService->deleteTags($tagNames)->will(function () {
|
||||
});
|
||||
|
||||
$this->commandTester->execute([
|
||||
'--name' => $tagNames,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains(sprintf('Deleted tags: ["%s"]', implode('", "', $tagNames)), $output);
|
||||
$deleteTags->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
73
module/CLI/test/Command/Tag/ListTagsCommandTest.php
Normal file
73
module/CLI/test/Command/Tag/ListTagsCommandTest.php
Normal file
@ -0,0 +1,73 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class ListTagsCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ListTagsCommand
|
||||
*/
|
||||
private $command;
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new ListTagsCommand($this->tagService->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function noTagsPrintsEmptyMessage()
|
||||
{
|
||||
/** @var MethodProphecy $listTags */
|
||||
$listTags = $this->tagService->listTags()->willReturn([]);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('No tags yet', $output);
|
||||
$listTags->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function listOfTagsIsPrinted()
|
||||
{
|
||||
/** @var MethodProphecy $listTags */
|
||||
$listTags = $this->tagService->listTags()->willReturn([
|
||||
(new Tag())->setName('foo'),
|
||||
(new Tag())->setName('bar'),
|
||||
]);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('foo', $output);
|
||||
$this->assertContains('bar', $output);
|
||||
$listTags->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
80
module/CLI/test/Command/Tag/RenameTagCommandTest.php
Normal file
80
module/CLI/test/Command/Tag/RenameTagCommandTest.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class RenameTagCommandTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var RenameTagCommand
|
||||
*/
|
||||
private $command;
|
||||
/**
|
||||
* @var CommandTester
|
||||
*/
|
||||
private $commandTester;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
|
||||
$command = new RenameTagCommand($this->tagService->reveal(), Translator::factory([]));
|
||||
$app = new Application();
|
||||
$app->add($command);
|
||||
|
||||
$this->commandTester = new CommandTester($command);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function errorIsPrintedIfExceptionIsThrown()
|
||||
{
|
||||
$oldName = 'foo';
|
||||
$newName = 'bar';
|
||||
/** @var MethodProphecy $renameTag */
|
||||
$renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(EntityDoesNotExistException::class);
|
||||
|
||||
$this->commandTester->execute([
|
||||
'oldName' => $oldName,
|
||||
'newName' => $newName,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('A tag with name "foo" was not found', $output);
|
||||
$renameTag->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function successIsPrintedIfNoErrorOccurs()
|
||||
{
|
||||
$oldName = 'foo';
|
||||
$newName = 'bar';
|
||||
/** @var MethodProphecy $renameTag */
|
||||
$renameTag = $this->tagService->renameTag($oldName, $newName)->willReturn(new Tag());
|
||||
|
||||
$this->commandTester->execute([
|
||||
'oldName' => $oldName,
|
||||
'newName' => $newName,
|
||||
]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
$this->assertContains('Tag properly renamed', $output);
|
||||
$renameTag->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
33
module/CLI/test/Factory/InstallApplicationFactoryTest.php
Normal file
33
module/CLI/test/Factory/InstallApplicationFactoryTest.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Factory;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class InstallApplicationFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var InstallApplicationFactory
|
||||
*/
|
||||
private $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->factory = new InstallApplicationFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function serviceIsCreated()
|
||||
{
|
||||
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
|
||||
Filesystem::class => $this->prophesize(Filesystem::class)->reveal(),
|
||||
]]), '');
|
||||
|
||||
$this->assertInstanceOf(Application::class, $instance);
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ApplicationConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
class ApplicationConfigCustomizerPluginTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ApplicationConfigCustomizerPlugin
|
||||
*/
|
||||
private $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $questionHelper;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->questionHelper = $this->prophesize(QuestionHelper::class);
|
||||
$this->plugin = new ApplicationConfigCustomizerPlugin($this->questionHelper->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
/** @var MethodProphecy $askSecret */
|
||||
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('the_secret');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertTrue($config->hasApp());
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'the_secret',
|
||||
], $config->getApp());
|
||||
$askSecret->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
|
||||
$last = array_pop($args);
|
||||
return $last instanceof ConfirmationQuestion ? false : 'the_new_secret';
|
||||
});
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'the_new_secret',
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setApp([
|
||||
'SECRET' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SECRET' => 'foo',
|
||||
], $config->getApp());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
@ -0,0 +1,152 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
|
||||
class DatabaseConfigCustomizerPluginTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DatabaseConfigCustomizerPlugin
|
||||
*/
|
||||
private $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $questionHelper;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $filesystem;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->questionHelper = $this->prophesize(QuestionHelper::class);
|
||||
$this->filesystem = $this->prophesize(Filesystem::class);
|
||||
|
||||
$this->plugin = new DatabaseConfigCustomizerPlugin(
|
||||
$this->questionHelper->reveal(),
|
||||
$this->filesystem->reveal()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
/** @var MethodProphecy $askSecret */
|
||||
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('MySQL');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertTrue($config->hasDatabase());
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_mysql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
], $config->getDatabase());
|
||||
$askSecret->shouldHaveBeenCalledTimes(6);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
|
||||
$last = array_pop($args);
|
||||
return $last instanceof ConfirmationQuestion ? false : 'MySQL';
|
||||
});
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setDatabase([
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_mysql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
], $config->getDatabase());
|
||||
$ask->shouldHaveBeenCalledTimes(7);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setDatabase([
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_pgsql',
|
||||
'NAME' => 'MySQL',
|
||||
'USER' => 'MySQL',
|
||||
'PASSWORD' => 'MySQL',
|
||||
'HOST' => 'MySQL',
|
||||
'PORT' => 'MySQL',
|
||||
], $config->getDatabase());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function sqliteDatabaseIsImportedWhenRequested()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
|
||||
/** @var MethodProphecy $copy */
|
||||
$copy = $this->filesystem->copy(Argument::cetera())->willReturn(null);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setDatabase([
|
||||
'DRIVER' => 'pdo_sqlite',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DRIVER' => 'pdo_sqlite',
|
||||
], $config->getDatabase());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
$copy->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin\Factory;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\ApplicationConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\Factory\DefaultConfigCustomizerPluginFactory;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\LanguageConfigCustomizerPlugin;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class DefaultConfigCustomizerPluginFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DefaultConfigCustomizerPluginFactory
|
||||
*/
|
||||
protected $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->factory = new DefaultConfigCustomizerPluginFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function createsProperService()
|
||||
{
|
||||
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
|
||||
QuestionHelper::class => $this->prophesize(QuestionHelper::class)->reveal(),
|
||||
]]), ApplicationConfigCustomizerPlugin::class);
|
||||
$this->assertInstanceOf(ApplicationConfigCustomizerPlugin::class, $instance);
|
||||
|
||||
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
|
||||
QuestionHelper::class => $this->prophesize(QuestionHelper::class)->reveal(),
|
||||
]]), LanguageConfigCustomizerPlugin::class);
|
||||
$this->assertInstanceOf(LanguageConfigCustomizerPlugin::class, $instance);
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\LanguageConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
class LanguageConfigCustomizerPluginTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var LanguageConfigCustomizerPlugin
|
||||
*/
|
||||
protected $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
protected $questionHelper;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->questionHelper = $this->prophesize(QuestionHelper::class);
|
||||
$this->plugin = new LanguageConfigCustomizerPlugin($this->questionHelper->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
/** @var MethodProphecy $askSecret */
|
||||
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('en');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertTrue($config->hasLanguage());
|
||||
$this->assertEquals([
|
||||
'DEFAULT' => 'en',
|
||||
'CLI' => 'en',
|
||||
], $config->getLanguage());
|
||||
$askSecret->shouldHaveBeenCalledTimes(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
|
||||
$last = array_pop($args);
|
||||
return $last instanceof ConfirmationQuestion ? false : 'es';
|
||||
});
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setLanguage([
|
||||
'DEFAULT' => 'en',
|
||||
'CLI' => 'en',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DEFAULT' => 'es',
|
||||
'CLI' => 'es',
|
||||
], $config->getLanguage());
|
||||
$ask->shouldHaveBeenCalledTimes(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setLanguage([
|
||||
'DEFAULT' => 'es',
|
||||
'CLI' => 'es',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'DEFAULT' => 'es',
|
||||
'CLI' => 'es',
|
||||
], $config->getLanguage());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\CLI\Install\Plugin\UrlShortenerConfigCustomizerPlugin;
|
||||
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
|
||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Question\ConfirmationQuestion;
|
||||
|
||||
class UrlShortenerConfigCustomizerPluginTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var UrlShortenerConfigCustomizerPlugin
|
||||
*/
|
||||
private $plugin;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $questionHelper;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->questionHelper = $this->prophesize(QuestionHelper::class);
|
||||
$this->plugin = new UrlShortenerConfigCustomizerPlugin($this->questionHelper->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function configIsRequestedToTheUser()
|
||||
{
|
||||
/** @var MethodProphecy $askSecret */
|
||||
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('something');
|
||||
$config = new CustomizableAppConfig();
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertTrue($config->hasUrlShortener());
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'something',
|
||||
'HOSTNAME' => 'something',
|
||||
'CHARS' => 'something',
|
||||
], $config->getUrlShortener());
|
||||
$askSecret->shouldHaveBeenCalledTimes(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function overwriteIsRequestedIfValueIsAlreadySet()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
|
||||
$last = array_pop($args);
|
||||
return $last instanceof ConfirmationQuestion ? false : 'foo';
|
||||
});
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setUrlShortener([
|
||||
'SCHEMA' => 'bar',
|
||||
'HOSTNAME' => 'bar',
|
||||
'CHARS' => 'bar',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'foo',
|
||||
], $config->getUrlShortener());
|
||||
$ask->shouldHaveBeenCalledTimes(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function existingValueIsKeptIfRequested()
|
||||
{
|
||||
/** @var MethodProphecy $ask */
|
||||
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
|
||||
|
||||
$config = new CustomizableAppConfig();
|
||||
$config->setUrlShortener([
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'foo',
|
||||
]);
|
||||
|
||||
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
|
||||
|
||||
$this->assertEquals([
|
||||
'SCHEMA' => 'foo',
|
||||
'HOSTNAME' => 'foo',
|
||||
'CHARS' => 'foo',
|
||||
], $config->getUrlShortener());
|
||||
$ask->shouldHaveBeenCalledTimes(1);
|
||||
}
|
||||
}
|
@ -1,36 +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;
|
||||
}
|
||||
namespace Shlinkio\Shlink\Common;
|
||||
|
||||
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);
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace Shlinkio\Shlink\Common\Factory;
|
||||
use Doctrine\Common\Cache;
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Shlinkio\Shlink\Common;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
@ -48,15 +49,14 @@ class CacheFactory implements FactoryInterface
|
||||
{
|
||||
// Try to get the adapter from config
|
||||
$config = $container->get('config');
|
||||
if (isset($config['cache'])
|
||||
&& isset($config['cache']['adapter'])
|
||||
if (isset($config['cache'], $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();
|
||||
return Common\env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,7 +80,7 @@ class CacheFactory implements FactoryInterface
|
||||
if (! isset($server['host'])) {
|
||||
continue;
|
||||
}
|
||||
$port = isset($server['port']) ? intval($server['port']) : 11211;
|
||||
$port = isset($server['port']) ? (int) $server['port'] : 11211;
|
||||
|
||||
$memcached->addServer($server['host'], $port);
|
||||
}
|
||||
|
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Common\Factory;
|
||||
|
||||
use Interop\Container\ContainerInterface;
|
||||
use Interop\Container\Exception\ContainerException;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware;
|
||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||
|
||||
class EmptyResponseImplicitOptionsMiddlewareFactory 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)
|
||||
{
|
||||
return new ImplicitOptionsMiddleware(new EmptyResponse());
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ trait StringUtilsTrait
|
||||
$charactersLength = strlen($characters);
|
||||
$randomString = '';
|
||||
for ($i = 0; $i < $length; $i++) {
|
||||
$randomString .= $characters[rand(0, $charactersLength - 1)];
|
||||
$randomString .= $characters[mt_rand(0, $charactersLength - 1)];
|
||||
}
|
||||
|
||||
return $randomString;
|
||||
|
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\Common\Factory;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Expressive\Middleware\ImplicitOptionsMiddleware;
|
||||
use Zend\ServiceManager\ServiceManager;
|
||||
|
||||
class EmptyResponseImplicitOptionsMiddlewareFactoryTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var EmptyResponseImplicitOptionsMiddlewareFactory
|
||||
*/
|
||||
protected $factory;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->factory = new EmptyResponseImplicitOptionsMiddlewareFactory();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function serviceIsCreated()
|
||||
{
|
||||
$instance = $this->factory->__invoke(new ServiceManager(), '');
|
||||
$this->assertInstanceOf(ImplicitOptionsMiddleware::class, $instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function responsePrototypeIsEmptyResponse()
|
||||
{
|
||||
$instance = $this->factory->__invoke(new ServiceManager(), '');
|
||||
|
||||
$ref = new \ReflectionObject($instance);
|
||||
$prop = $ref->getProperty('response');
|
||||
$prop->setAccessible(true);
|
||||
$this->assertInstanceOf(EmptyResponse::class, $prop->getValue($instance));
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ return [
|
||||
Service\VisitsTracker::class => AnnotatedFactory::class,
|
||||
Service\ShortUrlService::class => AnnotatedFactory::class,
|
||||
Service\VisitService::class => AnnotatedFactory::class,
|
||||
Service\Tag\TagService::class => AnnotatedFactory::class,
|
||||
|
||||
// Middleware
|
||||
Action\RedirectAction::class => AnnotatedFactory::class,
|
||||
|
@ -176,7 +176,7 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
|
||||
return [
|
||||
'shortCode' => $this->shortCode,
|
||||
'originalUrl' => $this->originalUrl,
|
||||
'dateCreated' => isset($this->dateCreated) ? $this->dateCreated->format(\DateTime::ISO8601) : null,
|
||||
'dateCreated' => isset($this->dateCreated) ? $this->dateCreated->format(\DateTime::ATOM) : null,
|
||||
'visitsCount' => count($this->visits),
|
||||
'tags' => $this->tags->toArray(),
|
||||
];
|
||||
|
@ -3,13 +3,14 @@ namespace Shlinkio\Shlink\Core\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Repository\TagRepository;
|
||||
|
||||
/**
|
||||
* Class Tag
|
||||
* @author
|
||||
* @link
|
||||
*
|
||||
* @ORM\Entity()
|
||||
* @ORM\Entity(repositoryClass=TagRepository::class)
|
||||
* @ORM\Table(name="tags")
|
||||
*/
|
||||
class Tag extends AbstractEntity implements \JsonSerializable
|
||||
@ -20,6 +21,11 @@ class Tag extends AbstractEntity implements \JsonSerializable
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
public function __construct($name = null)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
@ -171,7 +171,7 @@ class Visit extends AbstractEntity implements \JsonSerializable
|
||||
{
|
||||
return [
|
||||
'referer' => $this->referer,
|
||||
'date' => isset($this->date) ? $this->date->format(\DateTime::ISO8601) : null,
|
||||
'date' => isset($this->date) ? $this->date->format(\DateTime::ATOM) : null,
|
||||
'remoteAddr' => $this->remoteAddr,
|
||||
'userAgent' => $this->userAgent,
|
||||
'visitLocation' => $this->visitLocation,
|
||||
|
26
module/Core/src/Exception/EntityDoesNotExistException.php
Normal file
26
module/Core/src/Exception/EntityDoesNotExistException.php
Normal file
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Exception;
|
||||
|
||||
use Shlinkio\Shlink\Common\Exception\ExceptionInterface;
|
||||
|
||||
class EntityDoesNotExistException extends \RuntimeException implements ExceptionInterface
|
||||
{
|
||||
public static function createFromEntityAndConditions($entityName, array $conditions)
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Entity of type %s with params [%s] does not exist',
|
||||
$entityName,
|
||||
static::serializeParams($conditions)
|
||||
));
|
||||
}
|
||||
|
||||
private static function serializeParams(array $params)
|
||||
{
|
||||
$result = [];
|
||||
foreach ($params as $key => $value) {
|
||||
$result[] = sprintf('"%s" => "%s"', $key, $value);
|
||||
}
|
||||
|
||||
return implode(', ', $result);
|
||||
}
|
||||
}
|
27
module/Core/src/Repository/TagRepository.php
Normal file
27
module/Core/src/Repository/TagRepository.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Repository;
|
||||
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
|
||||
class TagRepository extends EntityRepository implements TagRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* Delete the tags identified by provided names
|
||||
*
|
||||
* @param array $names
|
||||
* @return int The number of affected entries
|
||||
*/
|
||||
public function deleteByName(array $names)
|
||||
{
|
||||
if (empty($names)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
$qb->delete(Tag::class, 't')
|
||||
->where($qb->expr()->in('t.name', $names));
|
||||
|
||||
return $qb->getQuery()->execute();
|
||||
}
|
||||
}
|
15
module/Core/src/Repository/TagRepositoryInterface.php
Normal file
15
module/Core/src/Repository/TagRepositoryInterface.php
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Repository;
|
||||
|
||||
use Doctrine\Common\Persistence\ObjectRepository;
|
||||
|
||||
interface TagRepositoryInterface extends ObjectRepository
|
||||
{
|
||||
/**
|
||||
* Delete the tags identified by provided names
|
||||
*
|
||||
* @param array $names
|
||||
* @return int The number of affected entries
|
||||
*/
|
||||
public function deleteByName(array $names);
|
||||
}
|
86
module/Core/src/Service/Tag/TagService.php
Normal file
86
module/Core/src/Service/Tag/TagService.php
Normal file
@ -0,0 +1,86 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Service\Tag;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Repository\TagRepository;
|
||||
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||
|
||||
class TagService implements TagServiceInterface
|
||||
{
|
||||
use TagManagerTrait;
|
||||
|
||||
/**
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
private $em;
|
||||
|
||||
/**
|
||||
* VisitService constructor.
|
||||
* @param EntityManagerInterface $em
|
||||
*
|
||||
* @DI\Inject({"em"})
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tag[]
|
||||
* @throws \UnexpectedValueException
|
||||
*/
|
||||
public function listTags()
|
||||
{
|
||||
return $this->em->getRepository(Tag::class)->findBy([], ['name' => 'ASC']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $tagNames
|
||||
* @return void
|
||||
*/
|
||||
public function deleteTags(array $tagNames)
|
||||
{
|
||||
/** @var TagRepository $repo */
|
||||
$repo = $this->em->getRepository(Tag::class);
|
||||
$repo->deleteByName($tagNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provided a list of tag names, creates all that do not exist yet
|
||||
*
|
||||
* @param string[] $tagNames
|
||||
* @return Collection|Tag[]
|
||||
*/
|
||||
public function createTags(array $tagNames)
|
||||
{
|
||||
$tags = $this->tagNamesToEntities($this->em, $tagNames);
|
||||
$this->em->flush();
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $oldName
|
||||
* @param string $newName
|
||||
* @return Tag
|
||||
* @throws EntityDoesNotExistException
|
||||
*/
|
||||
public function renameTag($oldName, $newName)
|
||||
{
|
||||
$criteria = ['name' => $oldName];
|
||||
/** @var Tag|null $tag */
|
||||
$tag = $this->em->getRepository(Tag::class)->findOneBy($criteria);
|
||||
if ($tag === null) {
|
||||
throw EntityDoesNotExistException::createFromEntityAndConditions(Tag::class, $criteria);
|
||||
}
|
||||
|
||||
$tag->setName($newName);
|
||||
$this->em->flush($tag);
|
||||
|
||||
return $tag;
|
||||
}
|
||||
}
|
36
module/Core/src/Service/Tag/TagServiceInterface.php
Normal file
36
module/Core/src/Service/Tag/TagServiceInterface.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Core\Service\Tag;
|
||||
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
|
||||
interface TagServiceInterface
|
||||
{
|
||||
/**
|
||||
* @return Tag[]
|
||||
*/
|
||||
public function listTags();
|
||||
|
||||
/**
|
||||
* @param string[] $tagNames
|
||||
* @return void
|
||||
*/
|
||||
public function deleteTags(array $tagNames);
|
||||
|
||||
/**
|
||||
* Provided a list of tag names, creates all that do not exist yet
|
||||
*
|
||||
* @param string[] $tagNames
|
||||
* @return Collection|Tag[]
|
||||
*/
|
||||
public function createTags(array $tagNames);
|
||||
|
||||
/**
|
||||
* @param string $oldName
|
||||
* @param string $newName
|
||||
* @return Tag
|
||||
* @throws EntityDoesNotExistException
|
||||
*/
|
||||
public function renameTag($oldName, $newName);
|
||||
}
|
@ -26,7 +26,7 @@ trait TagManagerTrait
|
||||
}
|
||||
|
||||
/**
|
||||
* Tag names are trimmed, lowercased and spaces are replaced by dashes
|
||||
* Tag names are trimmed, lower cased and spaces are replaced by dashes
|
||||
*
|
||||
* @param string $tagName
|
||||
* @return string
|
||||
|
134
module/Core/test/Service/Tag/TagServiceTest.php
Normal file
134
module/Core/test/Service/Tag/TagServiceTest.php
Normal file
@ -0,0 +1,134 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\Core\Service\Tag;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Repository\TagRepository;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class TagServiceTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var TagService
|
||||
*/
|
||||
private $service;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $em;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||
$this->service = new TagService($this->em->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function listTagsDelegatesOnRepository()
|
||||
{
|
||||
$expected = [new Tag(), new Tag()];
|
||||
|
||||
$repo = $this->prophesize(EntityRepository::class);
|
||||
/** @var MethodProphecy $find */
|
||||
$find = $repo->findBy(Argument::cetera())->willReturn($expected);
|
||||
/** @var MethodProphecy $getRepo */
|
||||
$getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal());
|
||||
|
||||
$result = $this->service->listTags();
|
||||
|
||||
$this->assertEquals($expected, $result);
|
||||
$find->shouldHaveBeenCalled();
|
||||
$getRepo->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function deleteTagsDelegatesOnRepository()
|
||||
{
|
||||
$repo = $this->prophesize(TagRepository::class);
|
||||
/** @var MethodProphecy $delete */
|
||||
$delete = $repo->deleteByName(['foo', 'bar'])->willReturn(4);
|
||||
/** @var MethodProphecy $getRepo */
|
||||
$getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal());
|
||||
|
||||
$this->service->deleteTags(['foo', 'bar']);
|
||||
|
||||
$delete->shouldHaveBeenCalled();
|
||||
$getRepo->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function createTagsPersistsEntities()
|
||||
{
|
||||
$repo = $this->prophesize(TagRepository::class);
|
||||
/** @var MethodProphecy $find */
|
||||
$find = $repo->findOneBy(Argument::cetera())->willReturn(new Tag());
|
||||
/** @var MethodProphecy $getRepo */
|
||||
$getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal());
|
||||
/** @var MethodProphecy $persist */
|
||||
$persist = $this->em->persist(Argument::type(Tag::class))->willReturn(null);
|
||||
/** @var MethodProphecy $flush */
|
||||
$flush = $this->em->flush()->willReturn(null);
|
||||
|
||||
$result = $this->service->createTags(['foo', 'bar']);
|
||||
|
||||
$this->assertCount(2, $result);
|
||||
$find->shouldHaveBeenCalled();
|
||||
$getRepo->shouldHaveBeenCalled();
|
||||
$persist->shouldHaveBeenCalledTimes(2);
|
||||
$flush->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function renameInvalidTagThrowsException()
|
||||
{
|
||||
$repo = $this->prophesize(TagRepository::class);
|
||||
/** @var MethodProphecy $find */
|
||||
$find = $repo->findOneBy(Argument::cetera())->willReturn(null);
|
||||
/** @var MethodProphecy $getRepo */
|
||||
$getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal());
|
||||
|
||||
$find->shouldBeCalled();
|
||||
$getRepo->shouldBeCalled();
|
||||
$this->expectException(EntityDoesNotExistException::class);
|
||||
|
||||
$this->service->renameTag('foo', 'bar');
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function renameValidTagChangesItsName()
|
||||
{
|
||||
$expected = new Tag();
|
||||
|
||||
$repo = $this->prophesize(TagRepository::class);
|
||||
/** @var MethodProphecy $find */
|
||||
$find = $repo->findOneBy(Argument::cetera())->willReturn($expected);
|
||||
/** @var MethodProphecy $getRepo */
|
||||
$getRepo = $this->em->getRepository(Tag::class)->willReturn($repo->reveal());
|
||||
/** @var MethodProphecy $flush */
|
||||
$flush = $this->em->flush($expected)->willReturn(null);
|
||||
|
||||
$tag = $this->service->renameTag('foo', 'bar');
|
||||
|
||||
$this->assertSame($expected, $tag);
|
||||
$this->assertEquals('bar', $tag->getName());
|
||||
$find->shouldHaveBeenCalled();
|
||||
$getRepo->shouldHaveBeenCalled();
|
||||
$flush->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
@ -18,7 +18,11 @@ return [
|
||||
Action\ResolveUrlAction::class => AnnotatedFactory::class,
|
||||
Action\GetVisitsAction::class => AnnotatedFactory::class,
|
||||
Action\ListShortcodesAction::class => AnnotatedFactory::class,
|
||||
Action\EditTagsAction::class => AnnotatedFactory::class,
|
||||
Action\EditShortcodeTagsAction::class => AnnotatedFactory::class,
|
||||
Action\Tag\ListTagsAction::class => AnnotatedFactory::class,
|
||||
Action\Tag\DeleteTagsAction::class => AnnotatedFactory::class,
|
||||
Action\Tag\CreateTagsAction::class => AnnotatedFactory::class,
|
||||
Action\Tag\UpdateTagAction::class => AnnotatedFactory::class,
|
||||
|
||||
Middleware\BodyParserMiddleware::class => AnnotatedFactory::class,
|
||||
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
|
||||
|
@ -1,44 +1,75 @@
|
||||
<?php
|
||||
use Shlinkio\Shlink\Rest\Action;
|
||||
use Fig\Http\Message\RequestMethodInterface as RequestMethod;
|
||||
|
||||
return [
|
||||
|
||||
'routes' => [
|
||||
[
|
||||
'name' => 'rest-authenticate',
|
||||
'name' => Action\AuthenticateAction::class,
|
||||
'path' => '/rest/v{version:1}/authenticate',
|
||||
'middleware' => Action\AuthenticateAction::class,
|
||||
'allowed_methods' => ['POST', 'OPTIONS'],
|
||||
'allowed_methods' => [RequestMethod::METHOD_POST],
|
||||
],
|
||||
|
||||
// Short codes
|
||||
[
|
||||
'name' => 'rest-create-shortcode',
|
||||
'name' => Action\CreateShortcodeAction::class,
|
||||
'path' => '/rest/v{version:1}/short-codes',
|
||||
'middleware' => Action\CreateShortcodeAction::class,
|
||||
'allowed_methods' => ['POST', 'OPTIONS'],
|
||||
'allowed_methods' => [RequestMethod::METHOD_POST],
|
||||
],
|
||||
[
|
||||
'name' => 'rest-resolve-url',
|
||||
'name' => Action\ResolveUrlAction::class,
|
||||
'path' => '/rest/v{version:1}/short-codes/{shortCode}',
|
||||
'middleware' => Action\ResolveUrlAction::class,
|
||||
'allowed_methods' => ['GET', 'OPTIONS'],
|
||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
||||
],
|
||||
[
|
||||
'name' => 'rest-list-shortened-url',
|
||||
'name' => Action\ListShortcodesAction::class,
|
||||
'path' => '/rest/v{version:1}/short-codes',
|
||||
'middleware' => Action\ListShortcodesAction::class,
|
||||
'allowed_methods' => ['GET'],
|
||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
||||
],
|
||||
[
|
||||
'name' => 'rest-get-visits',
|
||||
'name' => Action\EditShortcodeTagsAction::class,
|
||||
'path' => '/rest/v{version:1}/short-codes/{shortCode}/tags',
|
||||
'middleware' => Action\EditShortcodeTagsAction::class,
|
||||
'allowed_methods' => [RequestMethod::METHOD_PUT],
|
||||
],
|
||||
|
||||
// Visits
|
||||
[
|
||||
'name' => Action\GetVisitsAction::class,
|
||||
'path' => '/rest/v{version:1}/short-codes/{shortCode}/visits',
|
||||
'middleware' => Action\GetVisitsAction::class,
|
||||
'allowed_methods' => ['GET', 'OPTIONS'],
|
||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
||||
],
|
||||
|
||||
// Tags
|
||||
[
|
||||
'name' => Action\Tag\ListTagsAction::class,
|
||||
'path' => '/rest/v{version:1}/tags',
|
||||
'middleware' => Action\Tag\ListTagsAction::class,
|
||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
||||
],
|
||||
[
|
||||
'name' => 'rest-edit-tags',
|
||||
'path' => '/rest/v{version:1}/short-codes/{shortCode}/tags',
|
||||
'middleware' => Action\EditTagsAction::class,
|
||||
'allowed_methods' => ['PUT', 'OPTIONS'],
|
||||
'name' => Action\Tag\DeleteTagsAction::class,
|
||||
'path' => '/rest/v{version:1}/tags',
|
||||
'middleware' => Action\Tag\DeleteTagsAction::class,
|
||||
'allowed_methods' => [RequestMethod::METHOD_DELETE],
|
||||
],
|
||||
[
|
||||
'name' => Action\Tag\CreateTagsAction::class,
|
||||
'path' => '/rest/v{version:1}/tags',
|
||||
'middleware' => Action\Tag\CreateTagsAction::class,
|
||||
'allowed_methods' => [RequestMethod::METHOD_POST],
|
||||
],
|
||||
[
|
||||
'name' => Action\Tag\UpdateTagAction::class,
|
||||
'path' => '/rest/v{version:1}/tags',
|
||||
'middleware' => Action\Tag\UpdateTagAction::class,
|
||||
'allowed_methods' => [RequestMethod::METHOD_PUT],
|
||||
],
|
||||
],
|
||||
|
||||
|
Binary file not shown.
@ -1,15 +1,15 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2016-08-21 18:17+0200\n"
|
||||
"PO-Revision-Date: 2016-08-21 18:17+0200\n"
|
||||
"POT-Creation-Date: 2017-07-16 09:39+0200\n"
|
||||
"PO-Revision-Date: 2017-07-16 09:40+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-Generator: Poedit 2.0.1\n"
|
||||
"X-Poedit-Basepath: ..\n"
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"X-Poedit-SourceCharset: UTF-8\n"
|
||||
@ -50,6 +50,17 @@ msgstr "El código corto \"%s\" proporcionado no existe"
|
||||
msgid "Provided short code \"%s\" has an invalid format"
|
||||
msgstr "El código corto proporcionado \"%s\" tiene un formato no inválido"
|
||||
|
||||
msgid ""
|
||||
"You have to provide both 'oldName' and 'newName' params in order to properly "
|
||||
"rename the tag"
|
||||
msgstr ""
|
||||
"Debes proporcionar tanto el parámetro 'oldName' como 'newName' para poder "
|
||||
"renombrar la etiqueta correctamente"
|
||||
|
||||
#, php-format
|
||||
msgid "It wasn't possible to find a tag with name '%s'"
|
||||
msgstr "No fue posible encontrar una etiqueta con el nombre '%s'"
|
||||
|
||||
#, php-format
|
||||
msgid "You need to provide the Bearer type in the %s header."
|
||||
msgstr "Debes proporcionar el typo Bearer en la cabecera %s."
|
||||
|
@ -3,13 +3,9 @@ namespace Shlinkio\Shlink\Rest\Action;
|
||||
|
||||
use Fig\Http\Message\RequestMethodInterface;
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use Interop\Http\ServerMiddleware\MiddlewareInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
abstract class AbstractRestAction implements MiddlewareInterface, RequestMethodInterface, StatusCodeInterface
|
||||
{
|
||||
@ -22,29 +18,4 @@ abstract class AbstractRestAction implements MiddlewareInterface, RequestMethodI
|
||||
{
|
||||
$this->logger = $logger ?: new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming server request and return a response, optionally delegating
|
||||
* to the next middleware component to create the response.
|
||||
*
|
||||
* @param Request $request
|
||||
* @param DelegateInterface $delegate
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function process(Request $request, DelegateInterface $delegate)
|
||||
{
|
||||
if ($request->getMethod() === self::METHOD_OPTIONS) {
|
||||
return new EmptyResponse();
|
||||
}
|
||||
|
||||
return $this->dispatch($request, $delegate);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param DelegateInterface $delegate
|
||||
* @return null|Response
|
||||
*/
|
||||
abstract protected function dispatch(Request $request, DelegateInterface $delegate);
|
||||
}
|
||||
|
@ -54,8 +54,9 @@ class AuthenticateAction extends AbstractRestAction
|
||||
* @param Request $request
|
||||
* @param DelegateInterface $delegate
|
||||
* @return null|Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function dispatch(Request $request, DelegateInterface $delegate)
|
||||
public function process(Request $request, DelegateInterface $delegate)
|
||||
{
|
||||
$authData = $request->getParsedBody();
|
||||
if (! isset($authData['apiKey'])) {
|
||||
|
@ -55,8 +55,9 @@ class CreateShortcodeAction extends AbstractRestAction
|
||||
* @param Request $request
|
||||
* @param DelegateInterface $delegate
|
||||
* @return null|Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function dispatch(Request $request, DelegateInterface $delegate)
|
||||
public function process(Request $request, DelegateInterface $delegate)
|
||||
{
|
||||
$postData = $request->getParsedBody();
|
||||
if (! isset($postData['longUrl'])) {
|
||||
|
@ -13,7 +13,7 @@ use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class EditTagsAction extends AbstractRestAction
|
||||
class EditShortcodeTagsAction extends AbstractRestAction
|
||||
{
|
||||
/**
|
||||
* @var ShortUrlServiceInterface
|
||||
@ -25,7 +25,7 @@ class EditTagsAction extends AbstractRestAction
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* EditTagsAction constructor.
|
||||
* EditShortcodeTagsAction constructor.
|
||||
* @param ShortUrlServiceInterface $shortUrlService
|
||||
* @param TranslatorInterface $translator
|
||||
* @param LoggerInterface|null $logger
|
||||
@ -46,8 +46,9 @@ class EditTagsAction extends AbstractRestAction
|
||||
* @param Request $request
|
||||
* @param DelegateInterface $delegate
|
||||
* @return null|Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
protected function dispatch(Request $request, DelegateInterface $delegate)
|
||||
public function process(Request $request, DelegateInterface $delegate)
|
||||
{
|
||||
$shortCode = $request->getAttribute('shortCode');
|
||||
$bodyParams = $request->getParsedBody();
|
@ -47,8 +47,9 @@ class GetVisitsAction extends AbstractRestAction
|
||||
* @param Request $request
|
||||
* @param DelegateInterface $delegate
|
||||
* @return null|Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function dispatch(Request $request, DelegateInterface $delegate)
|
||||
public function process(Request $request, DelegateInterface $delegate)
|
||||
{
|
||||
$shortCode = $request->getAttribute('shortCode');
|
||||
$startDate = $this->getDateQueryParam($request, 'startDate');
|
||||
|
@ -6,7 +6,6 @@ use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||
@ -49,8 +48,9 @@ class ListShortcodesAction extends AbstractRestAction
|
||||
* @param Request $request
|
||||
* @param DelegateInterface $delegate
|
||||
* @return null|Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function dispatch(Request $request, DelegateInterface $delegate)
|
||||
public function process(Request $request, DelegateInterface $delegate)
|
||||
{
|
||||
try {
|
||||
$params = $this->queryToListParams($request->getQueryParams());
|
||||
@ -67,7 +67,7 @@ class ListShortcodesAction extends AbstractRestAction
|
||||
|
||||
/**
|
||||
* @param array $query
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
public function queryToListParams(array $query)
|
||||
{
|
||||
|
@ -46,14 +46,15 @@ class ResolveUrlAction extends AbstractRestAction
|
||||
* @param Request $request
|
||||
* @param DelegateInterface $delegate
|
||||
* @return null|Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function dispatch(Request $request, DelegateInterface $delegate)
|
||||
public function process(Request $request, DelegateInterface $delegate)
|
||||
{
|
||||
$shortCode = $request->getAttribute('shortCode');
|
||||
|
||||
try {
|
||||
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||
if (! isset($longUrl)) {
|
||||
if ($longUrl === null) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||
'message' => sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode),
|
||||
|
55
module/Rest/src/Action/Tag/CreateTagsAction.php
Normal file
55
module/Rest/src/Action/Tag/CreateTagsAction.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Rest\Action\Tag;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagService;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
class CreateTagsAction extends AbstractRestAction
|
||||
{
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
/**
|
||||
* CreateTagsAction constructor.
|
||||
* @param TagServiceInterface $tagService
|
||||
* @param LoggerInterface|null $logger
|
||||
*
|
||||
* @DI\Inject({TagService::class, LoggerInterface::class})
|
||||
*/
|
||||
public function __construct(TagServiceInterface $tagService, LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($logger);
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming server request and return a response, optionally delegating
|
||||
* to the next middleware component to create the response.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param DelegateInterface $delegate
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
$tags = isset($body['tags']) ? $body['tags'] : [];
|
||||
|
||||
return new JsonResponse([
|
||||
'tags' => [
|
||||
'data' => $this->tagService->createTags($tags)->toArray(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
51
module/Rest/src/Action/Tag/DeleteTagsAction.php
Normal file
51
module/Rest/src/Action/Tag/DeleteTagsAction.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Rest\Action\Tag;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagService;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
|
||||
class DeleteTagsAction extends AbstractRestAction
|
||||
{
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
/**
|
||||
* DeleteTagsAction constructor.
|
||||
* @param TagServiceInterface $tagService
|
||||
* @param LoggerInterface|null $logger
|
||||
*
|
||||
* @DI\Inject({TagService::class, LoggerInterface::class})
|
||||
*/
|
||||
public function __construct(TagServiceInterface $tagService, LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($logger);
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming server request and return a response, optionally delegating
|
||||
* to the next middleware component to create the response.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param DelegateInterface $delegate
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
|
||||
{
|
||||
$query = $request->getQueryParams();
|
||||
$tags = isset($query['tags']) ? $query['tags'] : [];
|
||||
|
||||
$this->tagService->deleteTags($tags);
|
||||
return new EmptyResponse();
|
||||
}
|
||||
}
|
52
module/Rest/src/Action/Tag/ListTagsAction.php
Normal file
52
module/Rest/src/Action/Tag/ListTagsAction.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Rest\Action\Tag;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagService;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
|
||||
class ListTagsAction extends AbstractRestAction
|
||||
{
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
/**
|
||||
* ListTagsAction constructor.
|
||||
* @param TagServiceInterface $tagService
|
||||
* @param LoggerInterface|null $logger
|
||||
*
|
||||
* @DI\Inject({TagService::class, LoggerInterface::class})
|
||||
*/
|
||||
public function __construct(TagServiceInterface $tagService, LoggerInterface $logger = null)
|
||||
{
|
||||
parent::__construct($logger);
|
||||
$this->tagService = $tagService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming server request and return a response, optionally delegating
|
||||
* to the next middleware component to create the response.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param DelegateInterface $delegate
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
|
||||
{
|
||||
return new JsonResponse([
|
||||
'tags' => [
|
||||
'data' => $this->tagService->listTags(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
83
module/Rest/src/Action/Tag/UpdateTagAction.php
Normal file
83
module/Rest/src/Action/Tag/UpdateTagAction.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Rest\Action\Tag;
|
||||
|
||||
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
|
||||
use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagService;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\AbstractRestAction;
|
||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||
use Zend\Diactoros\Response\EmptyResponse;
|
||||
use Zend\Diactoros\Response\JsonResponse;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\I18n\Translator\TranslatorInterface;
|
||||
|
||||
class UpdateTagAction extends AbstractRestAction
|
||||
{
|
||||
/**
|
||||
* @var TagServiceInterface
|
||||
*/
|
||||
private $tagService;
|
||||
/**
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
* UpdateTagAction constructor.
|
||||
* @param TagServiceInterface $tagService
|
||||
* @param TranslatorInterface $translator
|
||||
* @param LoggerInterface|null $logger
|
||||
*
|
||||
* @DI\Inject({TagService::class, Translator::class, LoggerInterface::class})
|
||||
*/
|
||||
public function __construct(
|
||||
TagServiceInterface $tagService,
|
||||
TranslatorInterface $translator,
|
||||
LoggerInterface $logger = null
|
||||
) {
|
||||
parent::__construct($logger);
|
||||
$this->tagService = $tagService;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process an incoming server request and return a response, optionally delegating
|
||||
* to the next middleware component to create the response.
|
||||
*
|
||||
* @param ServerRequestInterface $request
|
||||
* @param DelegateInterface $delegate
|
||||
*
|
||||
* @return ResponseInterface
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
|
||||
{
|
||||
$body = $request->getParsedBody();
|
||||
if (! isset($body['oldName'], $body['newName'])) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||
'message' => $this->translator->translate(
|
||||
'You have to provide both \'oldName\' and \'newName\' params in order to properly rename the tag'
|
||||
),
|
||||
], self::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->tagService->renameTag($body['oldName'], $body['newName']);
|
||||
return new EmptyResponse();
|
||||
} catch (EntityDoesNotExistException $e) {
|
||||
return new JsonResponse([
|
||||
'error' => RestUtils::NOT_FOUND_ERROR,
|
||||
'message' => sprintf(
|
||||
$this->translator->translate('It wasn\'t possible to find a tag with name \'%s\''),
|
||||
$body['oldName']
|
||||
),
|
||||
], self::STATUS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
|
||||
use Shlinkio\Shlink\Rest\Authentication\JWTService;
|
||||
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
|
||||
@ -69,7 +70,7 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface, StatusCodeIn
|
||||
$routeResult = $request->getAttribute(RouteResult::class);
|
||||
if (! isset($routeResult)
|
||||
|| $routeResult->isFailure()
|
||||
|| $routeResult->getMatchedRouteName() === 'rest-authenticate'
|
||||
|| $routeResult->getMatchedRouteName() === AuthenticateAction::class
|
||||
|| $request->getMethod() === 'OPTIONS'
|
||||
) {
|
||||
return $delegate->process($request);
|
||||
|
@ -1,12 +1,13 @@
|
||||
<?php
|
||||
namespace Shlinkio\Shlink\Rest\Middleware;
|
||||
|
||||
use Fig\Http\Message\RequestMethodInterface;
|
||||
use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use Interop\Http\ServerMiddleware\MiddlewareInterface;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
|
||||
class CrossDomainMiddleware implements MiddlewareInterface
|
||||
class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterface
|
||||
{
|
||||
/**
|
||||
* Process an incoming server request and return a response, optionally delegating
|
||||
@ -16,6 +17,7 @@ class CrossDomainMiddleware implements MiddlewareInterface
|
||||
* @param DelegateInterface $delegate
|
||||
*
|
||||
* @return Response
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function process(Request $request, DelegateInterface $delegate)
|
||||
{
|
||||
@ -28,13 +30,14 @@ class CrossDomainMiddleware implements MiddlewareInterface
|
||||
// Add Allow-Origin header
|
||||
$response = $response->withHeader('Access-Control-Allow-Origin', $request->getHeader('Origin'))
|
||||
->withHeader('Access-Control-Expose-Headers', 'Authorization');
|
||||
if ($request->getMethod() !== 'OPTIONS') {
|
||||
if ($request->getMethod() !== self::METHOD_OPTIONS) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Add OPTIONS-specific headers
|
||||
foreach ([
|
||||
'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS', // TODO Should be based on path
|
||||
'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS', // TODO Should be dynamic
|
||||
// 'Access-Control-Allow-Methods' => $response->getHeaderLine('Allow'),
|
||||
'Access-Control-Max-Age' => '1000',
|
||||
'Access-Control-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'),
|
||||
] as $key => $value) {
|
||||
|
@ -6,15 +6,15 @@ use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||
use Shlinkio\Shlink\Rest\Action\EditTagsAction;
|
||||
use Shlinkio\Shlink\Rest\Action\EditShortcodeTagsAction;
|
||||
use ShlinkioTest\Shlink\Common\Util\TestUtils;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class EditTagsActionTest extends TestCase
|
||||
class EditShortcodeTagsActionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var EditTagsAction
|
||||
* @var EditShortcodeTagsAction
|
||||
*/
|
||||
protected $action;
|
||||
/**
|
||||
@ -25,7 +25,7 @@ class EditTagsActionTest extends TestCase
|
||||
public function setUp()
|
||||
{
|
||||
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
|
||||
$this->action = new EditTagsAction($this->shortUrlService->reveal(), Translator::factory([]));
|
||||
$this->action = new EditShortcodeTagsAction($this->shortUrlService->reveal(), Translator::factory([]));
|
||||
}
|
||||
|
||||
/**
|
56
module/Rest/test/Action/Tag/CreateTagsActionTest.php
Normal file
56
module/Rest/test/Action/Tag/CreateTagsActionTest.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\Rest\Action\Tag;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\Tag\CreateTagsAction;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
|
||||
class CreateTagsActionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var CreateTagsAction
|
||||
*/
|
||||
private $action;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
$this->action = new CreateTagsAction($this->tagService->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideTags
|
||||
* @param array|null $tags
|
||||
*/
|
||||
public function processDelegatesIntoService($tags)
|
||||
{
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody(['tags' => $tags]);
|
||||
/** @var MethodProphecy $deleteTags */
|
||||
$deleteTags = $this->tagService->createTags($tags ?: [])->willReturn(new ArrayCollection());
|
||||
|
||||
$response = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$deleteTags->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideTags()
|
||||
{
|
||||
return [
|
||||
[['foo', 'bar', 'baz']],
|
||||
[['some', 'thing']],
|
||||
[null],
|
||||
[[]],
|
||||
];
|
||||
}
|
||||
}
|
55
module/Rest/test/Action/Tag/DeleteTagsActionTest.php
Normal file
55
module/Rest/test/Action/Tag/DeleteTagsActionTest.php
Normal file
@ -0,0 +1,55 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\Rest\Action\Tag;
|
||||
|
||||
use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\Tag\DeleteTagsAction;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
|
||||
class DeleteTagsActionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var DeleteTagsAction
|
||||
*/
|
||||
private $action;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
$this->action = new DeleteTagsAction($this->tagService->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideTags
|
||||
* @param array|null $tags
|
||||
*/
|
||||
public function processDelegatesIntoService($tags)
|
||||
{
|
||||
$request = ServerRequestFactory::fromGlobals()->withQueryParams(['tags' => $tags]);
|
||||
/** @var MethodProphecy $deleteTags */
|
||||
$deleteTags = $this->tagService->deleteTags($tags ?: []);
|
||||
|
||||
$response = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
|
||||
|
||||
$this->assertEquals(204, $response->getStatusCode());
|
||||
$deleteTags->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
public function provideTags()
|
||||
{
|
||||
return [
|
||||
[['foo', 'bar', 'baz']],
|
||||
[['some', 'thing']],
|
||||
[null],
|
||||
[[]],
|
||||
];
|
||||
}
|
||||
}
|
50
module/Rest/test/Action/Tag/ListTagsActionTest.php
Normal file
50
module/Rest/test/Action/Tag/ListTagsActionTest.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\Rest\Action\Tag;
|
||||
|
||||
use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\Tag\ListTagsAction;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
|
||||
class ListTagsActionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ListTagsAction
|
||||
*/
|
||||
private $action;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
$this->action = new ListTagsAction($this->tagService->reveal());
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function returnsDataFromService()
|
||||
{
|
||||
/** @var MethodProphecy $listTags */
|
||||
$listTags = $this->tagService->listTags()->willReturn([new Tag('foo'), new Tag('bar')]);
|
||||
|
||||
$resp = $this->action->process(
|
||||
ServerRequestFactory::fromGlobals(),
|
||||
$this->prophesize(DelegateInterface::class)->reveal()
|
||||
);
|
||||
|
||||
$this->assertEquals([
|
||||
'tags' => [
|
||||
'data' => ['foo', 'bar'],
|
||||
],
|
||||
], \json_decode((string) $resp->getBody(), true));
|
||||
$listTags->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
89
module/Rest/test/Action/Tag/UpdateTagActionTest.php
Normal file
89
module/Rest/test/Action/Tag/UpdateTagActionTest.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
namespace ShlinkioTest\Shlink\Rest\Action\Tag;
|
||||
|
||||
use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
|
||||
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\Action\Tag\UpdateTagAction;
|
||||
use Zend\Diactoros\ServerRequestFactory;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
|
||||
class UpdateTagActionTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var UpdateTagAction
|
||||
*/
|
||||
private $action;
|
||||
/**
|
||||
* @var ObjectProphecy
|
||||
*/
|
||||
private $tagService;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->tagService = $this->prophesize(TagServiceInterface::class);
|
||||
$this->action = new UpdateTagAction($this->tagService->reveal(), Translator::factory([]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideParams
|
||||
* @param array $bodyParams
|
||||
*/
|
||||
public function whenInvalidParamsAreProvidedAnErrorIsReturned(array $bodyParams)
|
||||
{
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody($bodyParams);
|
||||
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
|
||||
|
||||
$this->assertEquals(400, $resp->getStatusCode());
|
||||
}
|
||||
|
||||
public function provideParams()
|
||||
{
|
||||
return [
|
||||
[['oldName' => 'foo']],
|
||||
[['newName' => 'foo']],
|
||||
[[]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function requestingInvalidTagReturnsError()
|
||||
{
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||
'oldName' => 'foo',
|
||||
'newName' => 'bar',
|
||||
]);
|
||||
/** @var MethodProphecy $rename */
|
||||
$rename = $this->tagService->renameTag('foo', 'bar')->willThrow(EntityDoesNotExistException::class);
|
||||
|
||||
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
|
||||
|
||||
$this->assertEquals(404, $resp->getStatusCode());
|
||||
$rename->shouldHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
*/
|
||||
public function correctInvocationRenamesTag()
|
||||
{
|
||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||
'oldName' => 'foo',
|
||||
'newName' => 'bar',
|
||||
]);
|
||||
/** @var MethodProphecy $rename */
|
||||
$rename = $this->tagService->renameTag('foo', 'bar')->willReturn(new Tag());
|
||||
|
||||
$resp = $this->action->process($request, $this->prophesize(DelegateInterface::class)->reveal());
|
||||
|
||||
$this->assertEquals(204, $resp->getStatusCode());
|
||||
$rename->shouldHaveBeenCalled();
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ use Interop\Http\ServerMiddleware\DelegateInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Prophecy\MethodProphecy;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
|
||||
use Shlinkio\Shlink\Rest\Authentication\JWTService;
|
||||
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
|
||||
use ShlinkioTest\Shlink\Common\Util\TestUtils;
|
||||
@ -56,7 +57,7 @@ class CheckAuthenticationMiddlewareTest extends TestCase
|
||||
|
||||
$request = ServerRequestFactory::fromGlobals()->withAttribute(
|
||||
RouteResult::class,
|
||||
RouteResult::fromRoute(new Route('foo', '', Route::HTTP_METHOD_ANY, 'rest-authenticate'), [])
|
||||
RouteResult::fromRoute(new Route('foo', '', Route::HTTP_METHOD_ANY, AuthenticateAction::class))
|
||||
);
|
||||
$delegate = $this->prophesize(DelegateInterface::class);
|
||||
/** @var MethodProphecy $process */
|
||||
|
Loading…
Reference in New Issue
Block a user