mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
commit
a1aa9c2031
@ -1,5 +1,6 @@
|
|||||||
# Application
|
# Application
|
||||||
APP_ENV=
|
APP_ENV=
|
||||||
|
SECRET_KEY=
|
||||||
SHORTENED_URL_SCHEMA=
|
SHORTENED_URL_SCHEMA=
|
||||||
SHORTENED_URL_HOSTNAME=
|
SHORTENED_URL_HOSTNAME=
|
||||||
SHORTCODE_CHARS=
|
SHORTCODE_CHARS=
|
||||||
@ -12,7 +13,3 @@ CLI_LOCALE=
|
|||||||
DB_USER=
|
DB_USER=
|
||||||
DB_PASSWORD=
|
DB_PASSWORD=
|
||||||
DB_NAME=
|
DB_NAME=
|
||||||
|
|
||||||
# Rest authentication
|
|
||||||
REST_USER=
|
|
||||||
REST_PASSWORD=
|
|
||||||
|
19
.phpstorm.meta.php
Normal file
19
.phpstorm.meta.php
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
namespace PHPSTORM_META;
|
||||||
|
|
||||||
|
use Interop\Container\ContainerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PhpStorm Container Interop code completion
|
||||||
|
*
|
||||||
|
* Add code completion for container-interop.
|
||||||
|
*
|
||||||
|
* \App\ClassName::class will automatically resolve to it's own name.
|
||||||
|
*
|
||||||
|
* Custom strings like ``"cache"`` or ``"logger"`` need to be added manually.
|
||||||
|
*/
|
||||||
|
$STATIC_METHOD_TYPES = [
|
||||||
|
ContainerInterface::get('') => [
|
||||||
|
'' == '@',
|
||||||
|
],
|
||||||
|
];
|
1
.travis-php.ini
Normal file
1
.travis-php.ini
Normal file
@ -0,0 +1 @@
|
|||||||
|
extension="memcached.so"
|
@ -10,6 +10,8 @@ php:
|
|||||||
- 7
|
- 7
|
||||||
- 7.1
|
- 7.1
|
||||||
|
|
||||||
|
before_install: phpenv config-add .travis-php.ini
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- composer self-update
|
- composer self-update
|
||||||
- composer install --no-interaction
|
- composer install --no-interaction
|
||||||
|
@ -25,7 +25,11 @@
|
|||||||
"acelaya/zsm-annotated-services": "^0.2.0",
|
"acelaya/zsm-annotated-services": "^0.2.0",
|
||||||
"doctrine/orm": "^2.5",
|
"doctrine/orm": "^2.5",
|
||||||
"guzzlehttp/guzzle": "^6.2",
|
"guzzlehttp/guzzle": "^6.2",
|
||||||
"symfony/console": "^3.0"
|
"symfony/console": "^3.0",
|
||||||
|
"firebase/php-jwt": "^4.0",
|
||||||
|
"monolog/monolog": "^1.21",
|
||||||
|
"theorchard/monolog-cascade": "^0.4",
|
||||||
|
"endroid/qrcode": "^1.7"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^5.0",
|
"phpunit/phpunit": "^5.0",
|
||||||
|
10
config/autoload/app_options.global.php
Normal file
10
config/autoload/app_options.global.php
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
|
||||||
|
'app_options' => [
|
||||||
|
'name' => 'Shlink',
|
||||||
|
'version' => '1.1.0',
|
||||||
|
'secret_key' => env('SECRET_KEY'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
@ -1,15 +0,0 @@
|
|||||||
<?php
|
|
||||||
return [
|
|
||||||
|
|
||||||
'database' => [
|
|
||||||
'driver' => 'pdo_mysql',
|
|
||||||
'user' => env('DB_USER'),
|
|
||||||
'password' => env('DB_PASSWORD'),
|
|
||||||
'dbname' => env('DB_NAME', 'shlink'),
|
|
||||||
'charset' => 'utf8',
|
|
||||||
'driverOptions' => [
|
|
||||||
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
20
config/autoload/entity-manager.global.php
Normal file
20
config/autoload/entity-manager.global.php
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
|
||||||
|
'entity_manager' => [
|
||||||
|
'orm' => [
|
||||||
|
'proxies_dir' => 'data/proxies',
|
||||||
|
],
|
||||||
|
'connection' => [
|
||||||
|
'driver' => 'pdo_mysql',
|
||||||
|
'user' => env('DB_USER'),
|
||||||
|
'password' => env('DB_PASSWORD'),
|
||||||
|
'dbname' => env('DB_NAME', 'shlink'),
|
||||||
|
'charset' => 'utf8',
|
||||||
|
'driverOptions' => [
|
||||||
|
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
@ -1,7 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'debug' => true,
|
|
||||||
|
|
||||||
|
'debug' => true,
|
||||||
'config_cache_enabled' => false,
|
'config_cache_enabled' => false,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
32
config/autoload/logger.global.php
Normal file
32
config/autoload/logger.global.php
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
use Monolog\Handler\RotatingFileHandler;
|
||||||
|
use Monolog\Logger;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'logger' => [
|
||||||
|
'formatters' => [
|
||||||
|
'dashed' => [
|
||||||
|
'format' => '[%datetime%] %channel%.%level_name% - %message% %context%' . PHP_EOL,
|
||||||
|
'include_stacktraces' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'handlers' => [
|
||||||
|
'rotating_file_handler' => [
|
||||||
|
'class' => RotatingFileHandler::class,
|
||||||
|
'level' => Logger::INFO,
|
||||||
|
'filename' => 'data/log/shlink_log.log',
|
||||||
|
'max_files' => 30,
|
||||||
|
'formatter' => 'dashed',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
'loggers' => [
|
||||||
|
'Shlink' => [
|
||||||
|
'handlers' => ['rotating_file_handler'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
14
config/autoload/logger.local.php.dist
Normal file
14
config/autoload/logger.local.php.dist
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
use Monolog\Logger;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'logger' => [
|
||||||
|
'handlers' => [
|
||||||
|
'rotating_file_handler' => [
|
||||||
|
'level' => Logger::DEBUG,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
@ -1,19 +0,0 @@
|
|||||||
### Installation steps
|
|
||||||
|
|
||||||
- Define ENV vars in apache or nginx:
|
|
||||||
- SHORTENED_URL_SCHEMA: http|https
|
|
||||||
- SHORTENED_URL_HOSTNAME: Short domain
|
|
||||||
- SHORTCODE_CHARS: The char set used to generate short codes (defaults to **123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ**, but a new one can be generated with the `config:generate-charset` command)
|
|
||||||
- DB_USER: MySQL database user
|
|
||||||
- DB_PASSWORD: MySQL database password
|
|
||||||
- REST_USER: Username for REST authentication
|
|
||||||
- REST_PASSWORD: Password for REST authentication
|
|
||||||
- DB_NAME: MySQL database name (defaults to **shlink**)
|
|
||||||
- DEFAULT_LOCALE: Language in which web requests (browser and REST) will be returned if no `Accept-Language` header is sent (defaults to **en**)
|
|
||||||
- CLI_LOCALE: Language in which console command messages will be displayed (defaults to **en**)
|
|
||||||
- Create database (`vendor/bin/doctrine orm:schema-tool:create`)
|
|
||||||
- Add write permissions to `data` directory
|
|
||||||
- Create doctrine proxies (`vendor/bin/doctrine orm:generate-proxies`)
|
|
||||||
- Create symlink to bin/cli as `shlink` in /usr/local/bin (linux only. Optional)
|
|
||||||
|
|
||||||
Supported languages: es and en
|
|
@ -1,289 +0,0 @@
|
|||||||
|
|
||||||
# REST API documentation
|
|
||||||
|
|
||||||
## Error management
|
|
||||||
|
|
||||||
Statuses:
|
|
||||||
|
|
||||||
* 400 -> controlled error
|
|
||||||
* 401 -> authentication error
|
|
||||||
* 500 -> unexpected error
|
|
||||||
|
|
||||||
[TODO]
|
|
||||||
|
|
||||||
## Authentication
|
|
||||||
|
|
||||||
Once you have called to the authentication endpoint for the first time (see below) yopu will get an authentication token.
|
|
||||||
|
|
||||||
You will have to send that token in the `X-Auth-Token` header on any later request or you will get an authentication error.
|
|
||||||
|
|
||||||
## Language
|
|
||||||
|
|
||||||
In order to set the application language, you have to pass it by using the `Accept-Language` header.
|
|
||||||
|
|
||||||
If not provided or provided language is not supported, english (en_US) will be used.
|
|
||||||
|
|
||||||
## Endpoints
|
|
||||||
|
|
||||||
#### Authenticate
|
|
||||||
|
|
||||||
**REQUEST**
|
|
||||||
|
|
||||||
* `POST` -> `/rest/authenticate`
|
|
||||||
* Params:
|
|
||||||
* username: `string`
|
|
||||||
* password: `string`
|
|
||||||
|
|
||||||
**SUCCESS RESPONSE**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"token": "9f741eb0-33d7-4c56-b8f7-3719e9929946"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**ERROR RESPONSE**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "INVALID_ARGUMENT",
|
|
||||||
"message": "You have to provide both \"username\" and \"password\""
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Posible errors:
|
|
||||||
|
|
||||||
* **INVALID_ARGUMENT**: Username or password were not provided.
|
|
||||||
* **INVALID_CREDENTIALS**: Username or password are incorrect.
|
|
||||||
|
|
||||||
|
|
||||||
#### Create shortcode
|
|
||||||
|
|
||||||
**REQUEST**
|
|
||||||
|
|
||||||
* `POST` -> `/rest/short-codes`
|
|
||||||
* Params:
|
|
||||||
* longUrl: `string` -> The URL to shorten
|
|
||||||
* Headers:
|
|
||||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
|
||||||
|
|
||||||
**SUCCESS RESPONSE**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"longUrl": "https://www.facebook.com/something/something",
|
|
||||||
"shortUrl": "https://doma.in/rY9Kr",
|
|
||||||
"shortCode": "rY9Kr"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**ERROR RESPONSE**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "INVALID_URL",
|
|
||||||
"message": "Provided URL \"wfwef\" is invalid. Try with a different one."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Posible errors:
|
|
||||||
|
|
||||||
* **INVALID_ARGUMENT**: The longUrl was not provided.
|
|
||||||
* **INVALID_URL**: Provided longUrl has an invalid format or does not resolve.
|
|
||||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
|
||||||
|
|
||||||
|
|
||||||
#### Resolve URL
|
|
||||||
|
|
||||||
**REQUEST**
|
|
||||||
|
|
||||||
* `GET` -> `/rest/short-codes/{shortCode}`
|
|
||||||
* Route params:
|
|
||||||
* shortCode: `string` -> The short code we want to resolve
|
|
||||||
* Headers:
|
|
||||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
|
||||||
|
|
||||||
**SUCCESS RESPONSE**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"longUrl": "https://www.facebook.com/something/something"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**ERROR RESPONSE**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "INVALID_SHORTCODE",
|
|
||||||
"message": "Provided short code \"abc123\" has an invalid format"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Posible errors:
|
|
||||||
|
|
||||||
* **INVALID_ARGUMENT**: No longUrl was found for provided shortCode.
|
|
||||||
* **INVALID_SHORTCODE**: Provided shortCode does not match the character set used by the app to generate short codes.
|
|
||||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
|
||||||
|
|
||||||
|
|
||||||
#### List shortened URLs
|
|
||||||
|
|
||||||
**REQUEST**
|
|
||||||
|
|
||||||
* `GET` -> `/rest/short-codes`
|
|
||||||
* Query params:
|
|
||||||
* page: `integer` -> The page to list. Defaults to 1 if not provided.
|
|
||||||
* Headers:
|
|
||||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
|
||||||
|
|
||||||
**SUCCESS RESPONSE**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"shortUrls": {
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"shortCode": "abc123",
|
|
||||||
"originalUrl": "http://www.alejandrocelaya.com",
|
|
||||||
"dateCreated": "2016-04-30T18:01:47+0200",
|
|
||||||
"visitsCount": 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortCode": "def456",
|
|
||||||
"originalUrl": "http://www.alejandrocelaya.com/en",
|
|
||||||
"dateCreated": "2016-04-30T18:03:43+0200",
|
|
||||||
"visitsCount": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortCode": "ghi789",
|
|
||||||
"originalUrl": "http://www.alejandrocelaya.com/es",
|
|
||||||
"dateCreated": "2016-04-30T18:10:38+0200",
|
|
||||||
"visitsCount": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortCode": "jkl987",
|
|
||||||
"originalUrl": "http://www.alejandrocelaya.com/es/",
|
|
||||||
"dateCreated": "2016-04-30T18:10:57+0200",
|
|
||||||
"visitsCount": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortCode": "mno654",
|
|
||||||
"originalUrl": "http://blog.alejandrocelaya.com/2016/04/09/improving-zend-service-manager-workflow-with-annotations/",
|
|
||||||
"dateCreated": "2016-04-30T19:21:05+0200",
|
|
||||||
"visitsCount": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortCode": "pqr321",
|
|
||||||
"originalUrl": "http://www.google.com",
|
|
||||||
"dateCreated": "2016-05-01T11:19:53+0200",
|
|
||||||
"visitsCount": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortCode": "stv159",
|
|
||||||
"originalUrl": "http://www.acelaya.com",
|
|
||||||
"dateCreated": "2016-06-12T17:49:21+0200",
|
|
||||||
"visitsCount": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortCode": "wxy753",
|
|
||||||
"originalUrl": "http://www.atomic-reader.com",
|
|
||||||
"dateCreated": "2016-06-12T17:50:27+0200",
|
|
||||||
"visitsCount": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortCode": "zab852",
|
|
||||||
"originalUrl": "http://foo.com",
|
|
||||||
"dateCreated": "2016-07-03T09:07:36+0200",
|
|
||||||
"visitsCount": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"shortCode": "cde963",
|
|
||||||
"originalUrl": "https://www.facebook.com.com",
|
|
||||||
"dateCreated": "2016-07-03T09:12:35+0200",
|
|
||||||
"visitsCount": 0
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"pagination": {
|
|
||||||
"currentPage": 4,
|
|
||||||
"pagesCount": 15
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**ERROR RESPONSE**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "UNKNOWN_ERROR",
|
|
||||||
"message": "Unexpected error occured"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Posible errors:
|
|
||||||
|
|
||||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
|
||||||
|
|
||||||
|
|
||||||
#### Get visits
|
|
||||||
|
|
||||||
**REQUEST**
|
|
||||||
|
|
||||||
* `GET` -> `/rest/short-codes/{shortCode}/visits`
|
|
||||||
* Route params:
|
|
||||||
* shortCode: `string` -> The shortCode from which we eant to get the visits.
|
|
||||||
* Query params:
|
|
||||||
* startDate: `string` -> If provided, only visits older that this date will be returned
|
|
||||||
* endDate: `string` -> If provided, only visits newer that this date will be returned
|
|
||||||
* Headers:
|
|
||||||
* X-Auth-Token: `string` -> The token provided in the authentication request
|
|
||||||
|
|
||||||
**SUCCESS RESPONSE**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"shortUrls": {
|
|
||||||
"data": [
|
|
||||||
{
|
|
||||||
"referer": null,
|
|
||||||
"date": "2016-06-18T09:32:22+0200",
|
|
||||||
"remoteAddr": "127.0.0.1",
|
|
||||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"referer": null,
|
|
||||||
"date": "2016-04-30T19:20:06+0200",
|
|
||||||
"remoteAddr": "127.0.0.1",
|
|
||||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"referer": "google.com",
|
|
||||||
"date": "2016-04-30T19:19:57+0200",
|
|
||||||
"remoteAddr": "1.2.3.4",
|
|
||||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"referer": null,
|
|
||||||
"date": "2016-04-30T19:17:35+0200",
|
|
||||||
"remoteAddr": "127.0.0.1",
|
|
||||||
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**ERROR RESPONSE**
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"error": "INVALID_ARGUMENT",
|
|
||||||
"message": "Provided short code \"abc123\" is invalid"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Posible errors:
|
|
||||||
|
|
||||||
* **INVALID_ARGUMENT**: The shortcode does not belong to any short URL
|
|
||||||
* **UNKNOWN_ERROR**: Something unexpected happened.
|
|
2
data/log/.gitignore
vendored
Normal file
2
data/log/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
@ -5,12 +5,16 @@ return [
|
|||||||
|
|
||||||
'cli' => [
|
'cli' => [
|
||||||
'commands' => [
|
'commands' => [
|
||||||
Command\GenerateShortcodeCommand::class,
|
Command\Shortcode\GenerateShortcodeCommand::class,
|
||||||
Command\ResolveUrlCommand::class,
|
Command\Shortcode\ResolveUrlCommand::class,
|
||||||
Command\ListShortcodesCommand::class,
|
Command\Shortcode\ListShortcodesCommand::class,
|
||||||
Command\GetVisitsCommand::class,
|
Command\Shortcode\GetVisitsCommand::class,
|
||||||
Command\ProcessVisitsCommand::class,
|
Command\Visit\ProcessVisitsCommand::class,
|
||||||
Command\Config\GenerateCharsetCommand::class,
|
Command\Config\GenerateCharsetCommand::class,
|
||||||
|
Command\Config\GenerateSecretCommand::class,
|
||||||
|
Command\Api\GenerateKeyCommand::class,
|
||||||
|
Command\Api\DisableKeyCommand::class,
|
||||||
|
Command\Api\ListKeysCommand::class,
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -10,13 +10,16 @@ return [
|
|||||||
'factories' => [
|
'factories' => [
|
||||||
Application::class => ApplicationFactory::class,
|
Application::class => ApplicationFactory::class,
|
||||||
|
|
||||||
Command\GenerateShortcodeCommand::class => AnnotatedFactory::class,
|
Command\Shortcode\GenerateShortcodeCommand::class => AnnotatedFactory::class,
|
||||||
Command\ResolveUrlCommand::class => AnnotatedFactory::class,
|
Command\Shortcode\ResolveUrlCommand::class => AnnotatedFactory::class,
|
||||||
Command\ListShortcodesCommand::class => AnnotatedFactory::class,
|
Command\Shortcode\ListShortcodesCommand::class => AnnotatedFactory::class,
|
||||||
Command\GetVisitsCommand::class => AnnotatedFactory::class,
|
Command\Shortcode\GetVisitsCommand::class => AnnotatedFactory::class,
|
||||||
Command\ProcessVisitsCommand::class => AnnotatedFactory::class,
|
Command\Visit\ProcessVisitsCommand::class => AnnotatedFactory::class,
|
||||||
Command\ProcessVisitsCommand::class => AnnotatedFactory::class,
|
|
||||||
Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class,
|
Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class,
|
||||||
|
Command\Config\GenerateSecretCommand::class => AnnotatedFactory::class,
|
||||||
|
Command\Api\GenerateKeyCommand::class => AnnotatedFactory::class,
|
||||||
|
Command\Api\DisableKeyCommand::class => AnnotatedFactory::class,
|
||||||
|
Command\Api\ListKeysCommand::class => AnnotatedFactory::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
Binary file not shown.
@ -1,8 +1,8 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Shlink 1.0\n"
|
"Project-Id-Version: Shlink 1.0\n"
|
||||||
"POT-Creation-Date: 2016-08-01 21:21+0200\n"
|
"POT-Creation-Date: 2016-08-07 20:16+0200\n"
|
||||||
"PO-Revision-Date: 2016-08-01 21:22+0200\n"
|
"PO-Revision-Date: 2016-08-07 20:18+0200\n"
|
||||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
@ -17,6 +17,46 @@ msgstr ""
|
|||||||
"X-Poedit-SearchPath-0: src\n"
|
"X-Poedit-SearchPath-0: src\n"
|
||||||
"X-Poedit-SearchPath-1: config\n"
|
"X-Poedit-SearchPath-1: config\n"
|
||||||
|
|
||||||
|
msgid "Disables an API key."
|
||||||
|
msgstr "Desahbilita una clave de API."
|
||||||
|
|
||||||
|
msgid "The API key to disable"
|
||||||
|
msgstr "La clave de API a deshabilitar"
|
||||||
|
|
||||||
|
#, php-format
|
||||||
|
msgid "API key %s properly disabled"
|
||||||
|
msgstr "Clave de API %s deshabilitada correctamente"
|
||||||
|
|
||||||
|
#, php-format
|
||||||
|
msgid "API key \"%s\" does not exist."
|
||||||
|
msgstr "La clave de API \"%s\" no existe."
|
||||||
|
|
||||||
|
msgid "Generates a new valid API key."
|
||||||
|
msgstr "Genera una nueva clave de API válida."
|
||||||
|
|
||||||
|
msgid "The date in which the API key should expire. Use any valid PHP format."
|
||||||
|
msgstr ""
|
||||||
|
"La fecha en la que la clave de API debe expirar. Utiliza cualquier valor "
|
||||||
|
"válido en PHP."
|
||||||
|
|
||||||
|
msgid "Generated API key"
|
||||||
|
msgstr "Generada clave de API"
|
||||||
|
|
||||||
|
msgid "Lists all the available API keys."
|
||||||
|
msgstr "Lista todas las claves de API disponibles."
|
||||||
|
|
||||||
|
msgid "Tells if only enabled API keys should be returned."
|
||||||
|
msgstr "Define si sólo las claves de API habilitadas deben ser devueltas."
|
||||||
|
|
||||||
|
msgid "Key"
|
||||||
|
msgstr "Clave"
|
||||||
|
|
||||||
|
msgid "Expiration date"
|
||||||
|
msgstr "Fecha de caducidad"
|
||||||
|
|
||||||
|
msgid "Is enabled"
|
||||||
|
msgstr "Está habilitada"
|
||||||
|
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Generates a character set sample just by shuffling the default one, \"%s\". "
|
"Generates a character set sample just by shuffling the default one, \"%s\". "
|
||||||
|
62
module/CLI/src/Command/Api/DisableKeyCommand.php
Normal file
62
module/CLI/src/Command/Api/DisableKeyCommand.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
|
||||||
|
class DisableKeyCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ApiKeyServiceInterface
|
||||||
|
*/
|
||||||
|
private $apiKeyService;
|
||||||
|
/**
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
private $translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DisableKeyCommand constructor.
|
||||||
|
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
|
||||||
|
* @param TranslatorInterface $translator
|
||||||
|
*
|
||||||
|
* @Inject({ApiKeyService::class, "translator"})
|
||||||
|
*/
|
||||||
|
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||||
|
{
|
||||||
|
$this->apiKeyService = $apiKeyService;
|
||||||
|
$this->translator = $translator;
|
||||||
|
parent::__construct(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure()
|
||||||
|
{
|
||||||
|
$this->setName('api-key:disable')
|
||||||
|
->setDescription($this->translator->translate('Disables an API key.'))
|
||||||
|
->addArgument('apiKey', InputArgument::REQUIRED, $this->translator->translate('The API key to disable'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$apiKey = $input->getArgument('apiKey');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->apiKeyService->disable($apiKey);
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
$this->translator->translate('API key %s properly disabled'),
|
||||||
|
'<info>' . $apiKey . '</info>'
|
||||||
|
));
|
||||||
|
} catch (\InvalidArgumentException $e) {
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'<error>' . $this->translator->translate('API key "%s" does not exist.') . '</error>',
|
||||||
|
$apiKey
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
module/CLI/src/Command/Api/GenerateKeyCommand.php
Normal file
56
module/CLI/src/Command/Api/GenerateKeyCommand.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
|
||||||
|
class GenerateKeyCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ApiKeyServiceInterface
|
||||||
|
*/
|
||||||
|
private $apiKeyService;
|
||||||
|
/**
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
private $translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GenerateKeyCommand constructor.
|
||||||
|
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
|
||||||
|
* @param TranslatorInterface $translator
|
||||||
|
*
|
||||||
|
* @Inject({ApiKeyService::class, "translator"})
|
||||||
|
*/
|
||||||
|
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||||
|
{
|
||||||
|
$this->apiKeyService = $apiKeyService;
|
||||||
|
$this->translator = $translator;
|
||||||
|
parent::__construct(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure()
|
||||||
|
{
|
||||||
|
$this->setName('api-key:generate')
|
||||||
|
->setDescription($this->translator->translate('Generates a new valid API key.'))
|
||||||
|
->addOption(
|
||||||
|
'expirationDate',
|
||||||
|
'e',
|
||||||
|
InputOption::VALUE_OPTIONAL,
|
||||||
|
$this->translator->translate('The date in which the API key should expire. Use any valid PHP format.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$expirationDate = $input->getOption('expirationDate');
|
||||||
|
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? new \DateTime($expirationDate) : null);
|
||||||
|
$output->writeln($this->translator->translate('Generated API key') . sprintf(': <info>%s</info>', $apiKey));
|
||||||
|
}
|
||||||
|
}
|
108
module/CLI/src/Command/Api/ListKeysCommand.php
Normal file
108
module/CLI/src/Command/Api/ListKeysCommand.php
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Helper\Table;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
|
||||||
|
class ListKeysCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ApiKeyServiceInterface
|
||||||
|
*/
|
||||||
|
private $apiKeyService;
|
||||||
|
/**
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
private $translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ListKeysCommand constructor.
|
||||||
|
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
|
||||||
|
* @param TranslatorInterface $translator
|
||||||
|
*
|
||||||
|
* @Inject({ApiKeyService::class, "translator"})
|
||||||
|
*/
|
||||||
|
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
|
||||||
|
{
|
||||||
|
$this->apiKeyService = $apiKeyService;
|
||||||
|
$this->translator = $translator;
|
||||||
|
parent::__construct(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure()
|
||||||
|
{
|
||||||
|
$this->setName('api-key:list')
|
||||||
|
->setDescription($this->translator->translate('Lists all the available API keys.'))
|
||||||
|
->addOption(
|
||||||
|
'enabledOnly',
|
||||||
|
null,
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
$this->translator->translate('Tells if only enabled API keys should be returned.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$enabledOnly = $input->getOption('enabledOnly');
|
||||||
|
$list = $this->apiKeyService->listKeys($enabledOnly);
|
||||||
|
|
||||||
|
$table = new Table($output);
|
||||||
|
if ($enabledOnly) {
|
||||||
|
$table->setHeaders([
|
||||||
|
$this->translator->translate('Key'),
|
||||||
|
$this->translator->translate('Expiration date'),
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
$table->setHeaders([
|
||||||
|
$this->translator->translate('Key'),
|
||||||
|
$this->translator->translate('Is enabled'),
|
||||||
|
$this->translator->translate('Expiration date'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ApiKey $row */
|
||||||
|
foreach ($list as $row) {
|
||||||
|
$key = $row->getKey();
|
||||||
|
$expiration = $row->getExpirationDate();
|
||||||
|
$rowData = [];
|
||||||
|
|
||||||
|
if ($enabledOnly) {
|
||||||
|
$rowData[] = $key;
|
||||||
|
} else {
|
||||||
|
$rowData[] = $row->isEnabled() ? $this->getSuccessString($key) : $this->getErrorString($key);
|
||||||
|
$rowData[] = $row->isEnabled() ? $this->getSuccessString('+++') : $this->getErrorString('---');
|
||||||
|
}
|
||||||
|
|
||||||
|
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ISO8601) : '-';
|
||||||
|
$table->addRow($rowData);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->render();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $string
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getErrorString($string)
|
||||||
|
{
|
||||||
|
return sprintf('<fg=red>%s</>', $string);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $string
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getSuccessString($string)
|
||||||
|
{
|
||||||
|
return sprintf('<info>%s</info>', $string);
|
||||||
|
}
|
||||||
|
}
|
45
module/CLI/src/Command/Config/GenerateSecretCommand.php
Normal file
45
module/CLI/src/Command/Config/GenerateSecretCommand.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Config;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
|
||||||
|
class GenerateSecretCommand extends Command
|
||||||
|
{
|
||||||
|
use StringUtilsTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
private $translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GenerateCharsetCommand constructor.
|
||||||
|
* @param TranslatorInterface $translator
|
||||||
|
*
|
||||||
|
* @Inject({"translator"})
|
||||||
|
*/
|
||||||
|
public function __construct(TranslatorInterface $translator)
|
||||||
|
{
|
||||||
|
$this->translator = $translator;
|
||||||
|
parent::__construct(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure()
|
||||||
|
{
|
||||||
|
$this->setName('config:generate-secret')
|
||||||
|
->setDescription($this->translator->translate(
|
||||||
|
'Generates a random secret string that can be used for JWT token encryption'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$secret = $this->generateRandomString(32);
|
||||||
|
$output->writeln($this->translator->translate('Secret key:') . sprintf(' <info>%s</info>', $secret));
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Shlinkio\Shlink\CLI\Command;
|
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
@ -31,7 +31,7 @@ class GenerateShortcodeCommand extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GenerateShortcodeCommand constructor.
|
* GenerateShortcodeCommand constructor.
|
||||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
* @param UrlShortenerInterface $urlShortener
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
* @param array $domainConfig
|
* @param array $domainConfig
|
||||||
*
|
*
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Shlinkio\Shlink\CLI\Command;
|
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
@ -28,7 +28,7 @@ class GetVisitsCommand extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GetVisitsCommand constructor.
|
* GetVisitsCommand constructor.
|
||||||
* @param VisitsTrackerInterface|VisitsTracker $visitsTracker
|
* @param VisitsTrackerInterface $visitsTracker
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
*
|
*
|
||||||
* @Inject({VisitsTracker::class, "translator"})
|
* @Inject({VisitsTracker::class, "translator"})
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Shlinkio\Shlink\CLI\Command;
|
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||||
@ -30,7 +30,7 @@ class ListShortcodesCommand extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ListShortcodesCommand constructor.
|
* ListShortcodesCommand constructor.
|
||||||
* @param ShortUrlServiceInterface|ShortUrlService $shortUrlService
|
* @param ShortUrlServiceInterface $shortUrlService
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
*
|
*
|
||||||
* @Inject({ShortUrlService::class, "translator"})
|
* @Inject({ShortUrlService::class, "translator"})
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Shlinkio\Shlink\CLI\Command;
|
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
@ -26,7 +26,7 @@ class ResolveUrlCommand extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ResolveUrlCommand constructor.
|
* ResolveUrlCommand constructor.
|
||||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
* @param UrlShortenerInterface $urlShortener
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
*
|
*
|
||||||
* @Inject({UrlShortener::class, "translator"})
|
* @Inject({UrlShortener::class, "translator"})
|
@ -1,5 +1,5 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Shlinkio\Shlink\CLI\Command;
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
use Shlinkio\Shlink\Common\Exception\WrongIpException;
|
||||||
@ -32,8 +32,8 @@ class ProcessVisitsCommand extends Command
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* ProcessVisitsCommand constructor.
|
* ProcessVisitsCommand constructor.
|
||||||
* @param VisitServiceInterface|VisitService $visitService
|
* @param VisitServiceInterface $visitService
|
||||||
* @param IpLocationResolverInterface|IpLocationResolver $ipLocationResolver
|
* @param IpLocationResolverInterface $ipLocationResolver
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
*
|
*
|
||||||
* @Inject({VisitService::class, IpLocationResolver::class, "translator"})
|
* @Inject({VisitService::class, IpLocationResolver::class, "translator"})
|
62
module/CLI/test/Command/Api/DisableKeyCommandTest.php
Normal file
62
module/CLI/test/Command/Api/DisableKeyCommandTest.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
use Zend\I18n\Translator\Translator;
|
||||||
|
|
||||||
|
class DisableKeyCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var CommandTester
|
||||||
|
*/
|
||||||
|
protected $commandTester;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $apiKeyService;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||||
|
$command = new DisableKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||||
|
$app = new Application();
|
||||||
|
$app->add($command);
|
||||||
|
$this->commandTester = new CommandTester($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function providedApiKeyIsDisabled()
|
||||||
|
{
|
||||||
|
$apiKey = 'abcd1234';
|
||||||
|
$this->apiKeyService->disable($apiKey)->shouldBeCalledTimes(1);
|
||||||
|
$this->commandTester->execute([
|
||||||
|
'command' => 'api-key:disable',
|
||||||
|
'apiKey' => $apiKey,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function errorIsReturnedIfServiceThrowsException()
|
||||||
|
{
|
||||||
|
$apiKey = 'abcd1234';
|
||||||
|
$this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$this->commandTester->execute([
|
||||||
|
'command' => 'api-key:disable',
|
||||||
|
'apiKey' => $apiKey,
|
||||||
|
]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
$this->assertEquals('API key "abcd1234" does not exist.' . PHP_EOL, $output);
|
||||||
|
}
|
||||||
|
}
|
55
module/CLI/test/Command/Api/GenerateKeyCommandTest.php
Normal file
55
module/CLI/test/Command/Api/GenerateKeyCommandTest.php
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
use Zend\I18n\Translator\Translator;
|
||||||
|
|
||||||
|
class GenerateKeyCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var CommandTester
|
||||||
|
*/
|
||||||
|
protected $commandTester;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $apiKeyService;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||||
|
$command = new GenerateKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||||
|
$app = new Application();
|
||||||
|
$app->add($command);
|
||||||
|
$this->commandTester = new CommandTester($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function noExpirationDateIsDefinedIfNotProvided()
|
||||||
|
{
|
||||||
|
$this->apiKeyService->create(null)->shouldBeCalledTimes(1);
|
||||||
|
$this->commandTester->execute([
|
||||||
|
'command' => 'api-key:generate',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function expirationDateIsDefinedIfWhenProvided()
|
||||||
|
{
|
||||||
|
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1);
|
||||||
|
$this->commandTester->execute([
|
||||||
|
'command' => 'api-key:generate',
|
||||||
|
'--expirationDate' => '2016-01-01',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
62
module/CLI/test/Command/Api/ListKeysCommandTest.php
Normal file
62
module/CLI/test/Command/Api/ListKeysCommandTest.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Api;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
use Zend\I18n\Translator\Translator;
|
||||||
|
|
||||||
|
class ListKeysCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var CommandTester
|
||||||
|
*/
|
||||||
|
protected $commandTester;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $apiKeyService;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||||
|
$command = new ListKeysCommand($this->apiKeyService->reveal(), Translator::factory([]));
|
||||||
|
$app = new Application();
|
||||||
|
$app->add($command);
|
||||||
|
$this->commandTester = new CommandTester($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function ifEnabledOnlyIsNotProvidedEverythingIsListed()
|
||||||
|
{
|
||||||
|
$this->apiKeyService->listKeys(false)->willReturn([
|
||||||
|
new ApiKey(),
|
||||||
|
new ApiKey(),
|
||||||
|
new ApiKey(),
|
||||||
|
])->shouldBeCalledTimes(1);
|
||||||
|
$this->commandTester->execute([
|
||||||
|
'command' => 'api-key:list',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function ifEnabledOnlyIsProvidedOnlyThoseKeysAreListed()
|
||||||
|
{
|
||||||
|
$this->apiKeyService->listKeys(true)->willReturn([
|
||||||
|
new ApiKey(),
|
||||||
|
new ApiKey(),
|
||||||
|
])->shouldBeCalledTimes(1);
|
||||||
|
$this->commandTester->execute([
|
||||||
|
'command' => 'api-key:list',
|
||||||
|
'--enabledOnly' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
@ -4,7 +4,7 @@ namespace ShlinkioTest\Shlink\CLI\Command;
|
|||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\GenerateShortcodeCommand;
|
use Shlinkio\Shlink\CLI\Command\Shortcode\GenerateShortcodeCommand;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
|
@ -4,7 +4,7 @@ namespace ShlinkioTest\Shlink\CLI\Command;
|
|||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\GetVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Shortcode\GetVisitsCommand;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||||
|
@ -4,7 +4,7 @@ namespace ShlinkioTest\Shlink\CLI\Command;
|
|||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\ListShortcodesCommand;
|
use Shlinkio\Shlink\CLI\Command\Shortcode\ListShortcodesCommand;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
|
@ -4,7 +4,7 @@ namespace ShlinkioTest\Shlink\CLI\Command;
|
|||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\ProcessVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
|
||||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitService;
|
use Shlinkio\Shlink\Core\Service\VisitService;
|
||||||
|
@ -3,7 +3,7 @@ namespace ShlinkioTest\Shlink\CLI\Command;
|
|||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\ResolveUrlCommand;
|
use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
|
@ -2,9 +2,12 @@
|
|||||||
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
||||||
use Doctrine\Common\Cache\Cache;
|
use Doctrine\Common\Cache\Cache;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use Monolog\Logger;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler;
|
use Shlinkio\Shlink\Common\ErrorHandler;
|
||||||
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
||||||
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
|
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
|
||||||
|
use Shlinkio\Shlink\Common\Factory\LoggerFactory;
|
||||||
use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
|
use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
|
||||||
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
||||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
||||||
@ -19,11 +22,15 @@ return [
|
|||||||
EntityManager::class => EntityManagerFactory::class,
|
EntityManager::class => EntityManagerFactory::class,
|
||||||
GuzzleHttp\Client::class => InvokableFactory::class,
|
GuzzleHttp\Client::class => InvokableFactory::class,
|
||||||
Cache::class => CacheFactory::class,
|
Cache::class => CacheFactory::class,
|
||||||
IpLocationResolver::class => AnnotatedFactory::class,
|
LoggerInterface::class => LoggerFactory::class,
|
||||||
|
'Logger_Shlink' => LoggerFactory::class,
|
||||||
|
|
||||||
Translator::class => TranslatorFactory::class,
|
Translator::class => TranslatorFactory::class,
|
||||||
TranslatorExtension::class => AnnotatedFactory::class,
|
TranslatorExtension::class => AnnotatedFactory::class,
|
||||||
LocaleMiddleware::class => AnnotatedFactory::class,
|
LocaleMiddleware::class => AnnotatedFactory::class,
|
||||||
|
|
||||||
|
IpLocationResolver::class => AnnotatedFactory::class,
|
||||||
|
|
||||||
ErrorHandler\ContentBasedErrorHandler::class => AnnotatedFactory::class,
|
ErrorHandler\ContentBasedErrorHandler::class => AnnotatedFactory::class,
|
||||||
ErrorHandler\ErrorHandlerManager::class => ErrorHandler\ErrorHandlerManagerFactory::class,
|
ErrorHandler\ErrorHandlerManager::class => ErrorHandler\ErrorHandlerManagerFactory::class,
|
||||||
],
|
],
|
||||||
@ -31,6 +38,8 @@ return [
|
|||||||
'em' => EntityManager::class,
|
'em' => EntityManager::class,
|
||||||
'httpClient' => GuzzleHttp\Client::class,
|
'httpClient' => GuzzleHttp\Client::class,
|
||||||
'translator' => Translator::class,
|
'translator' => Translator::class,
|
||||||
|
'logger' => LoggerInterface::class,
|
||||||
|
Logger::class => LoggerInterface::class,
|
||||||
AnnotatedFactory::CACHE_SERVICE => Cache::class,
|
AnnotatedFactory::CACHE_SERVICE => Cache::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Shlinkio\Shlink\Common\Factory;
|
namespace Shlinkio\Shlink\Common\Factory;
|
||||||
|
|
||||||
use Doctrine\Common\Cache\ApcuCache;
|
use Doctrine\Common\Cache;
|
||||||
use Doctrine\Common\Cache\ArrayCache;
|
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
use Interop\Container\Exception\ContainerException;
|
use Interop\Container\Exception\ContainerException;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
@ -12,8 +11,11 @@ use Zend\ServiceManager\Factory\FactoryInterface;
|
|||||||
class CacheFactory implements FactoryInterface
|
class CacheFactory implements FactoryInterface
|
||||||
{
|
{
|
||||||
const VALID_CACHE_ADAPTERS = [
|
const VALID_CACHE_ADAPTERS = [
|
||||||
ApcuCache::class,
|
Cache\ApcuCache::class,
|
||||||
ArrayCache::class,
|
Cache\ArrayCache::class,
|
||||||
|
Cache\FilesystemCache::class,
|
||||||
|
Cache\PhpFileCache::class,
|
||||||
|
Cache\MemcachedCache::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,10 +38,44 @@ class CacheFactory implements FactoryInterface
|
|||||||
&& isset($config['cache']['adapter'])
|
&& isset($config['cache']['adapter'])
|
||||||
&& in_array($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS)
|
&& in_array($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS)
|
||||||
) {
|
) {
|
||||||
return new $config['cache']['adapter']();
|
return $this->resolveCacheAdapter($config['cache']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the adapter has not been set in config, create one based on environment
|
// If the adapter has not been set in config, create one based on environment
|
||||||
return env('APP_ENV', 'pro') === 'pro' ? new ApcuCache() : new ArrayCache();
|
return env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $cacheConfig
|
||||||
|
* @return Cache\Cache
|
||||||
|
*/
|
||||||
|
protected function resolveCacheAdapter(array $cacheConfig)
|
||||||
|
{
|
||||||
|
switch ($cacheConfig['adapter']) {
|
||||||
|
case Cache\ArrayCache::class:
|
||||||
|
case Cache\ApcuCache::class:
|
||||||
|
return new $cacheConfig['adapter']();
|
||||||
|
case Cache\FilesystemCache::class:
|
||||||
|
case Cache\PhpFileCache::class:
|
||||||
|
return new $cacheConfig['adapter']($cacheConfig['options']['dir']);
|
||||||
|
case Cache\MemcachedCache::class:
|
||||||
|
$memcached = new \Memcached();
|
||||||
|
$servers = isset($cacheConfig['options']['servers']) ? $cacheConfig['options']['servers'] : [];
|
||||||
|
|
||||||
|
foreach ($servers as $server) {
|
||||||
|
if (! isset($server['host'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$port = isset($server['port']) ? intval($server['port']) : 11211;
|
||||||
|
|
||||||
|
$memcached->addServer($server['host'], $port);
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache = new Cache\MemcachedCache();
|
||||||
|
$cache->setMemcached($memcached);
|
||||||
|
return $cache;
|
||||||
|
default:
|
||||||
|
return new Cache\ArrayCache();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,12 +30,14 @@ class EntityManagerFactory implements FactoryInterface
|
|||||||
$globalConfig = $container->get('config');
|
$globalConfig = $container->get('config');
|
||||||
$isDevMode = isset($globalConfig['debug']) ? ((bool) $globalConfig['debug']) : false;
|
$isDevMode = isset($globalConfig['debug']) ? ((bool) $globalConfig['debug']) : false;
|
||||||
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
|
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
|
||||||
$dbConfig = isset($globalConfig['database']) ? $globalConfig['database'] : [];
|
$emConfig = isset($globalConfig['entity_manager']) ? $globalConfig['entity_manager'] : [];
|
||||||
|
$connecitonConfig = isset($emConfig['connection']) ? $emConfig['connection'] : [];
|
||||||
|
$ormConfig = isset($emConfig['orm']) ? $emConfig['orm'] : [];
|
||||||
|
|
||||||
return EntityManager::create($dbConfig, Setup::createAnnotationMetadataConfiguration(
|
return EntityManager::create($connecitonConfig, Setup::createAnnotationMetadataConfiguration(
|
||||||
['module/Core/src/Entity'],
|
isset($ormConfig['entities_paths']) ? $ormConfig['entities_paths'] : [],
|
||||||
$isDevMode,
|
$isDevMode,
|
||||||
'data/proxies',
|
isset($ormConfig['proxies_dir']) ? $ormConfig['proxies_dir'] : null,
|
||||||
$cache,
|
$cache,
|
||||||
false
|
false
|
||||||
));
|
));
|
||||||
|
39
module/Common/src/Factory/LoggerFactory.php
Normal file
39
module/Common/src/Factory/LoggerFactory.php
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Common\Factory;
|
||||||
|
|
||||||
|
use Cascade\Cascade;
|
||||||
|
use Interop\Container\ContainerInterface;
|
||||||
|
use Interop\Container\Exception\ContainerException;
|
||||||
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
|
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||||
|
|
||||||
|
class LoggerFactory implements FactoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create an object
|
||||||
|
*
|
||||||
|
* @param ContainerInterface $container
|
||||||
|
* @param string $requestedName
|
||||||
|
* @param null|array $options
|
||||||
|
* @return object
|
||||||
|
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||||
|
* @throws ServiceNotCreatedException if an exception is raised when
|
||||||
|
* creating a service.
|
||||||
|
* @throws ContainerException if any other error occurs
|
||||||
|
*/
|
||||||
|
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||||
|
{
|
||||||
|
$config = $container->has('config') ? $container->get('config') : [];
|
||||||
|
Cascade::fileConfig(isset($config['logger']) ? $config['logger'] : ['loggers' => []]);
|
||||||
|
|
||||||
|
// Compose requested logger name
|
||||||
|
$loggerName = isset($options) & isset($options['logger_name']) ? $options['logger_name'] : 'Logger';
|
||||||
|
$nameParts = explode('_', $requestedName);
|
||||||
|
if (count($nameParts) > 1) {
|
||||||
|
$loggerName = $nameParts[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return Cascade::getLogger($loggerName);
|
||||||
|
}
|
||||||
|
}
|
35
module/Common/src/Response/QrCodeResponse.php
Normal file
35
module/Common/src/Response/QrCodeResponse.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Common\Response;
|
||||||
|
|
||||||
|
use Endroid\QrCode\QrCode;
|
||||||
|
use Psr\Http\Message\StreamInterface;
|
||||||
|
use Zend\Diactoros\Response;
|
||||||
|
use Zend\Diactoros\Stream;
|
||||||
|
|
||||||
|
class QrCodeResponse extends Response
|
||||||
|
{
|
||||||
|
use Response\InjectContentTypeTrait;
|
||||||
|
|
||||||
|
public function __construct(QrCode $qrCode, $status = 200, array $headers = [])
|
||||||
|
{
|
||||||
|
parent::__construct(
|
||||||
|
$this->createBody($qrCode),
|
||||||
|
$status,
|
||||||
|
$this->injectContentType($qrCode->getContentType(), $headers)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the message body.
|
||||||
|
*
|
||||||
|
* @param QrCode $qrCode
|
||||||
|
* @return StreamInterface
|
||||||
|
*/
|
||||||
|
private function createBody(QrCode $qrCode)
|
||||||
|
{
|
||||||
|
$body = new Stream('php://temp', 'wb+');
|
||||||
|
$body->write($qrCode->get());
|
||||||
|
$body->rewind();
|
||||||
|
return $body;
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,8 @@ namespace ShlinkioTest\Shlink\Common\Factory;
|
|||||||
use Doctrine\Common\Cache\ApcuCache;
|
use Doctrine\Common\Cache\ApcuCache;
|
||||||
use Doctrine\Common\Cache\ArrayCache;
|
use Doctrine\Common\Cache\ArrayCache;
|
||||||
use Doctrine\Common\Cache\FilesystemCache;
|
use Doctrine\Common\Cache\FilesystemCache;
|
||||||
|
use Doctrine\Common\Cache\MemcachedCache;
|
||||||
|
use Doctrine\Common\Cache\RedisCache;
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
||||||
use Zend\ServiceManager\ServiceManager;
|
use Zend\ServiceManager\ServiceManager;
|
||||||
@ -61,15 +63,51 @@ class CacheFactoryTest extends TestCase
|
|||||||
public function invalidAdapterDefinedInConfigFallbacksToEnvironment()
|
public function invalidAdapterDefinedInConfigFallbacksToEnvironment()
|
||||||
{
|
{
|
||||||
putenv('APP_ENV=pro');
|
putenv('APP_ENV=pro');
|
||||||
$instance = $this->factory->__invoke($this->createSM(FilesystemCache::class), '');
|
$instance = $this->factory->__invoke($this->createSM(RedisCache::class), '');
|
||||||
$this->assertInstanceOf(ApcuCache::class, $instance);
|
$this->assertInstanceOf(ApcuCache::class, $instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function createSM($cacheAdapter = null)
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function filesystemCacheAdaptersReadDirOption()
|
||||||
|
{
|
||||||
|
$dir = sys_get_temp_dir();
|
||||||
|
/** @var FilesystemCache $instance */
|
||||||
|
$instance = $this->factory->__invoke($this->createSM(FilesystemCache::class, ['dir' => $dir]), '');
|
||||||
|
$this->assertInstanceOf(FilesystemCache::class, $instance);
|
||||||
|
$this->assertEquals($dir, $instance->getDirectory());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function memcachedCacheAdaptersReadServersOption()
|
||||||
|
{
|
||||||
|
$servers = [
|
||||||
|
[
|
||||||
|
'host' => '1.2.3.4',
|
||||||
|
'port' => 123
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'host' => '4.3.2.1',
|
||||||
|
'port' => 321
|
||||||
|
],
|
||||||
|
];
|
||||||
|
/** @var MemcachedCache $instance */
|
||||||
|
$instance = $this->factory->__invoke($this->createSM(MemcachedCache::class, ['servers' => $servers]), '');
|
||||||
|
$this->assertInstanceOf(MemcachedCache::class, $instance);
|
||||||
|
$this->assertEquals(count($servers), count($instance->getMemcached()->getServerList()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createSM($cacheAdapter = null, array $options = [])
|
||||||
{
|
{
|
||||||
return new ServiceManager(['services' => [
|
return new ServiceManager(['services' => [
|
||||||
'config' => isset($cacheAdapter) ? [
|
'config' => isset($cacheAdapter) ? [
|
||||||
'cache' => ['adapter' => $cacheAdapter],
|
'cache' => [
|
||||||
|
'adapter' => $cacheAdapter,
|
||||||
|
'options' => $options,
|
||||||
|
],
|
||||||
] : [],
|
] : [],
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
|
@ -26,8 +26,10 @@ class EntityManagerFactoryTest extends TestCase
|
|||||||
$sm = new ServiceManager(['services' => [
|
$sm = new ServiceManager(['services' => [
|
||||||
'config' => [
|
'config' => [
|
||||||
'debug' => true,
|
'debug' => true,
|
||||||
'database' => [
|
'entity_manager' => [
|
||||||
'driver' => 'pdo_sqlite',
|
'connection' => [
|
||||||
|
'driver' => 'pdo_sqlite',
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
]]);
|
]]);
|
||||||
|
54
module/Common/test/Factory/LoggerFactoryTest.php
Normal file
54
module/Common/test/Factory/LoggerFactoryTest.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Common\Factory;
|
||||||
|
|
||||||
|
use Monolog\Logger;
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Shlinkio\Shlink\Common\Factory\LoggerFactory;
|
||||||
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
|
class LoggerFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var LoggerFactory
|
||||||
|
*/
|
||||||
|
protected $factory;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->factory = new LoggerFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function serviceIsCreated()
|
||||||
|
{
|
||||||
|
/** @var Logger $instance */
|
||||||
|
$instance = $this->factory->__invoke(new ServiceManager(), '');
|
||||||
|
$this->assertInstanceOf(LoggerInterface::class, $instance);
|
||||||
|
$this->assertEquals('Logger', $instance->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function nameIsSetFromOptions()
|
||||||
|
{
|
||||||
|
/** @var Logger $instance */
|
||||||
|
$instance = $this->factory->__invoke(new ServiceManager(), '', ['logger_name' => 'Foo']);
|
||||||
|
$this->assertInstanceOf(LoggerInterface::class, $instance);
|
||||||
|
$this->assertEquals('Foo', $instance->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function serviceNameOverwritesOptionsLoggerName()
|
||||||
|
{
|
||||||
|
/** @var Logger $instance */
|
||||||
|
$instance = $this->factory->__invoke(new ServiceManager(), 'Logger_Shlink', ['logger_name' => 'Foo']);
|
||||||
|
$this->assertInstanceOf(LoggerInterface::class, $instance);
|
||||||
|
$this->assertEquals('Shlink', $instance->getName());
|
||||||
|
}
|
||||||
|
}
|
6
module/Core/config/app_options.config.php
Normal file
6
module/Core/config/app_options.config.php
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
|
||||||
|
'app_options' => [],
|
||||||
|
|
||||||
|
];
|
@ -1,12 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
||||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
use Shlinkio\Shlink\Core\Action;
|
||||||
|
use Shlinkio\Shlink\Core\Middleware;
|
||||||
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
use Shlinkio\Shlink\Core\Service;
|
use Shlinkio\Shlink\Core\Service;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
|
AppOptions::class => AnnotatedFactory::class,
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
Service\UrlShortener::class => AnnotatedFactory::class,
|
Service\UrlShortener::class => AnnotatedFactory::class,
|
||||||
Service\VisitsTracker::class => AnnotatedFactory::class,
|
Service\VisitsTracker::class => AnnotatedFactory::class,
|
||||||
@ -14,7 +18,9 @@ return [
|
|||||||
Service\VisitService::class => AnnotatedFactory::class,
|
Service\VisitService::class => AnnotatedFactory::class,
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
RedirectAction::class => AnnotatedFactory::class,
|
Action\RedirectAction::class => AnnotatedFactory::class,
|
||||||
|
Action\QrCodeAction::class => AnnotatedFactory::class,
|
||||||
|
Middleware\QrCodeCacheMiddleware::class => AnnotatedFactory::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
12
module/Core/config/entity-manager.config.php
Normal file
12
module/Core/config/entity-manager.config.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
|
||||||
|
'entity_manager' => [
|
||||||
|
'orm' => [
|
||||||
|
'entities_paths' => [
|
||||||
|
__DIR__ . '/../src/Entity',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
@ -1,5 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
use Shlinkio\Shlink\Core\Action;
|
||||||
|
use Shlinkio\Shlink\Core\Middleware;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@ -7,7 +8,16 @@ return [
|
|||||||
[
|
[
|
||||||
'name' => 'long-url-redirect',
|
'name' => 'long-url-redirect',
|
||||||
'path' => '/{shortCode}',
|
'path' => '/{shortCode}',
|
||||||
'middleware' => RedirectAction::class,
|
'middleware' => Action\RedirectAction::class,
|
||||||
|
'allowed_methods' => ['GET'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'short-url-qr-code',
|
||||||
|
'path' => '/qr/{shortCode}[/{size:[0-9]+}]',
|
||||||
|
'middleware' => [
|
||||||
|
Middleware\QrCodeCacheMiddleware::class,
|
||||||
|
Action\QrCodeAction::class,
|
||||||
|
],
|
||||||
'allowed_methods' => ['GET'],
|
'allowed_methods' => ['GET'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
113
module/Core/src/Action/QrCodeAction.php
Normal file
113
module/Core/src/Action/QrCodeAction.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Core\Action;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Endroid\QrCode\QrCode;
|
||||||
|
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\Response\QrCodeResponse;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
|
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||||
|
use Zend\Expressive\Router\RouterInterface;
|
||||||
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
|
||||||
|
class QrCodeAction implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var RouterInterface
|
||||||
|
*/
|
||||||
|
private $router;
|
||||||
|
/**
|
||||||
|
* @var UrlShortenerInterface
|
||||||
|
*/
|
||||||
|
private $urlShortener;
|
||||||
|
/**
|
||||||
|
* @var LoggerInterface
|
||||||
|
*/
|
||||||
|
private $logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QrCodeAction constructor.
|
||||||
|
* @param RouterInterface $router
|
||||||
|
* @param UrlShortenerInterface $urlShortener
|
||||||
|
* @param LoggerInterface $logger
|
||||||
|
*
|
||||||
|
* @Inject({RouterInterface::class, UrlShortener::class, "Logger_Shlink"})
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
RouterInterface $router,
|
||||||
|
UrlShortenerInterface $urlShortener,
|
||||||
|
LoggerInterface $logger = null
|
||||||
|
) {
|
||||||
|
$this->router = $router;
|
||||||
|
$this->urlShortener = $urlShortener;
|
||||||
|
$this->logger = $logger ?: new NullLogger();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process an incoming request and/or response.
|
||||||
|
*
|
||||||
|
* Accepts a server-side request and a response instance, and does
|
||||||
|
* something with them.
|
||||||
|
*
|
||||||
|
* If the response is not complete and/or further processing would not
|
||||||
|
* interfere with the work done in the middleware, or if the middleware
|
||||||
|
* wants to delegate to another process, it can use the `$out` callable
|
||||||
|
* if present.
|
||||||
|
*
|
||||||
|
* If the middleware does not return a value, execution of the current
|
||||||
|
* request is considered complete, and the response instance provided will
|
||||||
|
* be considered the response to return.
|
||||||
|
*
|
||||||
|
* Alternately, the middleware may return a response instance.
|
||||||
|
*
|
||||||
|
* Often, middleware will `return $out();`, with the assumption that a
|
||||||
|
* later middleware will return a response.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param null|callable $out
|
||||||
|
* @return null|Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
|
{
|
||||||
|
// Make sure the short URL exists for this short code
|
||||||
|
$shortCode = $request->getAttribute('shortCode');
|
||||||
|
try {
|
||||||
|
$shortUrl = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||||
|
if (! isset($shortUrl)) {
|
||||||
|
return $out($request, $response->withStatus(404), 'Not Found');
|
||||||
|
}
|
||||||
|
} catch (InvalidShortCodeException $e) {
|
||||||
|
$this->logger->warning('Tried to create a QR code with an invalid short code' . PHP_EOL . $e);
|
||||||
|
return $out($request, $response->withStatus(404), 'Not Found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = $this->router->generateUri('long-url-redirect', ['shortCode' => $shortCode]);
|
||||||
|
$size = $this->getSizeParam($request);
|
||||||
|
|
||||||
|
$qrCode = new QrCode($request->getUri()->withPath($path)->withQuery(''));
|
||||||
|
$qrCode->setSize($size)
|
||||||
|
->setPadding(0);
|
||||||
|
return new QrCodeResponse($qrCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
protected function getSizeParam(Request $request)
|
||||||
|
{
|
||||||
|
$size = intval($request->getAttribute('size', 300));
|
||||||
|
if ($size < 50) {
|
||||||
|
return 50;
|
||||||
|
} elseif ($size > 1000) {
|
||||||
|
return 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $size;
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,8 @@ namespace Shlinkio\Shlink\Core\Action;
|
|||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
||||||
@ -18,21 +20,30 @@ class RedirectAction implements MiddlewareInterface
|
|||||||
*/
|
*/
|
||||||
private $urlShortener;
|
private $urlShortener;
|
||||||
/**
|
/**
|
||||||
* @var VisitsTracker|VisitsTrackerInterface
|
* @var VisitsTrackerInterface
|
||||||
*/
|
*/
|
||||||
private $visitTracker;
|
private $visitTracker;
|
||||||
|
/**
|
||||||
|
* @var null|LoggerInterface
|
||||||
|
*/
|
||||||
|
private $logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RedirectMiddleware constructor.
|
* RedirectMiddleware constructor.
|
||||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
* @param UrlShortenerInterface $urlShortener
|
||||||
* @param VisitsTrackerInterface|VisitsTracker $visitTracker
|
* @param VisitsTrackerInterface $visitTracker
|
||||||
|
* @param LoggerInterface|null $logger
|
||||||
*
|
*
|
||||||
* @Inject({UrlShortener::class, VisitsTracker::class})
|
* @Inject({UrlShortener::class, VisitsTracker::class, "Logger_Shlink"})
|
||||||
*/
|
*/
|
||||||
public function __construct(UrlShortenerInterface $urlShortener, VisitsTrackerInterface $visitTracker)
|
public function __construct(
|
||||||
{
|
UrlShortenerInterface $urlShortener,
|
||||||
|
VisitsTrackerInterface $visitTracker,
|
||||||
|
LoggerInterface $logger = null
|
||||||
|
) {
|
||||||
$this->urlShortener = $urlShortener;
|
$this->urlShortener = $urlShortener;
|
||||||
$this->visitTracker = $visitTracker;
|
$this->visitTracker = $visitTracker;
|
||||||
|
$this->logger = $logger ?: new NullLogger();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,13 +85,14 @@ class RedirectAction implements MiddlewareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Track visit to this short code
|
// Track visit to this short code
|
||||||
$this->visitTracker->track($shortCode);
|
$this->visitTracker->track($shortCode, $request);
|
||||||
|
|
||||||
// Return a redirect response to the long URL.
|
// Return a redirect response to the long URL.
|
||||||
// Use a temporary redirect to make sure browsers always hit the server for analytics purposes
|
// Use a temporary redirect to make sure browsers always hit the server for analytics purposes
|
||||||
return new RedirectResponse($longUrl);
|
return new RedirectResponse($longUrl);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
// In case of error, dispatch 404 error
|
// In case of error, dispatch 404 error
|
||||||
|
$this->logger->error('Error redirecting to long URL.' . PHP_EOL . $e);
|
||||||
return $this->notFoundResponse($request, $response, $out);
|
return $this->notFoundResponse($request, $response, $out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,103 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Shlinkio\Shlink\Core\Entity;
|
|
||||||
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
|
||||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
|
||||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class RestToken
|
|
||||||
* @author
|
|
||||||
* @link
|
|
||||||
*
|
|
||||||
* @ORM\Entity()
|
|
||||||
* @ORM\Table(name="rest_tokens")
|
|
||||||
*/
|
|
||||||
class RestToken extends AbstractEntity
|
|
||||||
{
|
|
||||||
use StringUtilsTrait;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default interval is 20 minutes
|
|
||||||
*/
|
|
||||||
const DEFAULT_INTERVAL = 'PT20M';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var \DateTime
|
|
||||||
* @ORM\Column(type="datetime", name="expiration_date", nullable=false)
|
|
||||||
*/
|
|
||||||
protected $expirationDate;
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
* @ORM\Column(nullable=false)
|
|
||||||
*/
|
|
||||||
protected $token;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->updateExpiration();
|
|
||||||
$this->setRandomTokenKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return \DateTime
|
|
||||||
*/
|
|
||||||
public function getExpirationDate()
|
|
||||||
{
|
|
||||||
return $this->expirationDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \DateTime $expirationDate
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setExpirationDate($expirationDate)
|
|
||||||
{
|
|
||||||
$this->expirationDate = $expirationDate;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getToken()
|
|
||||||
{
|
|
||||||
return $this->token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $token
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setToken($token)
|
|
||||||
{
|
|
||||||
$this->token = $token;
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isExpired()
|
|
||||||
{
|
|
||||||
return new \DateTime() > $this->expirationDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the expiration of the token, setting it to the default interval in the future
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function updateExpiration()
|
|
||||||
{
|
|
||||||
return $this->setExpirationDate((new \DateTime())->add(new \DateInterval(self::DEFAULT_INTERVAL)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets a random unique token key for this RestToken
|
|
||||||
* @return RestToken
|
|
||||||
*/
|
|
||||||
public function setRandomTokenKey()
|
|
||||||
{
|
|
||||||
return $this->setToken($this->generateV4Uuid());
|
|
||||||
}
|
|
||||||
}
|
|
73
module/Core/src/Middleware/QrCodeCacheMiddleware.php
Normal file
73
module/Core/src/Middleware/QrCodeCacheMiddleware.php
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Core\Middleware;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Doctrine\Common\Cache\Cache;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
|
||||||
|
class QrCodeCacheMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Cache
|
||||||
|
*/
|
||||||
|
private $cache;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QrCodeCacheMiddleware constructor.
|
||||||
|
* @param Cache $cache
|
||||||
|
*
|
||||||
|
* @Inject({Cache::class})
|
||||||
|
*/
|
||||||
|
public function __construct(Cache $cache)
|
||||||
|
{
|
||||||
|
$this->cache = $cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process an incoming request and/or response.
|
||||||
|
*
|
||||||
|
* Accepts a server-side request and a response instance, and does
|
||||||
|
* something with them.
|
||||||
|
*
|
||||||
|
* If the response is not complete and/or further processing would not
|
||||||
|
* interfere with the work done in the middleware, or if the middleware
|
||||||
|
* wants to delegate to another process, it can use the `$out` callable
|
||||||
|
* if present.
|
||||||
|
*
|
||||||
|
* If the middleware does not return a value, execution of the current
|
||||||
|
* request is considered complete, and the response instance provided will
|
||||||
|
* be considered the response to return.
|
||||||
|
*
|
||||||
|
* Alternately, the middleware may return a response instance.
|
||||||
|
*
|
||||||
|
* Often, middleware will `return $out();`, with the assumption that a
|
||||||
|
* later middleware will return a response.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param null|callable $out
|
||||||
|
* @return null|Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
|
{
|
||||||
|
$cacheKey = $request->getUri()->getPath();
|
||||||
|
|
||||||
|
// If this QR code is already cached, just return it
|
||||||
|
if ($this->cache->contains($cacheKey)) {
|
||||||
|
$qrData = $this->cache->fetch($cacheKey);
|
||||||
|
$response->getBody()->write($qrData['body']);
|
||||||
|
return $response->withHeader('Content-Type', $qrData['content-type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not, call the next middleware and cache it
|
||||||
|
/** @var Response $resp */
|
||||||
|
$resp = $out($request, $response);
|
||||||
|
$this->cache->save($cacheKey, [
|
||||||
|
'body' => $resp->getBody()->__toString(),
|
||||||
|
'content-type' => $resp->getHeaderLine('Content-Type'),
|
||||||
|
]);
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
}
|
97
module/Core/src/Options/AppOptions.php
Normal file
97
module/Core/src/Options/AppOptions.php
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Core\Options;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||||
|
use Zend\Stdlib\AbstractOptions;
|
||||||
|
|
||||||
|
class AppOptions extends AbstractOptions
|
||||||
|
{
|
||||||
|
use StringUtilsTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $name = '';
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $version = '1.0';
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $secretKey = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AppOptions constructor.
|
||||||
|
* @param array|null|\Traversable $options
|
||||||
|
*
|
||||||
|
* @Inject({"config.app_options"})
|
||||||
|
*/
|
||||||
|
public function __construct($options = null)
|
||||||
|
{
|
||||||
|
parent::__construct($options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
protected function setName($name)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getVersion()
|
||||||
|
{
|
||||||
|
return $this->version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $version
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
protected function setVersion($version)
|
||||||
|
{
|
||||||
|
$this->version = $version;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function getSecretKey()
|
||||||
|
{
|
||||||
|
return $this->secretKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $secretKey
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
protected function setSecretKey($secretKey)
|
||||||
|
{
|
||||||
|
$this->secretKey = $secretKey;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return sprintf('%s:v%s', $this->name, $this->version);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
namespace Shlinkio\Shlink\Core\Service;
|
namespace Shlinkio\Shlink\Core\Service;
|
||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Doctrine\Common\Cache\Cache;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Doctrine\ORM\ORMException;
|
use Doctrine\ORM\ORMException;
|
||||||
use GuzzleHttp\ClientInterface;
|
use GuzzleHttp\ClientInterface;
|
||||||
@ -28,23 +29,30 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
private $chars;
|
private $chars;
|
||||||
|
/**
|
||||||
|
* @var Cache
|
||||||
|
*/
|
||||||
|
private $cache;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UrlShortener constructor.
|
* UrlShortener constructor.
|
||||||
* @param ClientInterface $httpClient
|
* @param ClientInterface $httpClient
|
||||||
* @param EntityManagerInterface $em
|
* @param EntityManagerInterface $em
|
||||||
|
* @param Cache $cache
|
||||||
* @param string $chars
|
* @param string $chars
|
||||||
*
|
*
|
||||||
* @Inject({"httpClient", "em", "config.url_shortener.shortcode_chars"})
|
* @Inject({"httpClient", "em", Cache::class, "config.url_shortener.shortcode_chars"})
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
ClientInterface $httpClient,
|
ClientInterface $httpClient,
|
||||||
EntityManagerInterface $em,
|
EntityManagerInterface $em,
|
||||||
|
Cache $cache,
|
||||||
$chars = self::DEFAULT_CHARS
|
$chars = self::DEFAULT_CHARS
|
||||||
) {
|
) {
|
||||||
$this->httpClient = $httpClient;
|
$this->httpClient = $httpClient;
|
||||||
$this->em = $em;
|
$this->em = $em;
|
||||||
$this->chars = empty($chars) ? self::DEFAULT_CHARS : $chars;
|
$this->chars = empty($chars) ? self::DEFAULT_CHARS : $chars;
|
||||||
|
$this->cache = $cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,7 +99,7 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
$this->em->close();
|
$this->em->close();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new RuntimeException('An error occured while persisting the short URL', -1, $e);
|
throw new RuntimeException('An error occurred while persisting the short URL', -1, $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -140,6 +148,12 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
*/
|
*/
|
||||||
public function shortCodeToUrl($shortCode)
|
public function shortCodeToUrl($shortCode)
|
||||||
{
|
{
|
||||||
|
$cacheKey = sprintf('%s_longUrl', $shortCode);
|
||||||
|
// Check if the short code => URL map is already cached
|
||||||
|
if ($this->cache->contains($cacheKey)) {
|
||||||
|
return $this->cache->fetch($cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
// Validate short code format
|
// Validate short code format
|
||||||
if (! preg_match('|[' . $this->chars . "]+|", $shortCode)) {
|
if (! preg_match('|[' . $this->chars . "]+|", $shortCode)) {
|
||||||
throw InvalidShortCodeException::fromShortCode($shortCode, $this->chars);
|
throw InvalidShortCodeException::fromShortCode($shortCode, $this->chars);
|
||||||
@ -149,6 +163,13 @@ class UrlShortener implements UrlShortenerInterface
|
|||||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||||
'shortCode' => $shortCode,
|
'shortCode' => $shortCode,
|
||||||
]);
|
]);
|
||||||
return isset($shortUrl) ? $shortUrl->getOriginalUrl() : null;
|
// Cache the shortcode
|
||||||
|
if (isset($shortUrl)) {
|
||||||
|
$url = $shortUrl->getOriginalUrl();
|
||||||
|
$this->cache->save($cacheKey, $url);
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ namespace Shlinkio\Shlink\Core\Service;
|
|||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
@ -31,12 +32,10 @@ class VisitsTracker implements VisitsTrackerInterface
|
|||||||
* Tracks a new visit to provided short code, using an array of data to look up information
|
* Tracks a new visit to provided short code, using an array of data to look up information
|
||||||
*
|
*
|
||||||
* @param string $shortCode
|
* @param string $shortCode
|
||||||
* @param array $visitorData Defaults to global $_SERVER
|
* @param ServerRequestInterface $request
|
||||||
*/
|
*/
|
||||||
public function track($shortCode, array $visitorData = null)
|
public function track($shortCode, ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
$visitorData = $visitorData ?: $_SERVER;
|
|
||||||
|
|
||||||
/** @var ShortUrl $shortUrl */
|
/** @var ShortUrl $shortUrl */
|
||||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||||
'shortCode' => $shortCode,
|
'shortCode' => $shortCode,
|
||||||
@ -44,22 +43,27 @@ class VisitsTracker implements VisitsTrackerInterface
|
|||||||
|
|
||||||
$visit = new Visit();
|
$visit = new Visit();
|
||||||
$visit->setShortUrl($shortUrl)
|
$visit->setShortUrl($shortUrl)
|
||||||
->setUserAgent($this->getArrayValue($visitorData, 'HTTP_USER_AGENT'))
|
->setUserAgent($request->getHeaderLine('User-Agent'))
|
||||||
->setReferer($this->getArrayValue($visitorData, 'HTTP_REFERER'))
|
->setReferer($request->getHeaderLine('Referer'))
|
||||||
->setRemoteAddr($this->getArrayValue($visitorData, 'REMOTE_ADDR'));
|
->setRemoteAddr($this->findOutRemoteAddr($request));
|
||||||
$this->em->persist($visit);
|
$this->em->persist($visit);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $array
|
* @param ServerRequestInterface $request
|
||||||
* @param $key
|
* @return string
|
||||||
* @param null $default
|
|
||||||
* @return mixed|null
|
|
||||||
*/
|
*/
|
||||||
protected function getArrayValue(array $array, $key, $default = null)
|
protected function findOutRemoteAddr(ServerRequestInterface $request)
|
||||||
{
|
{
|
||||||
return isset($array[$key]) ? $array[$key] : $default;
|
$forwardedFor = $request->getHeaderLine('X-Forwarded-For');
|
||||||
|
if (empty($forwardedFor)) {
|
||||||
|
$serverParams = $request->getServerParams();
|
||||||
|
return isset($serverParams['REMOTE_ADDR']) ? $serverParams['REMOTE_ADDR'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ips = explode(',', $forwardedFor);
|
||||||
|
return $ips[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace Shlinkio\Shlink\Core\Service;
|
namespace Shlinkio\Shlink\Core\Service;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
|
||||||
@ -10,9 +11,10 @@ interface VisitsTrackerInterface
|
|||||||
* Tracks a new visit to provided short code, using an array of data to look up information
|
* Tracks a new visit to provided short code, using an array of data to look up information
|
||||||
*
|
*
|
||||||
* @param string $shortCode
|
* @param string $shortCode
|
||||||
* @param array $visitorData Defaults to global $_SERVER
|
* @param ServerRequestInterface $request
|
||||||
|
* @return
|
||||||
*/
|
*/
|
||||||
public function track($shortCode, array $visitorData = null);
|
public function track($shortCode, ServerRequestInterface $request);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the visits on certain short code
|
* Returns the visits on certain short code
|
||||||
|
@ -27,7 +27,7 @@
|
|||||||
<hr />
|
<hr />
|
||||||
{% block footer %}
|
{% block footer %}
|
||||||
<p>
|
<p>
|
||||||
© {{ "now" | date("Y") }} by <a href="http://www.alejandrocelaya.com">Alejandro Celaya</a>.
|
© {{ "now" | date("Y") }} <a href="https://shlink.io">Shlink</a>
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
93
module/Core/test/Action/QrCodeActionTest.php
Normal file
93
module/Core/test/Action/QrCodeActionTest.php
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Core\Action;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
|
||||||
|
use Shlinkio\Shlink\Core\Action\QrCodeAction;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
|
use Zend\Diactoros\Response;
|
||||||
|
use Zend\Diactoros\ServerRequestFactory;
|
||||||
|
use Zend\Expressive\Router\RouterInterface;
|
||||||
|
|
||||||
|
class QrCodeActionTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var QrCodeAction
|
||||||
|
*/
|
||||||
|
protected $action;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $urlShortener;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$router = $this->prophesize(RouterInterface::class);
|
||||||
|
$router->generateUri(Argument::cetera())->willReturn('/foo/bar');
|
||||||
|
|
||||||
|
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||||
|
|
||||||
|
$this->action = new QrCodeAction($router->reveal(), $this->urlShortener->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function aNonexistentShortCodeWillReturnNotFoundResponse()
|
||||||
|
{
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$resp = $this->action->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
|
||||||
|
new Response(),
|
||||||
|
function ($req, $resp) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$this->assertEquals(404, $resp->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function anInvalidShortCodeWillReturnNotFoundResponse()
|
||||||
|
{
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$resp = $this->action->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
|
||||||
|
new Response(),
|
||||||
|
function ($req, $resp) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$this->assertEquals(404, $resp->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function aCorrectRequestReturnsTheQrCodeResponse()
|
||||||
|
{
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(new ShortUrl())->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$resp = $this->action->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
|
||||||
|
new Response(),
|
||||||
|
function ($req, $resp) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertInstanceOf(QrCodeResponse::class, $resp);
|
||||||
|
$this->assertEquals(200, $resp->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
69
module/Core/test/Middleware/QrCodeCacheMiddlewareTest.php
Normal file
69
module/Core/test/Middleware/QrCodeCacheMiddlewareTest.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Core\Middleware;
|
||||||
|
|
||||||
|
use Doctrine\Common\Cache\ArrayCache;
|
||||||
|
use Doctrine\Common\Cache\Cache;
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Shlinkio\Shlink\Core\Middleware\QrCodeCacheMiddleware;
|
||||||
|
use Zend\Diactoros\Response;
|
||||||
|
use Zend\Diactoros\ServerRequestFactory;
|
||||||
|
use Zend\Diactoros\Uri;
|
||||||
|
|
||||||
|
class QrCodeCacheMiddlewareTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var QrCodeCacheMiddleware
|
||||||
|
*/
|
||||||
|
protected $middleware;
|
||||||
|
/**
|
||||||
|
* @var Cache
|
||||||
|
*/
|
||||||
|
protected $cache;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->cache = new ArrayCache();
|
||||||
|
$this->middleware = new QrCodeCacheMiddleware($this->cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function noCachedPathFallbacksToNextMiddleware()
|
||||||
|
{
|
||||||
|
$isCalled = false;
|
||||||
|
$this->middleware->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals(),
|
||||||
|
new Response(),
|
||||||
|
function ($req, $resp) use (&$isCalled) {
|
||||||
|
$isCalled = true;
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
$this->assertTrue($isCalled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function cachedPathReturnsCacheContent()
|
||||||
|
{
|
||||||
|
$isCalled = false;
|
||||||
|
$uri = (new Uri())->withPath('/foo');
|
||||||
|
$this->cache->save('/foo', ['body' => 'the body', 'content-type' => 'image/png']);
|
||||||
|
|
||||||
|
$resp = $this->middleware->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withUri($uri),
|
||||||
|
new Response(),
|
||||||
|
function ($req, $resp) use (&$isCalled) {
|
||||||
|
$isCalled = true;
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertFalse($isCalled);
|
||||||
|
$resp->getBody()->rewind();
|
||||||
|
$this->assertEquals('the body', $resp->getBody()->getContents());
|
||||||
|
$this->assertEquals('image/png', $resp->getHeaderLine('Content-Type'));
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
namespace ShlinkioTest\Shlink\Core\Service;
|
namespace ShlinkioTest\Shlink\Core\Service;
|
||||||
|
|
||||||
|
use Doctrine\Common\Cache\ArrayCache;
|
||||||
|
use Doctrine\Common\Cache\Cache;
|
||||||
use Doctrine\Common\Persistence\ObjectRepository;
|
use Doctrine\Common\Persistence\ObjectRepository;
|
||||||
use Doctrine\DBAL\Connection;
|
use Doctrine\DBAL\Connection;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
@ -29,6 +31,10 @@ class UrlShortenerTest extends TestCase
|
|||||||
* @var ObjectProphecy
|
* @var ObjectProphecy
|
||||||
*/
|
*/
|
||||||
protected $httpClient;
|
protected $httpClient;
|
||||||
|
/**
|
||||||
|
* @var Cache
|
||||||
|
*/
|
||||||
|
protected $cache;
|
||||||
|
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
@ -50,7 +56,9 @@ class UrlShortenerTest extends TestCase
|
|||||||
$repo->findOneBy(Argument::any())->willReturn(null);
|
$repo->findOneBy(Argument::any())->willReturn(null);
|
||||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
$this->urlShortener = new UrlShortener($this->httpClient->reveal(), $this->em->reveal());
|
$this->cache = new ArrayCache();
|
||||||
|
|
||||||
|
$this->urlShortener = new UrlShortener($this->httpClient->reveal(), $this->em->reveal(), $this->cache);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,16 +120,19 @@ class UrlShortenerTest extends TestCase
|
|||||||
public function shortCodeIsProperlyParsed()
|
public function shortCodeIsProperlyParsed()
|
||||||
{
|
{
|
||||||
// 12C1c -> 10
|
// 12C1c -> 10
|
||||||
|
$shortCode = '12C1c';
|
||||||
$shortUrl = new ShortUrl();
|
$shortUrl = new ShortUrl();
|
||||||
$shortUrl->setShortCode('12C1c')
|
$shortUrl->setShortCode($shortCode)
|
||||||
->setOriginalUrl('expected_url');
|
->setOriginalUrl('expected_url');
|
||||||
|
|
||||||
$repo = $this->prophesize(ObjectRepository::class);
|
$repo = $this->prophesize(ObjectRepository::class);
|
||||||
$repo->findOneBy(['shortCode' => '12C1c'])->willReturn($shortUrl);
|
$repo->findOneBy(['shortCode' => $shortCode])->willReturn($shortUrl);
|
||||||
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
$url = $this->urlShortener->shortCodeToUrl('12C1c');
|
$this->assertFalse($this->cache->contains($shortCode . '_longUrl'));
|
||||||
|
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||||
$this->assertEquals($shortUrl->getOriginalUrl(), $url);
|
$this->assertEquals($shortUrl->getOriginalUrl(), $url);
|
||||||
|
$this->assertTrue($this->cache->contains($shortCode . '_longUrl'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -132,4 +143,18 @@ class UrlShortenerTest extends TestCase
|
|||||||
{
|
{
|
||||||
$this->urlShortener->shortCodeToUrl('&/(');
|
$this->urlShortener->shortCodeToUrl('&/(');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function cachedShortCodeDoesNotHitDatabase()
|
||||||
|
{
|
||||||
|
$shortCode = '12C1c';
|
||||||
|
$expectedUrl = 'expected_url';
|
||||||
|
$this->cache->save($shortCode . '_longUrl', $expectedUrl);
|
||||||
|
$this->em->getRepository(ShortUrl::class)->willReturn(null)->shouldBeCalledTimes(0);
|
||||||
|
|
||||||
|
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||||
|
$this->assertEquals($expectedUrl, $url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
|||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
use Shlinkio\Shlink\Core\Repository\VisitRepository;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
||||||
|
use Zend\Diactoros\ServerRequestFactory;
|
||||||
|
|
||||||
class VisitsTrackerTest extends TestCase
|
class VisitsTrackerTest extends TestCase
|
||||||
{
|
{
|
||||||
@ -41,7 +42,30 @@ class VisitsTrackerTest extends TestCase
|
|||||||
$this->em->persist(Argument::any())->shouldBeCalledTimes(1);
|
$this->em->persist(Argument::any())->shouldBeCalledTimes(1);
|
||||||
$this->em->flush()->shouldBeCalledTimes(1);
|
$this->em->flush()->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$this->visitsTracker->track($shortCode);
|
$this->visitsTracker->track($shortCode, ServerRequestFactory::fromGlobals());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function trackUsesForwardedForHeaderIfPresent()
|
||||||
|
{
|
||||||
|
$shortCode = '123ABC';
|
||||||
|
$test = $this;
|
||||||
|
$repo = $this->prophesize(EntityRepository::class);
|
||||||
|
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(new ShortUrl());
|
||||||
|
|
||||||
|
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);
|
||||||
|
$this->em->persist(Argument::any())->will(function ($args) use ($test) {
|
||||||
|
/** @var Visit $visit */
|
||||||
|
$visit = $args[0];
|
||||||
|
$test->assertEquals('4.3.2.1', $visit->getRemoteAddr());
|
||||||
|
})->shouldBeCalledTimes(1);
|
||||||
|
$this->em->flush()->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$this->visitsTracker->track($shortCode, ServerRequestFactory::fromGlobals(
|
||||||
|
['REMOTE_ADDR' => '1.2.3.4']
|
||||||
|
)->withHeader('X-Forwarded-For', '4.3.2.1,99.99.99.99'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
||||||
use Shlinkio\Shlink\Rest\Action;
|
use Shlinkio\Shlink\Rest\Action;
|
||||||
|
use Shlinkio\Shlink\Rest\Authentication\JWTService;
|
||||||
use Shlinkio\Shlink\Rest\Middleware;
|
use Shlinkio\Shlink\Rest\Middleware;
|
||||||
use Shlinkio\Shlink\Rest\Service;
|
use Shlinkio\Shlink\Rest\Service;
|
||||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||||
@ -9,7 +10,8 @@ return [
|
|||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
Service\RestTokenService::class => AnnotatedFactory::class,
|
JWTService::class => AnnotatedFactory::class,
|
||||||
|
Service\ApiKeyService::class => AnnotatedFactory::class,
|
||||||
|
|
||||||
Action\AuthenticateAction::class => AnnotatedFactory::class,
|
Action\AuthenticateAction::class => AnnotatedFactory::class,
|
||||||
Action\CreateShortcodeAction::class => AnnotatedFactory::class,
|
Action\CreateShortcodeAction::class => AnnotatedFactory::class,
|
||||||
|
12
module/Rest/config/entity-manager.config.php
Normal file
12
module/Rest/config/entity-manager.config.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
return [
|
||||||
|
|
||||||
|
'entity_manager' => [
|
||||||
|
'orm' => [
|
||||||
|
'entities_paths' => [
|
||||||
|
__DIR__ . '/../src/Entity',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
Binary file not shown.
@ -1,8 +1,8 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Shlink 1.0\n"
|
"Project-Id-Version: Shlink 1.0\n"
|
||||||
"POT-Creation-Date: 2016-07-27 08:53+0200\n"
|
"POT-Creation-Date: 2016-08-07 20:19+0200\n"
|
||||||
"PO-Revision-Date: 2016-07-27 08:53+0200\n"
|
"PO-Revision-Date: 2016-08-07 20:21+0200\n"
|
||||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
@ -17,11 +17,13 @@ msgstr ""
|
|||||||
"X-Poedit-SearchPath-0: config\n"
|
"X-Poedit-SearchPath-0: config\n"
|
||||||
"X-Poedit-SearchPath-1: src\n"
|
"X-Poedit-SearchPath-1: src\n"
|
||||||
|
|
||||||
msgid "You have to provide both \"username\" and \"password\""
|
msgid "You have to provide a valid API key under the \"apiKey\" param name."
|
||||||
msgstr "Debes proporcionar tanto \"username\" como \"password\""
|
msgstr ""
|
||||||
|
"Debes proporcionar una clave de API válida bajo el nombre de parámetro "
|
||||||
|
"\"apiKey\"."
|
||||||
|
|
||||||
msgid "Invalid username and/or password"
|
msgid "Provided API key does not exist or is invalid."
|
||||||
msgstr "Usuario y/o contraseña no válidos"
|
msgstr "La clave de API proporcionada no existe o es inválida."
|
||||||
|
|
||||||
msgid "A URL was not provided"
|
msgid "A URL was not provided"
|
||||||
msgstr "No se ha proporcionado una URL"
|
msgstr "No se ha proporcionado una URL"
|
||||||
@ -47,6 +49,16 @@ msgstr "No se ha encontrado una URL para el código corto \"%s\""
|
|||||||
msgid "Provided short code \"%s\" has an invalid format"
|
msgid "Provided short code \"%s\" has an invalid format"
|
||||||
msgstr "El código corto proporcionado \"%s\" tiene un formato no inválido"
|
msgstr "El código corto proporcionado \"%s\" tiene un formato no inválido"
|
||||||
|
|
||||||
|
#, php-format
|
||||||
|
msgid "You need to provide the Bearer type in the %s header."
|
||||||
|
msgstr "Debes proporcionar el typo Bearer en la cabecera %s."
|
||||||
|
|
||||||
|
#, php-format
|
||||||
|
msgid "Provided authorization type %s is not supported. Use Bearer instead."
|
||||||
|
msgstr ""
|
||||||
|
"El tipo de autorización proporcionado %s no está soportado. En vez de eso "
|
||||||
|
"utiliza Bearer."
|
||||||
|
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"Missing or invalid auth token provided. Perform a new authentication request "
|
"Missing or invalid auth token provided. Perform a new authentication request "
|
||||||
@ -56,8 +68,14 @@ msgstr ""
|
|||||||
"una nueva petición de autenticación y envía el token proporcionado en cada "
|
"una nueva petición de autenticación y envía el token proporcionado en cada "
|
||||||
"nueva petición en la cabecera \"%s\""
|
"nueva petición en la cabecera \"%s\""
|
||||||
|
|
||||||
msgid "Requested route does not exist."
|
#~ msgid "You have to provide both \"username\" and \"password\""
|
||||||
msgstr "La ruta solicitada no existe."
|
#~ msgstr "Debes proporcionar tanto \"username\" como \"password\""
|
||||||
|
|
||||||
|
#~ msgid "Invalid username and/or password"
|
||||||
|
#~ msgstr "Usuario y/o contraseña no válidos"
|
||||||
|
|
||||||
|
#~ msgid "Requested route does not exist."
|
||||||
|
#~ msgstr "La ruta solicitada no existe."
|
||||||
|
|
||||||
#~ msgid "RestToken not found for token \"%s\""
|
#~ msgid "RestToken not found for token \"%s\""
|
||||||
#~ msgstr "No se ha encontrado un RestToken para el token \"%s\""
|
#~ msgstr "No se ha encontrado un RestToken para el token \"%s\""
|
||||||
|
@ -3,10 +3,22 @@ namespace Shlinkio\Shlink\Rest\Action;
|
|||||||
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
use Zend\Stratigility\MiddlewareInterface;
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
|
||||||
abstract class AbstractRestAction implements MiddlewareInterface
|
abstract class AbstractRestAction implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* @var LoggerInterface
|
||||||
|
*/
|
||||||
|
protected $logger;
|
||||||
|
|
||||||
|
public function __construct(LoggerInterface $logger = null)
|
||||||
|
{
|
||||||
|
$this->logger = $logger ?: new NullLogger();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Process an incoming request and/or response.
|
* Process an incoming request and/or response.
|
||||||
*
|
*
|
||||||
|
@ -2,37 +2,48 @@
|
|||||||
namespace Shlinkio\Shlink\Rest\Action;
|
namespace Shlinkio\Shlink\Rest\Action;
|
||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
|
use Shlinkio\Shlink\Rest\Authentication\JWTService;
|
||||||
use Shlinkio\Shlink\Rest\Service\RestTokenService;
|
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
|
||||||
use Shlinkio\Shlink\Rest\Service\RestTokenServiceInterface;
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
use Zend\I18n\Translator\TranslatorInterface;
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
|
||||||
class AuthenticateAction extends AbstractRestAction
|
class AuthenticateAction extends AbstractRestAction
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var RestTokenServiceInterface
|
|
||||||
*/
|
|
||||||
private $restTokenService;
|
|
||||||
/**
|
/**
|
||||||
* @var TranslatorInterface
|
* @var TranslatorInterface
|
||||||
*/
|
*/
|
||||||
private $translator;
|
private $translator;
|
||||||
|
/**
|
||||||
|
* @var ApiKeyService|ApiKeyServiceInterface
|
||||||
|
*/
|
||||||
|
private $apiKeyService;
|
||||||
|
/**
|
||||||
|
* @var JWTServiceInterface
|
||||||
|
*/
|
||||||
|
private $jwtService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AuthenticateAction constructor.
|
* AuthenticateAction constructor.
|
||||||
* @param RestTokenServiceInterface|RestTokenService $restTokenService
|
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
|
||||||
|
* @param JWTServiceInterface|JWTService $jwtService
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
*
|
*
|
||||||
* @Inject({RestTokenService::class, "translator"})
|
* @Inject({ApiKeyService::class, JWTService::class, "translator"})
|
||||||
*/
|
*/
|
||||||
public function __construct(RestTokenServiceInterface $restTokenService, TranslatorInterface $translator)
|
public function __construct(
|
||||||
{
|
ApiKeyServiceInterface $apiKeyService,
|
||||||
$this->restTokenService = $restTokenService;
|
JWTServiceInterface $jwtService,
|
||||||
|
TranslatorInterface $translator
|
||||||
|
) {
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
|
$this->apiKeyService = $apiKeyService;
|
||||||
|
$this->jwtService = $jwtService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,21 +55,26 @@ class AuthenticateAction extends AbstractRestAction
|
|||||||
public function dispatch(Request $request, Response $response, callable $out = null)
|
public function dispatch(Request $request, Response $response, callable $out = null)
|
||||||
{
|
{
|
||||||
$authData = $request->getParsedBody();
|
$authData = $request->getParsedBody();
|
||||||
if (! isset($authData['username'], $authData['password'])) {
|
if (! isset($authData['apiKey'])) {
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||||
'message' => $this->translator->translate('You have to provide both "username" and "password"'),
|
'message' => $this->translator->translate(
|
||||||
|
'You have to provide a valid API key under the "apiKey" param name.'
|
||||||
|
),
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// Authenticate using provided API key
|
||||||
$token = $this->restTokenService->createToken($authData['username'], $authData['password']);
|
$apiKey = $this->apiKeyService->getByKey($authData['apiKey']);
|
||||||
return new JsonResponse(['token' => $token->getToken()]);
|
if (! $apiKey->isValid()) {
|
||||||
} catch (AuthenticationException $e) {
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
'error' => RestUtils::INVALID_API_KEY_ERROR,
|
||||||
'message' => $this->translator->translate('Invalid username and/or password'),
|
'message' => $this->translator->translate('Provided API key does not exist or is invalid.'),
|
||||||
], 401);
|
], 401);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate a JSON Web Token that will be used for authorization in next requests
|
||||||
|
$token = $this->jwtService->create($apiKey);
|
||||||
|
return new JsonResponse(['token' => $token]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ namespace Shlinkio\Shlink\Rest\Action;
|
|||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||||
@ -33,14 +34,17 @@ class CreateShortcodeAction extends AbstractRestAction
|
|||||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
* @param array $domainConfig
|
* @param array $domainConfig
|
||||||
|
* @param LoggerInterface|null $logger
|
||||||
*
|
*
|
||||||
* @Inject({UrlShortener::class, "translator", "config.url_shortener.domain"})
|
* @Inject({UrlShortener::class, "translator", "config.url_shortener.domain", "Logger_Shlink"})
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
UrlShortenerInterface $urlShortener,
|
UrlShortenerInterface $urlShortener,
|
||||||
TranslatorInterface $translator,
|
TranslatorInterface $translator,
|
||||||
array $domainConfig
|
array $domainConfig,
|
||||||
|
LoggerInterface $logger = null
|
||||||
) {
|
) {
|
||||||
|
parent::__construct($logger);
|
||||||
$this->urlShortener = $urlShortener;
|
$this->urlShortener = $urlShortener;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
$this->domainConfig = $domainConfig;
|
$this->domainConfig = $domainConfig;
|
||||||
@ -75,14 +79,16 @@ class CreateShortcodeAction extends AbstractRestAction
|
|||||||
'shortCode' => $shortCode,
|
'shortCode' => $shortCode,
|
||||||
]);
|
]);
|
||||||
} catch (InvalidUrlException $e) {
|
} catch (InvalidUrlException $e) {
|
||||||
|
$this->logger->warning('Provided Invalid URL.' . PHP_EOL . $e);
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||||
'message' => sprintf(
|
'message' => sprintf(
|
||||||
$this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'),
|
$this->translator->translate('Provided URL %s is invalid. Try with a different one.'),
|
||||||
$longUrl
|
$longUrl
|
||||||
),
|
),
|
||||||
], 400);
|
], 400);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error('Unexpected error creating shortcode.' . PHP_EOL . $e);
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'error' => RestUtils::UNKNOWN_ERROR,
|
'error' => RestUtils::UNKNOWN_ERROR,
|
||||||
'message' => $this->translator->translate('Unexpected error occurred'),
|
'message' => $this->translator->translate('Unexpected error occurred'),
|
||||||
|
@ -4,6 +4,7 @@ namespace Shlinkio\Shlink\Rest\Action;
|
|||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
use Shlinkio\Shlink\Core\Service\VisitsTracker;
|
||||||
@ -25,13 +26,18 @@ class GetVisitsAction extends AbstractRestAction
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* GetVisitsAction constructor.
|
* GetVisitsAction constructor.
|
||||||
* @param VisitsTrackerInterface|VisitsTracker $visitsTracker
|
* @param VisitsTrackerInterface $visitsTracker
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
|
* @param LoggerInterface $logger
|
||||||
*
|
*
|
||||||
* @Inject({VisitsTracker::class, "translator"})
|
* @Inject({VisitsTracker::class, "translator", "Logger_Shlink"})
|
||||||
*/
|
*/
|
||||||
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
|
public function __construct(
|
||||||
{
|
VisitsTrackerInterface $visitsTracker,
|
||||||
|
TranslatorInterface $translator,
|
||||||
|
LoggerInterface $logger = null
|
||||||
|
) {
|
||||||
|
parent::__construct($logger);
|
||||||
$this->visitsTracker = $visitsTracker;
|
$this->visitsTracker = $visitsTracker;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
}
|
}
|
||||||
@ -57,11 +63,16 @@ class GetVisitsAction extends AbstractRestAction
|
|||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
} catch (InvalidArgumentException $e) {
|
} catch (InvalidArgumentException $e) {
|
||||||
|
$this->logger->warning('Provided nonexistent shortcode'. PHP_EOL . $e);
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||||
'message' => sprintf($this->translator->translate('Provided short code "%s" is invalid'), $shortCode),
|
'message' => sprintf(
|
||||||
], 400);
|
$this->translator->translate('Provided short code %s does not exist'),
|
||||||
|
$shortCode
|
||||||
|
),
|
||||||
|
], 404);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error('Unexpected error while parsing short code'. PHP_EOL . $e);
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'error' => RestUtils::UNKNOWN_ERROR,
|
'error' => RestUtils::UNKNOWN_ERROR,
|
||||||
'message' => $this->translator->translate('Unexpected error occurred'),
|
'message' => $this->translator->translate('Unexpected error occurred'),
|
||||||
|
@ -4,6 +4,8 @@ namespace Shlinkio\Shlink\Rest\Action;
|
|||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
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\Common\Paginator\Util\PaginatorUtilsTrait;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||||
@ -28,11 +30,16 @@ class ListShortcodesAction extends AbstractRestAction
|
|||||||
* ListShortcodesAction constructor.
|
* ListShortcodesAction constructor.
|
||||||
* @param ShortUrlServiceInterface|ShortUrlService $shortUrlService
|
* @param ShortUrlServiceInterface|ShortUrlService $shortUrlService
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
|
* @param LoggerInterface $logger
|
||||||
*
|
*
|
||||||
* @Inject({ShortUrlService::class, "translator"})
|
* @Inject({ShortUrlService::class, "translator", "Logger_Shlink"})
|
||||||
*/
|
*/
|
||||||
public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
|
public function __construct(
|
||||||
{
|
ShortUrlServiceInterface $shortUrlService,
|
||||||
|
TranslatorInterface $translator,
|
||||||
|
LoggerInterface $logger = null
|
||||||
|
) {
|
||||||
|
parent::__construct($logger);
|
||||||
$this->shortUrlService = $shortUrlService;
|
$this->shortUrlService = $shortUrlService;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
}
|
}
|
||||||
@ -50,6 +57,7 @@ class ListShortcodesAction extends AbstractRestAction
|
|||||||
$shortUrls = $this->shortUrlService->listShortUrls(isset($query['page']) ? $query['page'] : 1);
|
$shortUrls = $this->shortUrlService->listShortUrls(isset($query['page']) ? $query['page'] : 1);
|
||||||
return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls)]);
|
return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls)]);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error('Unexpected error while listing short URLs.' . PHP_EOL . $e);
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'error' => RestUtils::UNKNOWN_ERROR,
|
'error' => RestUtils::UNKNOWN_ERROR,
|
||||||
'message' => $this->translator->translate('Unexpected error occurred'),
|
'message' => $this->translator->translate('Unexpected error occurred'),
|
||||||
|
@ -4,6 +4,7 @@ namespace Shlinkio\Shlink\Rest\Action;
|
|||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||||
@ -26,11 +27,16 @@ class ResolveUrlAction extends AbstractRestAction
|
|||||||
* ResolveUrlAction constructor.
|
* ResolveUrlAction constructor.
|
||||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
|
* @param LoggerInterface $logger
|
||||||
*
|
*
|
||||||
* @Inject({UrlShortener::class, "translator"})
|
* @Inject({UrlShortener::class, "translator"})
|
||||||
*/
|
*/
|
||||||
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
|
public function __construct(
|
||||||
{
|
UrlShortenerInterface $urlShortener,
|
||||||
|
TranslatorInterface $translator,
|
||||||
|
LoggerInterface $logger = null
|
||||||
|
) {
|
||||||
|
parent::__construct($logger);
|
||||||
$this->urlShortener = $urlShortener;
|
$this->urlShortener = $urlShortener;
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
}
|
}
|
||||||
@ -50,14 +56,15 @@ class ResolveUrlAction extends AbstractRestAction
|
|||||||
if (! isset($longUrl)) {
|
if (! isset($longUrl)) {
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||||
'message' => sprintf($this->translator->translate('No URL found for shortcode "%s"'), $shortCode),
|
'message' => sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode),
|
||||||
], 400);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'longUrl' => $longUrl,
|
'longUrl' => $longUrl,
|
||||||
]);
|
]);
|
||||||
} catch (InvalidShortCodeException $e) {
|
} catch (InvalidShortCodeException $e) {
|
||||||
|
$this->logger->warning('Provided short code with invalid format.' . PHP_EOL . $e);
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'error' => RestUtils::getRestErrorCodeFromException($e),
|
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||||
'message' => sprintf(
|
'message' => sprintf(
|
||||||
@ -66,6 +73,7 @@ class ResolveUrlAction extends AbstractRestAction
|
|||||||
),
|
),
|
||||||
], 400);
|
], 400);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
|
$this->logger->error('Unexpected error while resolving the URL behind a short code.' . PHP_EOL . $e);
|
||||||
return new JsonResponse([
|
return new JsonResponse([
|
||||||
'error' => RestUtils::UNKNOWN_ERROR,
|
'error' => RestUtils::UNKNOWN_ERROR,
|
||||||
'message' => $this->translator->translate('Unexpected error occurred'),
|
'message' => $this->translator->translate('Unexpected error occurred'),
|
||||||
|
113
module/Rest/src/Authentication/JWTService.php
Normal file
113
module/Rest/src/Authentication/JWTService.php
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Rest\Authentication;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
|
||||||
|
|
||||||
|
class JWTService implements JWTServiceInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var AppOptions
|
||||||
|
*/
|
||||||
|
private $appOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JWTService constructor.
|
||||||
|
* @param AppOptions $appOptions
|
||||||
|
*
|
||||||
|
* @Inject({AppOptions::class})
|
||||||
|
*/
|
||||||
|
public function __construct(AppOptions $appOptions)
|
||||||
|
{
|
||||||
|
$this->appOptions = $appOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new JSON web token por provided API key
|
||||||
|
*
|
||||||
|
* @param ApiKey $apiKey
|
||||||
|
* @param int $lifetime
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function create(ApiKey $apiKey, $lifetime = self::DEFAULT_LIFETIME)
|
||||||
|
{
|
||||||
|
$currentTimestamp = time();
|
||||||
|
|
||||||
|
return $this->encode([
|
||||||
|
'iss' => $this->appOptions->__toString(),
|
||||||
|
'iat' => $currentTimestamp,
|
||||||
|
'exp' => $currentTimestamp + $lifetime,
|
||||||
|
'sub' => 'auth',
|
||||||
|
'key' => $apiKey->getId(), // The ID is opaque. Returning the key would be insecure
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes a token and returns it with the new expiration
|
||||||
|
*
|
||||||
|
* @param string $jwt
|
||||||
|
* @param int $lifetime
|
||||||
|
* @return string
|
||||||
|
* @throws AuthenticationException If the token has expired
|
||||||
|
*/
|
||||||
|
public function refresh($jwt, $lifetime = self::DEFAULT_LIFETIME)
|
||||||
|
{
|
||||||
|
$payload = $this->getPayload($jwt);
|
||||||
|
$payload['exp'] = time() + $lifetime;
|
||||||
|
return $this->encode($payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that certain JWT is valid
|
||||||
|
*
|
||||||
|
* @param string $jwt
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function verify($jwt)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// If no exception is thrown while decoding the token, it is considered valid
|
||||||
|
$this->decode($jwt);
|
||||||
|
return true;
|
||||||
|
} catch (\UnexpectedValueException $e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes certain token and returns the payload
|
||||||
|
*
|
||||||
|
* @param string $jwt
|
||||||
|
* @return array
|
||||||
|
* @throws AuthenticationException If the token has expired
|
||||||
|
*/
|
||||||
|
public function getPayload($jwt)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return $this->decode($jwt);
|
||||||
|
} catch (\UnexpectedValueException $e) {
|
||||||
|
throw AuthenticationException::expiredJWT($e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $data
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function encode(array $data)
|
||||||
|
{
|
||||||
|
return JWT::encode($data, $this->appOptions->getSecretKey(), self::DEFAULT_ENCRYPTION_ALG);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $jwt
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function decode($jwt)
|
||||||
|
{
|
||||||
|
return (array) JWT::decode($jwt, $this->appOptions->getSecretKey(), [self::DEFAULT_ENCRYPTION_ALG]);
|
||||||
|
}
|
||||||
|
}
|
47
module/Rest/src/Authentication/JWTServiceInterface.php
Normal file
47
module/Rest/src/Authentication/JWTServiceInterface.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Rest\Authentication;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
|
||||||
|
|
||||||
|
interface JWTServiceInterface
|
||||||
|
{
|
||||||
|
const DEFAULT_LIFETIME = 604800; // 1 week
|
||||||
|
const DEFAULT_ENCRYPTION_ALG = 'HS256';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new JSON web token por provided API key
|
||||||
|
*
|
||||||
|
* @param ApiKey $apiKey
|
||||||
|
* @param int $lifetime
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function create(ApiKey $apiKey, $lifetime = self::DEFAULT_LIFETIME);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes a token and returns it with the new expiration
|
||||||
|
*
|
||||||
|
* @param string $jwt
|
||||||
|
* @param int $lifetime
|
||||||
|
* @return string
|
||||||
|
* @throws AuthenticationException If the token has expired
|
||||||
|
*/
|
||||||
|
public function refresh($jwt, $lifetime = self::DEFAULT_LIFETIME);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies that certain JWT is valid
|
||||||
|
*
|
||||||
|
* @param string $jwt
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function verify($jwt);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes certain token and returns the payload
|
||||||
|
*
|
||||||
|
* @param string $jwt
|
||||||
|
* @return array
|
||||||
|
* @throws AuthenticationException If the token has expired
|
||||||
|
*/
|
||||||
|
public function getPayload($jwt);
|
||||||
|
}
|
137
module/Rest/src/Entity/ApiKey.php
Normal file
137
module/Rest/src/Entity/ApiKey.php
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Rest\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||||
|
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class ApiKey
|
||||||
|
* @author Shlink
|
||||||
|
* @link http://shlink.io
|
||||||
|
*
|
||||||
|
* @ORM\Entity()
|
||||||
|
* @ORM\Table(name="api_keys")
|
||||||
|
*/
|
||||||
|
class ApiKey extends AbstractEntity
|
||||||
|
{
|
||||||
|
use StringUtilsTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
* @ORM\Column(name="`key`", nullable=false, unique=true)
|
||||||
|
*/
|
||||||
|
protected $key;
|
||||||
|
/**
|
||||||
|
* @var \DateTime
|
||||||
|
* @ORM\Column(name="expiration_date", nullable=true, type="datetime")
|
||||||
|
*/
|
||||||
|
protected $expirationDate;
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
* @ORM\Column(type="boolean")
|
||||||
|
*/
|
||||||
|
protected $enabled;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->enabled = true;
|
||||||
|
$this->key = $this->generateV4Uuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getKey()
|
||||||
|
{
|
||||||
|
return $this->key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $key
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setKey($key)
|
||||||
|
{
|
||||||
|
$this->key = $key;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \DateTime
|
||||||
|
*/
|
||||||
|
public function getExpirationDate()
|
||||||
|
{
|
||||||
|
return $this->expirationDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DateTime $expirationDate
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setExpirationDate($expirationDate)
|
||||||
|
{
|
||||||
|
$this->expirationDate = $expirationDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isExpired()
|
||||||
|
{
|
||||||
|
if (! isset($this->expirationDate)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->expirationDate < new \DateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function isEnabled()
|
||||||
|
{
|
||||||
|
return $this->enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param boolean $enabled
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setEnabled($enabled)
|
||||||
|
{
|
||||||
|
$this->enabled = $enabled;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables this API key
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function disable()
|
||||||
|
{
|
||||||
|
return $this->setEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells if this api key is enabled and not expired
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isValid()
|
||||||
|
{
|
||||||
|
return $this->isEnabled() && ! $this->isExpired();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The string repesentation of an API key is the key itself
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function __toString()
|
||||||
|
{
|
||||||
|
return $this->getKey();
|
||||||
|
}
|
||||||
|
}
|
@ -9,4 +9,9 @@ class AuthenticationException extends \RuntimeException implements ExceptionInte
|
|||||||
{
|
{
|
||||||
return new self(sprintf('Invalid credentials. Username -> "%s". Password -> "%s"', $username, $password));
|
return new self(sprintf('Invalid credentials. Username -> "%s". Password -> "%s"', $username, $password));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function expiredJWT(\Exception $prev = null)
|
||||||
|
{
|
||||||
|
return new self('The token has expired.', -1, $prev);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,11 @@ namespace Shlinkio\Shlink\Rest\Middleware;
|
|||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
use Psr\Log\LoggerInterface;
|
||||||
use Shlinkio\Shlink\Rest\Service\RestTokenService;
|
use Psr\Log\NullLogger;
|
||||||
use Shlinkio\Shlink\Rest\Service\RestTokenServiceInterface;
|
use Shlinkio\Shlink\Rest\Authentication\JWTService;
|
||||||
|
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
|
||||||
|
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
|
||||||
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
use Zend\Expressive\Router\RouteResult;
|
use Zend\Expressive\Router\RouteResult;
|
||||||
@ -15,28 +17,37 @@ use Zend\Stratigility\MiddlewareInterface;
|
|||||||
|
|
||||||
class CheckAuthenticationMiddleware implements MiddlewareInterface
|
class CheckAuthenticationMiddleware implements MiddlewareInterface
|
||||||
{
|
{
|
||||||
const AUTH_TOKEN_HEADER = 'X-Auth-Token';
|
const AUTHORIZATION_HEADER = 'Authorization';
|
||||||
|
|
||||||
/**
|
|
||||||
* @var RestTokenServiceInterface
|
|
||||||
*/
|
|
||||||
private $restTokenService;
|
|
||||||
/**
|
/**
|
||||||
* @var TranslatorInterface
|
* @var TranslatorInterface
|
||||||
*/
|
*/
|
||||||
private $translator;
|
private $translator;
|
||||||
|
/**
|
||||||
|
* @var JWTServiceInterface
|
||||||
|
*/
|
||||||
|
private $jwtService;
|
||||||
|
/**
|
||||||
|
* @var LoggerInterface
|
||||||
|
*/
|
||||||
|
private $logger;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CheckAuthenticationMiddleware constructor.
|
* CheckAuthenticationMiddleware constructor.
|
||||||
* @param RestTokenServiceInterface|RestTokenService $restTokenService
|
* @param JWTServiceInterface|JWTService $jwtService
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
|
* @param LoggerInterface $logger
|
||||||
*
|
*
|
||||||
* @Inject({RestTokenService::class, "translator"})
|
* @Inject({JWTService::class, "translator", "Logger_Shlink"})
|
||||||
*/
|
*/
|
||||||
public function __construct(RestTokenServiceInterface $restTokenService, TranslatorInterface $translator)
|
public function __construct(
|
||||||
{
|
JWTServiceInterface $jwtService,
|
||||||
$this->restTokenService = $restTokenService;
|
TranslatorInterface $translator,
|
||||||
|
LoggerInterface $logger = null
|
||||||
|
) {
|
||||||
$this->translator = $translator;
|
$this->translator = $translator;
|
||||||
|
$this->jwtService = $jwtService;
|
||||||
|
$this->logger = $logger ?: new NullLogger();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -78,21 +89,47 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check that the auth header was provided, and that it belongs to a non-expired token
|
// Check that the auth header was provided, and that it belongs to a non-expired token
|
||||||
if (! $request->hasHeader(self::AUTH_TOKEN_HEADER)) {
|
if (! $request->hasHeader(self::AUTHORIZATION_HEADER)) {
|
||||||
return $this->createTokenErrorResponse();
|
return $this->createTokenErrorResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
$authToken = $request->getHeaderLine(self::AUTH_TOKEN_HEADER);
|
// Get token making sure the an authorization type is provided
|
||||||
|
$authToken = $request->getHeaderLine(self::AUTHORIZATION_HEADER);
|
||||||
|
$authTokenParts = explode(' ', $authToken);
|
||||||
|
if (count($authTokenParts) === 1) {
|
||||||
|
return new JsonResponse([
|
||||||
|
'error' => RestUtils::INVALID_AUTHORIZATION_ERROR,
|
||||||
|
'message' => sprintf($this->translator->translate(
|
||||||
|
'You need to provide the Bearer type in the %s header.'
|
||||||
|
), self::AUTHORIZATION_HEADER),
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the authorization type is Bearer
|
||||||
|
list($authType, $jwt) = $authTokenParts;
|
||||||
|
if (strtolower($authType) !== 'bearer') {
|
||||||
|
return new JsonResponse([
|
||||||
|
'error' => RestUtils::INVALID_AUTHORIZATION_ERROR,
|
||||||
|
'message' => sprintf($this->translator->translate(
|
||||||
|
'Provided authorization type %s is not supported. Use Bearer instead.'
|
||||||
|
), $authType),
|
||||||
|
], 401);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$restToken = $this->restTokenService->getByToken($authToken);
|
if (! $this->jwtService->verify($jwt)) {
|
||||||
if ($restToken->isExpired()) {
|
|
||||||
return $this->createTokenErrorResponse();
|
return $this->createTokenErrorResponse();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the token expiration and continue to next middleware
|
// Update the token expiration and continue to next middleware
|
||||||
$this->restTokenService->updateExpiration($restToken);
|
$jwt = $this->jwtService->refresh($jwt);
|
||||||
return $out($request, $response);
|
/** @var Response $response */
|
||||||
} catch (InvalidArgumentException $e) {
|
$response = $out($request, $response);
|
||||||
|
|
||||||
|
// Return the response with the updated token on it
|
||||||
|
return $response->withHeader(self::AUTHORIZATION_HEADER, 'Bearer ' . $jwt);
|
||||||
|
} catch (AuthenticationException $e) {
|
||||||
|
$this->logger->warning('Tried to access API with an invalid JWT.' . PHP_EOL . $e);
|
||||||
return $this->createTokenErrorResponse();
|
return $this->createTokenErrorResponse();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,7 +143,7 @@ class CheckAuthenticationMiddleware implements MiddlewareInterface
|
|||||||
'Missing or invalid auth token provided. Perform a new authentication request and send provided '
|
'Missing or invalid auth token provided. Perform a new authentication request and send provided '
|
||||||
. 'token on every new request on the "%s" header'
|
. 'token on every new request on the "%s" header'
|
||||||
),
|
),
|
||||||
self::AUTH_TOKEN_HEADER
|
self::AUTHORIZATION_HEADER
|
||||||
),
|
),
|
||||||
], 401);
|
], 401);
|
||||||
}
|
}
|
||||||
|
106
module/Rest/src/Service/ApiKeyService.php
Normal file
106
module/Rest/src/Service/ApiKeyService.php
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Rest\Service;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
|
class ApiKeyService implements ApiKeyServiceInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var EntityManagerInterface
|
||||||
|
*/
|
||||||
|
private $em;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ApiKeyService constructor.
|
||||||
|
* @param EntityManagerInterface $em
|
||||||
|
*
|
||||||
|
* @Inject({"em"})
|
||||||
|
*/
|
||||||
|
public function __construct(EntityManagerInterface $em)
|
||||||
|
{
|
||||||
|
$this->em = $em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new ApiKey with provided expiration date
|
||||||
|
*
|
||||||
|
* @param \DateTime $expirationDate
|
||||||
|
* @return ApiKey
|
||||||
|
*/
|
||||||
|
public function create(\DateTime $expirationDate = null)
|
||||||
|
{
|
||||||
|
$key = new ApiKey();
|
||||||
|
if (isset($expirationDate)) {
|
||||||
|
$key->setExpirationDate($expirationDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->em->persist($key);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if provided key is a valid api key
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function check($key)
|
||||||
|
{
|
||||||
|
/** @var ApiKey $apiKey */
|
||||||
|
$apiKey = $this->getByKey($key);
|
||||||
|
if (! isset($apiKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $apiKey->isValid();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables provided api key
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @return ApiKey
|
||||||
|
*/
|
||||||
|
public function disable($key)
|
||||||
|
{
|
||||||
|
/** @var ApiKey $apiKey */
|
||||||
|
$apiKey = $this->getByKey($key);
|
||||||
|
if (! isset($apiKey)) {
|
||||||
|
throw new InvalidArgumentException(sprintf('API key "%s" does not exist and can\'t be disabled', $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiKey->disable();
|
||||||
|
$this->em->flush();
|
||||||
|
return $apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all existing appi keys
|
||||||
|
*
|
||||||
|
* @param bool $enabledOnly Tells if only enabled keys should be returned
|
||||||
|
* @return ApiKey[]
|
||||||
|
*/
|
||||||
|
public function listKeys($enabledOnly = false)
|
||||||
|
{
|
||||||
|
$conditions = $enabledOnly ? ['enabled' => true] : [];
|
||||||
|
return $this->em->getRepository(ApiKey::class)->findBy($conditions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to find one API key by its key string
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @return ApiKey|null
|
||||||
|
*/
|
||||||
|
public function getByKey($key)
|
||||||
|
{
|
||||||
|
return $this->em->getRepository(ApiKey::class)->findOneBy([
|
||||||
|
'key' => $key,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
47
module/Rest/src/Service/ApiKeyServiceInterface.php
Normal file
47
module/Rest/src/Service/ApiKeyServiceInterface.php
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Rest\Service;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
|
interface ApiKeyServiceInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new ApiKey with provided expiration date
|
||||||
|
*
|
||||||
|
* @param \DateTime $expirationDate
|
||||||
|
* @return ApiKey
|
||||||
|
*/
|
||||||
|
public function create(\DateTime $expirationDate = null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if provided key is a valid api key
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function check($key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables provided api key
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @return ApiKey
|
||||||
|
*/
|
||||||
|
public function disable($key);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lists all existing appi keys
|
||||||
|
*
|
||||||
|
* @param bool $enabledOnly Tells if only enabled keys should be returned
|
||||||
|
* @return ApiKey[]
|
||||||
|
*/
|
||||||
|
public function listKeys($enabledOnly = false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tries to find one API key by its key string
|
||||||
|
*
|
||||||
|
* @param string $key
|
||||||
|
* @return ApiKey|null
|
||||||
|
*/
|
||||||
|
public function getByKey($key);
|
||||||
|
}
|
@ -1,98 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Shlinkio\Shlink\Rest\Service;
|
|
||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
|
||||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
|
||||||
use Shlinkio\Shlink\Core\Entity\RestToken;
|
|
||||||
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
|
|
||||||
|
|
||||||
class RestTokenService implements RestTokenServiceInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var EntityManagerInterface
|
|
||||||
*/
|
|
||||||
private $em;
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
private $restConfig;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ShortUrlService constructor.
|
|
||||||
* @param EntityManagerInterface $em
|
|
||||||
* @param array $restConfig
|
|
||||||
*
|
|
||||||
* @Inject({"em", "config.rest"})
|
|
||||||
*/
|
|
||||||
public function __construct(EntityManagerInterface $em, array $restConfig)
|
|
||||||
{
|
|
||||||
$this->em = $em;
|
|
||||||
$this->restConfig = $restConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $token
|
|
||||||
* @return RestToken
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
*/
|
|
||||||
public function getByToken($token)
|
|
||||||
{
|
|
||||||
$restToken = $this->em->getRepository(RestToken::class)->findOneBy([
|
|
||||||
'token' => $token,
|
|
||||||
]);
|
|
||||||
if (! isset($restToken)) {
|
|
||||||
throw new InvalidArgumentException(sprintf('RestToken not found for token "%s"', $token));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $restToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and returns a new RestToken if username and password are correct
|
|
||||||
* @param $username
|
|
||||||
* @param $password
|
|
||||||
* @return RestToken
|
|
||||||
* @throws AuthenticationException
|
|
||||||
*/
|
|
||||||
public function createToken($username, $password)
|
|
||||||
{
|
|
||||||
$this->processCredentials($username, $password);
|
|
||||||
|
|
||||||
$restToken = new RestToken();
|
|
||||||
$this->em->persist($restToken);
|
|
||||||
$this->em->flush();
|
|
||||||
|
|
||||||
return $restToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $username
|
|
||||||
* @param string $password
|
|
||||||
*/
|
|
||||||
protected function processCredentials($username, $password)
|
|
||||||
{
|
|
||||||
$configUsername = strtolower(trim($this->restConfig['username']));
|
|
||||||
$providedUsername = strtolower(trim($username));
|
|
||||||
$configPassword = trim($this->restConfig['password']);
|
|
||||||
$providedPassword = trim($password);
|
|
||||||
|
|
||||||
if ($configUsername === $providedUsername && $configPassword === $providedPassword) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If credentials are not correct, throw exception
|
|
||||||
throw AuthenticationException::fromCredentials($providedUsername, $providedPassword);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the expiration of provided token, extending its life
|
|
||||||
*
|
|
||||||
* @param RestToken $token
|
|
||||||
*/
|
|
||||||
public function updateExpiration(RestToken $token)
|
|
||||||
{
|
|
||||||
$token->updateExpiration();
|
|
||||||
$this->em->flush();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,32 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Shlinkio\Shlink\Rest\Service;
|
|
||||||
|
|
||||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
|
||||||
use Shlinkio\Shlink\Core\Entity\RestToken;
|
|
||||||
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
|
|
||||||
|
|
||||||
interface RestTokenServiceInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param string $token
|
|
||||||
* @return RestToken
|
|
||||||
* @throws InvalidArgumentException
|
|
||||||
*/
|
|
||||||
public function getByToken($token);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates and returns a new RestToken if username and password are correct
|
|
||||||
* @param $username
|
|
||||||
* @param $password
|
|
||||||
* @return RestToken
|
|
||||||
* @throws AuthenticationException
|
|
||||||
*/
|
|
||||||
public function createToken($username, $password);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the expiration of provided token, extending its life
|
|
||||||
*
|
|
||||||
* @param RestToken $token
|
|
||||||
*/
|
|
||||||
public function updateExpiration(RestToken $token);
|
|
||||||
}
|
|
@ -12,6 +12,8 @@ class RestUtils
|
|||||||
const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT';
|
const INVALID_ARGUMENT_ERROR = 'INVALID_ARGUMENT';
|
||||||
const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS';
|
const INVALID_CREDENTIALS_ERROR = 'INVALID_CREDENTIALS';
|
||||||
const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN';
|
const INVALID_AUTH_TOKEN_ERROR = 'INVALID_AUTH_TOKEN';
|
||||||
|
const INVALID_AUTHORIZATION_ERROR = 'INVALID_AUTHORIZATION';
|
||||||
|
const INVALID_API_KEY_ERROR = 'INVALID_API_KEY';
|
||||||
const NOT_FOUND_ERROR = 'NOT_FOUND';
|
const NOT_FOUND_ERROR = 'NOT_FOUND';
|
||||||
const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
|
const UNKNOWN_ERROR = 'UNKNOWN_ERROR';
|
||||||
|
|
||||||
|
@ -3,10 +3,10 @@ namespace ShlinkioTest\Shlink\Rest\Action;
|
|||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\Core\Entity\RestToken;
|
|
||||||
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
|
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
|
||||||
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
|
use Shlinkio\Shlink\Rest\Authentication\JWTService;
|
||||||
use Shlinkio\Shlink\Rest\Service\RestTokenService;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
use Zend\Diactoros\Response;
|
use Zend\Diactoros\Response;
|
||||||
use Zend\Diactoros\ServerRequestFactory;
|
use Zend\Diactoros\ServerRequestFactory;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
@ -20,12 +20,21 @@ class AuthenticateActionTest extends TestCase
|
|||||||
/**
|
/**
|
||||||
* @var ObjectProphecy
|
* @var ObjectProphecy
|
||||||
*/
|
*/
|
||||||
protected $tokenService;
|
protected $apiKeyService;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $jwtService;
|
||||||
|
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
$this->tokenService = $this->prophesize(RestTokenService::class);
|
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
|
||||||
$this->action = new AuthenticateAction($this->tokenService->reveal(), Translator::factory([]));
|
$this->jwtService = $this->prophesize(JWTService::class);
|
||||||
|
$this->action = new AuthenticateAction(
|
||||||
|
$this->apiKeyService->reveal(),
|
||||||
|
$this->jwtService->reveal(),
|
||||||
|
Translator::factory([])
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,34 +49,31 @@ class AuthenticateActionTest extends TestCase
|
|||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function properCredentialsReturnTokenInResponse()
|
public function properApiKeyReturnsTokenInResponse()
|
||||||
{
|
{
|
||||||
$this->tokenService->createToken('foo', 'bar')->willReturn(
|
$this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setId(5))
|
||||||
(new RestToken())->setToken('abc-ABC')
|
->shouldBeCalledTimes(1);
|
||||||
)->shouldBeCalledTimes(1);
|
|
||||||
|
|
||||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||||
'username' => 'foo',
|
'apiKey' => 'foo',
|
||||||
'password' => 'bar',
|
|
||||||
]);
|
]);
|
||||||
$response = $this->action->__invoke($request, new Response());
|
$response = $this->action->__invoke($request, new Response());
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
|
||||||
$response->getBody()->rewind();
|
$response->getBody()->rewind();
|
||||||
$this->assertEquals(['token' => 'abc-ABC'], json_decode($response->getBody()->getContents(), true));
|
$this->assertTrue(strpos($response->getBody()->getContents(), '"token"') > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function authenticationExceptionsReturnErrorResponse()
|
public function invalidApiKeyReturnsErrorResponse()
|
||||||
{
|
{
|
||||||
$this->tokenService->createToken('foo', 'bar')->willThrow(new AuthenticationException())
|
$this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setEnabled(false))
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||||
'username' => 'foo',
|
'apiKey' => 'foo',
|
||||||
'password' => 'bar',
|
|
||||||
]);
|
]);
|
||||||
$response = $this->action->__invoke($request, new Response());
|
$response = $this->action->__invoke($request, new Response());
|
||||||
$this->assertEquals(401, $response->getStatusCode());
|
$this->assertEquals(401, $response->getStatusCode());
|
||||||
|
@ -59,7 +59,7 @@ class GetVisitsActionTest extends TestCase
|
|||||||
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
|
||||||
new Response()
|
new Response()
|
||||||
);
|
);
|
||||||
$this->assertEquals(400, $response->getStatusCode());
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -39,7 +39,7 @@ class ResolveUrlActionTest extends TestCase
|
|||||||
|
|
||||||
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
|
$request = ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode);
|
||||||
$response = $this->action->__invoke($request, new Response());
|
$response = $this->action->__invoke($request, new Response());
|
||||||
$this->assertEquals(400, $response->getStatusCode());
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_ARGUMENT_ERROR) > 0);
|
$this->assertTrue(strpos($response->getBody()->getContents(), RestUtils::INVALID_ARGUMENT_ERROR) > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
93
module/Rest/test/Authentication/JWTServiceTest.php
Normal file
93
module/Rest/test/Authentication/JWTServiceTest.php
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Rest\Authentication;
|
||||||
|
|
||||||
|
use Firebase\JWT\JWT;
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
|
use Shlinkio\Shlink\Rest\Authentication\JWTService;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
|
class JWTServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var JWTService
|
||||||
|
*/
|
||||||
|
protected $service;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->service = new JWTService(new AppOptions([
|
||||||
|
'name' => 'ShlinkTest',
|
||||||
|
'version' => '10000.3.1',
|
||||||
|
'secret_key' => 'foo',
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function tokenIsProperlyCreated()
|
||||||
|
{
|
||||||
|
$id = 34;
|
||||||
|
$token = $this->service->create((new ApiKey())->setId($id));
|
||||||
|
$payload = (array) JWT::decode($token, 'foo', [JWTService::DEFAULT_ENCRYPTION_ALG]);
|
||||||
|
$this->assertGreaterThanOrEqual($payload['iat'], time());
|
||||||
|
$this->assertGreaterThan(time(), $payload['exp']);
|
||||||
|
$this->assertEquals($id, $payload['key']);
|
||||||
|
$this->assertEquals('auth', $payload['sub']);
|
||||||
|
$this->assertEquals('ShlinkTest:v10000.3.1', $payload['iss']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function refreshIncreasesExpiration()
|
||||||
|
{
|
||||||
|
$originalLifetime = 10;
|
||||||
|
$newLifetime = 30;
|
||||||
|
$originalPayload = ['exp' => time() + $originalLifetime];
|
||||||
|
$token = JWT::encode($originalPayload, 'foo');
|
||||||
|
$newToken = $this->service->refresh($token, $newLifetime);
|
||||||
|
$newPayload = (array) JWT::decode($newToken, 'foo', [JWTService::DEFAULT_ENCRYPTION_ALG]);
|
||||||
|
|
||||||
|
$this->assertGreaterThan($originalPayload['exp'], $newPayload['exp']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function verifyReturnsTrueWhenTheTokenIsCorrect()
|
||||||
|
{
|
||||||
|
$this->assertTrue($this->service->verify(JWT::encode([], 'foo')));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function verifyReturnsFalseWhenTheTokenIsCorrect()
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->service->verify('invalidToken'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function getPayloadWorksWithCorrectTokens()
|
||||||
|
{
|
||||||
|
$originalPayload = [
|
||||||
|
'exp' => time() + 10,
|
||||||
|
'sub' => 'testing',
|
||||||
|
];
|
||||||
|
$token = JWT::encode($originalPayload, 'foo');
|
||||||
|
$this->assertEquals($originalPayload, $this->service->getPayload($token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @expectedException \Shlinkio\Shlink\Rest\Exception\AuthenticationException
|
||||||
|
*/
|
||||||
|
public function getPayloadThrowsExceptionWithIncorrectTokens()
|
||||||
|
{
|
||||||
|
$this->service->getPayload('invalidToken');
|
||||||
|
}
|
||||||
|
}
|
@ -3,9 +3,8 @@ namespace ShlinkioTest\Shlink\Rest\Middleware;
|
|||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\Core\Entity\RestToken;
|
use Shlinkio\Shlink\Rest\Authentication\JWTService;
|
||||||
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
|
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
|
||||||
use Shlinkio\Shlink\Rest\Service\RestTokenService;
|
|
||||||
use Zend\Diactoros\Response;
|
use Zend\Diactoros\Response;
|
||||||
use Zend\Diactoros\ServerRequestFactory;
|
use Zend\Diactoros\ServerRequestFactory;
|
||||||
use Zend\Expressive\Router\RouteResult;
|
use Zend\Expressive\Router\RouteResult;
|
||||||
@ -20,18 +19,18 @@ class CheckAuthenticationMiddlewareTest extends TestCase
|
|||||||
/**
|
/**
|
||||||
* @var ObjectProphecy
|
* @var ObjectProphecy
|
||||||
*/
|
*/
|
||||||
protected $tokenService;
|
protected $jwtService;
|
||||||
|
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
$this->tokenService = $this->prophesize(RestTokenService::class);
|
$this->jwtService = $this->prophesize(JWTService::class);
|
||||||
$this->middleware = new CheckAuthenticationMiddleware($this->tokenService->reveal(), Translator::factory([]));
|
$this->middleware = new CheckAuthenticationMiddleware($this->jwtService->reveal(), Translator::factory([]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function someWhitelistedSituationsFallbackToNextMiddleware()
|
public function someWhiteListedSituationsFallbackToNextMiddleware()
|
||||||
{
|
{
|
||||||
$request = ServerRequestFactory::fromGlobals();
|
$request = ServerRequestFactory::fromGlobals();
|
||||||
$response = new Response();
|
$response = new Response();
|
||||||
@ -92,6 +91,40 @@ class CheckAuthenticationMiddlewareTest extends TestCase
|
|||||||
$this->assertEquals(401, $response->getStatusCode());
|
$this->assertEquals(401, $response->getStatusCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function provideAnAuthorizationWithoutTypeReturnsError()
|
||||||
|
{
|
||||||
|
$authToken = 'ABC-abc';
|
||||||
|
$request = ServerRequestFactory::fromGlobals()->withAttribute(
|
||||||
|
RouteResult::class,
|
||||||
|
RouteResult::fromRouteMatch('bar', 'foo', [])
|
||||||
|
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, $authToken);
|
||||||
|
|
||||||
|
$response = $this->middleware->__invoke($request, new Response());
|
||||||
|
$this->assertEquals(401, $response->getStatusCode());
|
||||||
|
$this->assertTrue(strpos($response->getBody()->getContents(), 'You need to provide the Bearer type') > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function provideAnAuthorizationWithWrongTypeReturnsError()
|
||||||
|
{
|
||||||
|
$authToken = 'ABC-abc';
|
||||||
|
$request = ServerRequestFactory::fromGlobals()->withAttribute(
|
||||||
|
RouteResult::class,
|
||||||
|
RouteResult::fromRouteMatch('bar', 'foo', [])
|
||||||
|
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Basic ' . $authToken);
|
||||||
|
|
||||||
|
$response = $this->middleware->__invoke($request, new Response());
|
||||||
|
$this->assertEquals(401, $response->getStatusCode());
|
||||||
|
$this->assertTrue(
|
||||||
|
strpos($response->getBody()->getContents(), 'Provided authorization type Basic is not supported') > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
@ -101,10 +134,8 @@ class CheckAuthenticationMiddlewareTest extends TestCase
|
|||||||
$request = ServerRequestFactory::fromGlobals()->withAttribute(
|
$request = ServerRequestFactory::fromGlobals()->withAttribute(
|
||||||
RouteResult::class,
|
RouteResult::class,
|
||||||
RouteResult::fromRouteMatch('bar', 'foo', [])
|
RouteResult::fromRouteMatch('bar', 'foo', [])
|
||||||
)->withHeader(CheckAuthenticationMiddleware::AUTH_TOKEN_HEADER, $authToken);
|
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'Bearer ' . $authToken);
|
||||||
$this->tokenService->getByToken($authToken)->willReturn(
|
$this->jwtService->verify($authToken)->willReturn(false)->shouldBeCalledTimes(1);
|
||||||
(new RestToken())->setExpirationDate((new \DateTime())->sub(new \DateInterval('P1D')))
|
|
||||||
)->shouldBeCalledTimes(1);
|
|
||||||
|
|
||||||
$response = $this->middleware->__invoke($request, new Response());
|
$response = $this->middleware->__invoke($request, new Response());
|
||||||
$this->assertEquals(401, $response->getStatusCode());
|
$this->assertEquals(401, $response->getStatusCode());
|
||||||
@ -116,18 +147,18 @@ class CheckAuthenticationMiddlewareTest extends TestCase
|
|||||||
public function provideCorrectTokenUpdatesExpirationAndFallbacksToNextMiddleware()
|
public function provideCorrectTokenUpdatesExpirationAndFallbacksToNextMiddleware()
|
||||||
{
|
{
|
||||||
$authToken = 'ABC-abc';
|
$authToken = 'ABC-abc';
|
||||||
$restToken = (new RestToken())->setExpirationDate((new \DateTime())->add(new \DateInterval('P1D')));
|
|
||||||
$request = ServerRequestFactory::fromGlobals()->withAttribute(
|
$request = ServerRequestFactory::fromGlobals()->withAttribute(
|
||||||
RouteResult::class,
|
RouteResult::class,
|
||||||
RouteResult::fromRouteMatch('bar', 'foo', [])
|
RouteResult::fromRouteMatch('bar', 'foo', [])
|
||||||
)->withHeader(CheckAuthenticationMiddleware::AUTH_TOKEN_HEADER, $authToken);
|
)->withHeader(CheckAuthenticationMiddleware::AUTHORIZATION_HEADER, 'bearer ' . $authToken);
|
||||||
$this->tokenService->getByToken($authToken)->willReturn($restToken)->shouldBeCalledTimes(1);
|
$this->jwtService->verify($authToken)->willReturn(true)->shouldBeCalledTimes(1);
|
||||||
$this->tokenService->updateExpiration($restToken)->shouldBeCalledTimes(1);
|
$this->jwtService->refresh($authToken)->willReturn($authToken)->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$isCalled = false;
|
$isCalled = false;
|
||||||
$this->assertFalse($isCalled);
|
$this->assertFalse($isCalled);
|
||||||
$this->middleware->__invoke($request, new Response(), function ($req, $resp) use (&$isCalled) {
|
$this->middleware->__invoke($request, new Response(), function ($req, $resp) use (&$isCalled) {
|
||||||
$isCalled = true;
|
$isCalled = true;
|
||||||
|
return $resp;
|
||||||
});
|
});
|
||||||
$this->assertTrue($isCalled);
|
$this->assertTrue($isCalled);
|
||||||
}
|
}
|
||||||
|
168
module/Rest/test/Service/ApiKeyServiceTest.php
Normal file
168
module/Rest/test/Service/ApiKeyServiceTest.php
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Rest\Service;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityManager;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
|
||||||
|
|
||||||
|
class ApiKeyServiceTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ApiKeyService
|
||||||
|
*/
|
||||||
|
protected $service;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $em;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->em = $this->prophesize(EntityManager::class);
|
||||||
|
$this->service = new ApiKeyService($this->em->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function keyIsProperlyCreated()
|
||||||
|
{
|
||||||
|
$this->em->flush()->shouldBeCalledTimes(1);
|
||||||
|
$this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$key = $this->service->create();
|
||||||
|
$this->assertNull($key->getExpirationDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function keyIsProperlyCreatedWithExpirationDate()
|
||||||
|
{
|
||||||
|
$this->em->flush()->shouldBeCalledTimes(1);
|
||||||
|
$this->em->persist(Argument::type(ApiKey::class))->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$date = new \DateTime('2030-01-01');
|
||||||
|
$key = $this->service->create($date);
|
||||||
|
$this->assertSame($date, $key->getExpirationDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function checkReturnsFalseWhenKeyIsInvalid()
|
||||||
|
{
|
||||||
|
$repo = $this->prophesize(EntityRepository::class);
|
||||||
|
$repo->findOneBy(['key' => '12345'])->willReturn(null)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$this->assertFalse($this->service->check('12345'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function checkReturnsFalseWhenKeyIsDisabled()
|
||||||
|
{
|
||||||
|
$key = new ApiKey();
|
||||||
|
$key->disable();
|
||||||
|
$repo = $this->prophesize(EntityRepository::class);
|
||||||
|
$repo->findOneBy(['key' => '12345'])->willReturn($key)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$this->assertFalse($this->service->check('12345'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function checkReturnsFalseWhenKeyIsExpired()
|
||||||
|
{
|
||||||
|
$key = new ApiKey();
|
||||||
|
$key->setExpirationDate((new \DateTime())->sub(new \DateInterval('P1D')));
|
||||||
|
$repo = $this->prophesize(EntityRepository::class);
|
||||||
|
$repo->findOneBy(['key' => '12345'])->willReturn($key)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$this->assertFalse($this->service->check('12345'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function checkReturnsTrueWhenConditionsAreFavorable()
|
||||||
|
{
|
||||||
|
$repo = $this->prophesize(EntityRepository::class);
|
||||||
|
$repo->findOneBy(['key' => '12345'])->willReturn(new ApiKey())
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$this->assertTrue($this->service->check('12345'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @expectedException \Shlinkio\Shlink\Common\Exception\InvalidArgumentException
|
||||||
|
*/
|
||||||
|
public function disableThrowsExceptionWhenNoTokenIsFound()
|
||||||
|
{
|
||||||
|
$repo = $this->prophesize(EntityRepository::class);
|
||||||
|
$repo->findOneBy(['key' => '12345'])->willReturn(null)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$this->service->disable('12345');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function disableReturnsDisabledKeyWhenFOund()
|
||||||
|
{
|
||||||
|
$key = new ApiKey();
|
||||||
|
$repo = $this->prophesize(EntityRepository::class);
|
||||||
|
$repo->findOneBy(['key' => '12345'])->willReturn($key)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$this->em->flush()->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$this->assertTrue($key->isEnabled());
|
||||||
|
$returnedKey = $this->service->disable('12345');
|
||||||
|
$this->assertFalse($key->isEnabled());
|
||||||
|
$this->assertSame($key, $returnedKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function listFindsAllApiKeys()
|
||||||
|
{
|
||||||
|
$repo = $this->prophesize(EntityRepository::class);
|
||||||
|
$repo->findBy([])->willReturn([])
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$this->service->listKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function listEnabledFindsOnlyEnabledApiKeys()
|
||||||
|
{
|
||||||
|
$repo = $this->prophesize(EntityRepository::class);
|
||||||
|
$repo->findBy(['enabled' => true])->willReturn([])
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->em->getRepository(ApiKey::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$this->service->listKeys(true);
|
||||||
|
}
|
||||||
|
}
|
@ -1,93 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace ShlinkioTest\Shlink\Rest\Service;
|
|
||||||
|
|
||||||
use Doctrine\ORM\EntityManager;
|
|
||||||
use Doctrine\ORM\EntityRepository;
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
|
||||||
use Prophecy\Argument;
|
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
|
||||||
use Shlinkio\Shlink\Core\Entity\RestToken;
|
|
||||||
use Shlinkio\Shlink\Rest\Service\RestTokenService;
|
|
||||||
|
|
||||||
class RestTokenServiceTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var RestTokenService
|
|
||||||
*/
|
|
||||||
protected $service;
|
|
||||||
/**
|
|
||||||
* @var ObjectProphecy
|
|
||||||
*/
|
|
||||||
protected $em;
|
|
||||||
|
|
||||||
public function setUp()
|
|
||||||
{
|
|
||||||
$this->em = $this->prophesize(EntityManager::class);
|
|
||||||
$this->service = new RestTokenService($this->em->reveal(), [
|
|
||||||
'username' => 'foo',
|
|
||||||
'password' => 'bar',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function tokenIsCreatedIfCredentialsAreCorrect()
|
|
||||||
{
|
|
||||||
$this->em->persist(Argument::type(RestToken::class))->shouldBeCalledTimes(1);
|
|
||||||
$this->em->flush()->shouldBeCalledTimes(1);
|
|
||||||
|
|
||||||
$token = $this->service->createToken('foo', 'bar');
|
|
||||||
$this->assertInstanceOf(RestToken::class, $token);
|
|
||||||
$this->assertFalse($token->isExpired());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
* @expectedException \Shlinkio\Shlink\Rest\Exception\AuthenticationException
|
|
||||||
*/
|
|
||||||
public function exceptionIsThrownWhileCreatingTokenWithWrongCredentials()
|
|
||||||
{
|
|
||||||
$this->service->createToken('foo', 'wrong');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function restTokenIsReturnedFromTokenString()
|
|
||||||
{
|
|
||||||
$authToken = 'ABC-abc';
|
|
||||||
$theToken = new RestToken();
|
|
||||||
$repo = $this->prophesize(EntityRepository::class);
|
|
||||||
$repo->findOneBy(['token' => $authToken])->willReturn($theToken)->shouldBeCalledTimes(1);
|
|
||||||
$this->em->getRepository(RestToken::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);
|
|
||||||
|
|
||||||
$this->assertSame($theToken, $this->service->getByToken($authToken));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
* @expectedException \Shlinkio\Shlink\Common\Exception\InvalidArgumentException
|
|
||||||
*/
|
|
||||||
public function exceptionIsThrownWhenRequestingWrongToken()
|
|
||||||
{
|
|
||||||
$authToken = 'ABC-abc';
|
|
||||||
$repo = $this->prophesize(EntityRepository::class);
|
|
||||||
$repo->findOneBy(['token' => $authToken])->willReturn(null)->shouldBeCalledTimes(1);
|
|
||||||
$this->em->getRepository(RestToken::class)->willReturn($repo->reveal())->shouldBeCalledTimes(1);
|
|
||||||
|
|
||||||
$this->service->getByToken($authToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function updateExpirationFlushesEntityManager()
|
|
||||||
{
|
|
||||||
$token = $this->prophesize(RestToken::class);
|
|
||||||
$token->updateExpiration()->shouldBeCalledTimes(1);
|
|
||||||
$this->em->flush()->shouldBeCalledTimes(1);
|
|
||||||
|
|
||||||
$this->service->updateExpiration($token->reveal());
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user