Merge pull request #590 from acelaya-forks/feature/remove-deprecations

Feature/remove deprecations
This commit is contained in:
Alejandro Celaya 2019-12-31 23:51:27 +01:00 committed by GitHub
commit f172146f27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
107 changed files with 94 additions and 2682 deletions

2
.gitattributes vendored
View file

@ -5,8 +5,6 @@
/module/CLI/test-resources export-ignore
/module/Core/test export-ignore
/module/Core/test-db export-ignore
/module/PreviewGenerator/test export-ignore
/module/PreviewGenerator/test-db export-ignore
/module/Rest/test export-ignore
/module/Rest/test-api export-ignore
.gitattributes export-ignore

View file

@ -22,6 +22,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
* [#429](https://github.com/shlinkio/shlink/issues/429) Dropped support for PHP 7.2 and 7.3
* [#229](https://github.com/shlinkio/shlink/issues/229) Remove everything which was deprecated, including:
* Preview generation feature completely removed.
* Authentication against REST API using JWT is no longer supported.
See [UPGRADING doc](UPGRADING.md) in order to get details on how to migrate to this version.
#### Fixed
* *Nothing*

View file

@ -215,12 +215,6 @@ Those tasks can be performed using shlink's CLI tool, so it should be easy to sc
> You don't need this if you use Shlink v1.17.0 or newer, since now it downloads/updates the geolocation database automatically just before trying to use it.
* Generate website previews: `/path/to/shlink/bin/cli short-url:process-previews`
Running this will improve the performance of the `doma.in/abc123/preview` URLs, which return a preview of the site.
> **Important!** Generating previews is considered deprecated and the feature will be removed in Shlink v2.
*Any of these commands accept the `-q` flag, which makes it not display any output. This is recommended when configuring the commands as cron jobs.*
## Update to new version
@ -274,33 +268,28 @@ Options:
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Available commands:
help Displays help for a command
list Lists commands
help Displays help for a command
list Lists commands
api-key
api-key:disable Disables an API key.
api-key:generate Generates a new valid API key.
api-key:list Lists all the available API keys.
config
config:generate-charset [DEPRECATED] Generates a character set sample just by shuffling the default one, "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ". Then it can be set in the SHORTCODE_CHARS environment variable
config:generate-secret [DEPRECATED] Generates a random secret string that can be used for JWT token encryption
api-key:disable Disables an API key.
api-key:generate Generates a new valid API key.
api-key:list Lists all the available API keys.
db
db:create Creates the database needed for shlink to work. It will do nothing if the database already exists
db:migrate Runs database migrations, which will ensure the shlink database is up to date.
db:create Creates the database needed for shlink to work. It will do nothing if the database already exists
db:migrate Runs database migrations, which will ensure the shlink database is up to date.
short-url
short-url:delete [short-code:delete] Deletes a short URL
short-url:generate [shortcode:generate|short-code:generate] Generates a short URL for provided long URL and returns it
short-url:list [shortcode:list|short-code:list] List all short URLs
short-url:parse [shortcode:parse|short-code:parse] Returns the long URL behind a short code
short-url:process-previews [shortcode:process-previews|short-code:process-previews] [DEPRECATED] Processes and generates the previews for every URL, improving performance for later web requests.
short-url:visits [shortcode:visits|short-code:visits] Returns the detailed visits information for provided short code
short-url:delete Deletes a short URL
short-url:generate Generates a short URL for provided long URL and returns it
short-url:list List all short URLs
short-url:parse Returns the long URL behind a short code
short-url:visits Returns the detailed visits information for provided short code
tag
tag:create Creates one or more tags.
tag:delete Deletes one or more tags.
tag:list Lists existing tags.
tag:rename Renames one existing tag.
tag:create Creates one or more tags.
tag:delete Deletes one or more tags.
tag:list Lists existing tags.
tag:rename Renames one existing tag.
visit
visit:locate [visit:process] Resolves visits origin locations.
visit:update-db [DEPRECATED] Updates the GeoLite2 database file used to geolocate IP addresses
visit:locate Resolves visits origin locations.
```
> This product includes GeoLite2 data created by MaxMind, available from [https://www.maxmind.com](https://www.maxmind.com)

Binary file not shown.

View file

@ -71,8 +71,7 @@
"psr-4": {
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
"Shlinkio\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/src/"
"Shlinkio\\Shlink\\Core\\": "module/Core/src"
},
"files": [
"module/Core/functions/functions.php"
@ -86,8 +85,7 @@
"ShlinkioTest\\Shlink\\Core\\": [
"module/Core/test",
"module/Core/test-db"
],
"ShlinkioTest\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/test"
]
}
},
"scripts": {

View file

@ -2,14 +2,11 @@
declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
return [
'app_options' => [
'name' => 'Shlink',
'version' => '%SHLINK_VERSION%',
'secret_key' => env('SECRET_KEY', ''),
'disable_track_param' => null,
],

View file

@ -15,10 +15,6 @@ return [
],
],
'backwards_compatible_problem_details' => [
'json_flags' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION,
],
'error_handler' => [
'listeners' => [Logger\ErrorLogger::class],
],

View file

@ -21,7 +21,6 @@ return [
'path' => '/rest',
'middleware' => [
Rest\Middleware\CrossDomainMiddleware::class,
Rest\Middleware\BackwardsCompatibleProblemDetailsMiddleware::class,
ProblemDetails\ProblemDetailsMiddleware::class,
],
],
@ -31,13 +30,6 @@ return [
Common\Middleware\CloseDbConnectionMiddleware::class,
],
],
'pre-routing-rest' => [
'path' => '/rest',
'middleware' => [
Rest\Middleware\PathVersionMiddleware::class,
Rest\Middleware\ShortUrl\ShortCodePathMiddleware::class,
],
],
'routing' => [
'middleware' => [

View file

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
/* @deprecated */
return [
'preview_generation' => [
'files_location' => 'data/cache',
],
];

View file

@ -9,7 +9,7 @@ return [
'schema' => 'https',
'hostname' => '',
],
'validate_url' => true,
'validate_url' => false,
'visits_webhooks' => [],
],

View file

@ -23,7 +23,6 @@ return (new ConfigAggregator\ConfigAggregator([
CLI\ConfigProvider::class,
Rest\ConfigProvider::class,
EventDispatcher\ConfigProvider::class,
PreviewGenerator\ConfigProvider::class,
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
env('APP_ENV') === 'test'
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')

View file

@ -61,6 +61,7 @@ return [
'schema' => 'http',
'hostname' => 'doma.in',
],
'validate_url' => true,
],
'zend-expressive-swoole' => [

View file

@ -103,7 +103,7 @@ This is the complete list of supported env vars:
* **postgres** -> `5432`
* `DISABLE_TRACK_PARAM`: The name of a query param that can be used to visit short URLs avoiding the visit to be tracked. This feature won't be available if not value is provided.
* `DELETE_SHORT_URL_THRESHOLD`: The amount of visits on short URLs which will not allow them to be deleted. Defaults to `15`.
* `VALIDATE_URLS`: Boolean which tells if shlink should validate a status 20x (after following redirects) is returned when trying to shorten a URL. Defaults to `true`.
* `VALIDATE_URLS`: Boolean which tells if shlink should validate a status 20x is returned (after following redirects) when trying to shorten a URL. Defaults to `false`.
* `INVALID_SHORT_URL_REDIRECT_TO`: If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `REGULAR_404_REDIRECT_TO`: If a URL is provided here, when a user tries to access a URL not matching any one supported by the router, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `BASE_URL_REDIRECT_TO`: If a URL is provided here, when a user tries to access Shlink's base URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
@ -119,7 +119,6 @@ This is the complete list of supported env vars:
In the future, these redis servers could be used for other caching operations performed by shlink.
* `NOT_FOUND_REDIRECT_TO`: **Deprecated since v1.20 in favor of `INVALID_SHORT_URL_REDIRECT_TO`** If a URL is provided here, when a user tries to access an invalid short URL, he/she will be redirected to this value. If this env var is not provided, the user will see a generic `404 - not found` page.
* `SHORTCODE_CHARS`: **Ignored when using Shlink 1.20 or newer**. A charset to use when building short codes. Only needed when using more than one shlink instance ([Multi instance considerations](#multi-instance-considerations)).
An example using all env vars could look like this:
@ -138,7 +137,7 @@ docker run \
-e DB_PORT=3306 \
-e DISABLE_TRACK_PARAM="no-track" \
-e DELETE_SHORT_URL_THRESHOLD=30 \
-e VALIDATE_URLS=false \
-e VALIDATE_URLS=true \
-e "INVALID_SHORT_URL_REDIRECT_TO=https://my-landing-page.com" \
-e "REGULAR_404_REDIRECT_TO=https://my-landing-page.com" \
-e "BASE_URL_REDIRECT_TO=https://my-landing-page.com" \
@ -164,7 +163,7 @@ The whole configuration should have this format, but it can be split into multip
"delete_short_url_threshold": 30,
"short_domain_schema": "https",
"short_domain_host": "doma.in",
"validate_url": false,
"validate_url": true,
"invalid_short_url_redirect_to": "https://my-landing-page.com",
"regular_404_redirect_to": "https://my-landing-page.com",
"base_url_redirect_to": "https://my-landing-page.com",
@ -186,15 +185,12 @@ The whole configuration should have this format, but it can be split into multip
"password": "123abc",
"host": "something.rds.amazonaws.com",
"port": "3306"
},
"not_found_redirect_to": "https://my-landing-page.com"
}
}
```
> This is internally parsed to how shlink expects the config. If you are using a version previous to 1.17.0, this parser is not present and you need to provide a config structure like the one [documented previously](https://github.com/shlinkio/shlink-docker-image/tree/v1.16.3#provide-config-via-volumes).
> The `not_found_redirect_to` option has been deprecated in v1.20. Use `invalid_short_url_redirect_to` instead (however, it will still work for backwards compatibility).
Once created just run shlink with the volume:
```bash

View file

@ -8,19 +8,10 @@ use Monolog\Handler\StreamHandler;
use Monolog\Logger;
use function explode;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function Functional\contains;
use function implode;
use function Shlinkio\Shlink\Common\env;
use function sprintf;
use function str_shuffle;
use function substr;
use function sys_get_temp_dir;
$helper = new class {
private const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
private const DB_DRIVERS_MAP = [
'mysql' => 'pdo_mysql',
'maria' => 'pdo_mysql',
@ -32,40 +23,6 @@ $helper = new class {
'postgres' => '5432',
];
/** @var string */
private $secretKey;
public function __construct()
{
[, $this->secretKey] = $this->initShlinkSecretKey();
}
private function initShlinkSecretKey(): array
{
$keysFile = sprintf('%s/shlink.keys', sys_get_temp_dir());
if (file_exists($keysFile)) {
return explode(',', file_get_contents($keysFile));
}
$keys = [
'', // This was the SHORTCODE_CHARS. Kept as empty string for BC
env('SECRET_KEY', $this->generateSecretKey()), // Deprecated
];
file_put_contents($keysFile, implode(',', $keys));
return $keys;
}
private function generateSecretKey(): string
{
return substr(str_shuffle(self::BASE62), 0, 32);
}
public function getSecretKey(): string
{
return $this->secretKey;
}
public function getDbConfig(): array
{
$driver = env('DB_DRIVER');
@ -94,7 +51,7 @@ $helper = new class {
public function getNotFoundRedirectsConfig(): array
{
return [
'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO', env('NOT_FOUND_REDIRECT_TO')),
'invalid_short_url' => env('INVALID_SHORT_URL_REDIRECT_TO'),
'regular_404' => env('REGULAR_404_REDIRECT_TO'),
'base_url' => env('BASE_URL_REDIRECT_TO'),
];
@ -112,7 +69,6 @@ return [
'config_cache_enabled' => false,
'app_options' => [
'secret_key' => $helper->getSecretKey(),
'disable_track_param' => env('DISABLE_TRACK_PARAM'),
],
@ -130,7 +86,7 @@ return [
'schema' => env('SHORT_DOMAIN_SCHEMA', 'http'),
'hostname' => env('SHORT_DOMAIN_HOST', ''),
],
'validate_url' => (bool) env('VALIDATE_URLS', true),
'validate_url' => (bool) env('VALIDATE_URLS', false),
'visits_webhooks' => $helper->getVisitsWebhooks(),
],

View file

@ -17,16 +17,6 @@
"status": {
"type": "number",
"description": "HTTP response status code"
},
"code": {
"type": "string",
"description": "**[Deprecated] Use type instead. Not returned for v2 of the REST API** A machine unique code",
"deprecated": true
},
"message": {
"type": "string",
"description": "**[Deprecated] Use detail instead. Not returned for v2 of the REST API** A human-friendly error message",
"deprecated": true
}
}
}

View file

@ -31,11 +31,6 @@
},
"meta": {
"$ref": "./ShortUrlMeta.json"
},
"originalUrl": {
"deprecated": true,
"type": "string",
"description": "The original long URL. [DEPRECATED. Use longUrl instead]"
}
}
}

View file

@ -16,11 +16,6 @@
},
"visitLocation": {
"$ref": "./VisitLocation.json"
},
"remoteAddr": {
"type": "string",
"description": "This value is deprecated and will always be null",
"deprecated": true
}
}
}

View file

@ -11,10 +11,10 @@
"type": "string"
},
"latitude": {
"type": "string"
"type": "number"
},
"longitude": {
"type": "string"
"type": "number"
},
"regionName": {
"type": "string"

View file

@ -1,84 +0,0 @@
{
"post": {
"deprecated": true,
"operationId": "authenticate",
"tags": [
"Authentication"
],
"summary": "[Deprecated] Perform authentication",
"description": "**This endpoint is deprecated, since the authentication can be performed via API key now**. Performs an authentication.",
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"apiKey"
],
"properties": {
"apiKey": {
"description": "The API key to authenticate with",
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "The authentication worked.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"token": {
"type": "string",
"description": "The authentication token that needs to be sent in the Authorization header"
}
}
}
}
},
"examples": {
"application/json": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
}
}
},
"400": {
"description": "An API key was not provided.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"401": {
"description": "The API key is incorrect, is disabled or has expired.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View file

@ -5,7 +5,7 @@
"Short URLs"
],
"summary": "List short URLs",
"description": "Returns the list of short URLs.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
"description": "Returns the list of short URLs.",
"parameters": [
{
"$ref": "../parameters/version.json"
@ -77,9 +77,6 @@
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"responses": {
@ -187,13 +184,10 @@
"Short URLs"
],
"summary": "Create short URL",
"description": "Creates a new short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.<br></br>**Param findIfExists:**: Starting with v1.16, this new param allows to force shlink to return existing short URLs when found based on provided params, instead of creating a new one. However, it might add complexity and have unexpected outputs.\n\nThese are the use cases:\n* Only the long URL is provided: It will return the newest match or create a new short URL if none is found.\n* Long url and custom slug are provided: It will return the short URL when both params match, return an error when the slug is in use for another long URL, or create a new short URL otherwise.\n* Any of the above but including other params (tags, validSince, validUntil, maxVisits): It will behave the same as the previous two cases, but it will try to exactly match existing results using all the params. If any of them does not match, it will try to create a new short URL.",
"description": "Creates a new short URL.<br></br>**Param findIfExists:**: Starting with v1.16, this new param allows to force shlink to return existing short URLs when found based on provided params, instead of creating a new one. However, it might add complexity and have unexpected outputs.\n\nThese are the use cases:\n* Only the long URL is provided: It will return the newest match or create a new short URL if none is found.\n* Long url and custom slug are provided: It will return the short URL when both params match, return an error when the slug is in use for another long URL, or create a new short URL otherwise.\n* Any of the above but including other params (tags, validSince, validUntil, maxVisits): It will behave the same as the previous two cases, but it will try to exactly match existing results using all the params. If any of them does not match, it will try to create a new short URL.",
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"parameters": [

View file

@ -5,7 +5,7 @@
"Short URLs"
],
"summary": "Create a short URL",
"description": "Creates a short URL in a single API call. Useful for third party integrations.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
"description": "Creates a short URL in a single API call. Useful for third party integrations.",
"parameters": [
{
"$ref": "../parameters/version.json"

View file

@ -5,7 +5,7 @@
"Short URLs"
],
"summary": "Parse short code",
"description": "Get the long URL behind a short URL's short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
"description": "Get the long URL behind a short URL's short code.",
"parameters": [
{
"$ref": "../parameters/version.json"
@ -32,9 +32,6 @@
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"responses": {
@ -94,7 +91,7 @@
"Short URLs"
],
"summary": "Edit short URL",
"description": "Update certain meta arguments from an existing short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
"description": "Update certain meta arguments from an existing short URL.",
"parameters": [
{
"$ref": "../parameters/version.json"
@ -137,9 +134,6 @@
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"responses": {
@ -201,105 +195,13 @@
}
},
"put": {
"deprecated": true,
"operationId": "editShortUrlPut",
"tags": [
"Short URLs"
],
"summary": "[DEPRECATED] Edit short URL",
"description": "**[DEPRECATED]** Use [editShortUrl](#/Short_URLs/getShortUrl) instead",
"parameters": [
{
"$ref": "../parameters/version.json"
},
{
"name": "shortCode",
"in": "path",
"description": "The short code to edit.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": "string"
},
"validUntil": {
"description": "The date (in ISO-8601 format) until which this short code will be valid",
"type": "string"
},
"maxVisits": {
"description": "The maximum number of allowed visits for this short code",
"type": "number"
}
}
}
}
}
},
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"responses": {
"204": {
"description": "The short code has been properly updated."
},
"400": {
"description": "Provided meta arguments are invalid.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"404": {
"description": "No short URL was found for provided short code.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
},
"delete": {
"operationId": "deleteShortUrl",
"tags": [
"Short URLs"
],
"summary": "Delete short URL",
"description": "Deletes the short URL for provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
"description": "Deletes the short URL for provided short code.",
"parameters": [
{
"$ref": "../parameters/version.json"
@ -317,9 +219,6 @@
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"responses": {

View file

@ -5,7 +5,7 @@
"Short URLs"
],
"summary": "Edit tags on short URL",
"description": "Edit the tags on URL identified by provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
"description": "Edit the tags on URL identified by provided short code.",
"parameters": [
{
"$ref": "../parameters/version.json"
@ -46,9 +46,6 @@
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"responses": {

View file

@ -5,7 +5,7 @@
"Visits"
],
"summary": "List visits for short URL",
"description": "Get the list of visits on the short URL behind provided short code.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.",
"description": "Get the list of visits on the short URL behind provided short code.",
"parameters": [
{
"$ref": "../parameters/version.json"
@ -59,9 +59,6 @@
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"responses": {
@ -108,8 +105,8 @@
"cityName": "Cupertino",
"countryCode": "US",
"countryName": "United States",
"latitude": "37.3042",
"longitude": "-122.0946",
"latitude": 37.3042,
"longitude": -122.0946,
"regionName": "California",
"timezone": "America/Los_Angeles"
}

View file

@ -9,9 +9,6 @@
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"parameters": [
@ -78,9 +75,6 @@
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"parameters": [
@ -170,9 +164,6 @@
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"parameters": [
@ -279,9 +270,6 @@
"security": [
{
"ApiKey": []
},
{
"Bearer": []
}
],
"responses": {

View file

@ -1,35 +0,0 @@
{
"get": {
"deprecated": true,
"operationId": "shortUrlPreview",
"tags": [
"URL Shortener"
],
"summary": "Short URL preview image",
"description": "Returns the preview of the page behind a short URL",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Image in PNG format",
"content": {
"image/png": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
}
}

View file

@ -33,12 +33,6 @@
"type": "apiKey",
"in": "header",
"name": "X-Api-Key"
},
"Bearer": {
"description": "**[DEPRECATED]** The JWT identifying a previously authenticated API key",
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
@ -63,10 +57,6 @@
{
"name": "URL Shortener",
"description": "Non-rest endpoints, used to be publicly exposed"
},
{
"name": "Authentication",
"description": "**[DEPRECATED]** Authentication-related endpoints"
}
],
@ -104,13 +94,6 @@
},
"/{shortCode}/qr-code": {
"$ref": "paths/{shortCode}_qr-code.json"
},
"/{shortCode}/preview": {
"$ref": "paths/{shortCode}_preview.json"
},
"/rest/v1/authenticate": {
"$ref": "paths/v1_authenticate.json"
}
}
}

View file

@ -12,14 +12,9 @@ return [
Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
Command\ShortUrl\GetVisitsCommand::NAME => Command\ShortUrl\GetVisitsCommand::class,
Command\ShortUrl\GeneratePreviewCommand::NAME => Command\ShortUrl\GeneratePreviewCommand::class,
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
Command\Visit\UpdateDbCommand::NAME => Command\Visit\UpdateDbCommand::class,
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
Command\Config\GenerateSecretCommand::NAME => Command\Config\GenerateSecretCommand::class,
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,

View file

@ -12,7 +12,6 @@ use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console as SymfonyCli;
use Symfony\Component\Lock\LockFactory;
@ -34,14 +33,9 @@ return [
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
Command\Visit\UpdateDbCommand::class => ConfigAbstractFactory::class,
Command\Config\GenerateCharsetCommand::class => InvokableFactory::class,
Command\Config\GenerateSecretCommand::class => InvokableFactory::class,
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
@ -64,7 +58,6 @@ return [
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'],
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class],
Command\ShortUrl\GeneratePreviewCommand::class => [Service\ShortUrlService::class, PreviewGenerator::class],
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
Command\Visit\LocateVisitsCommand::class => [
@ -73,7 +66,6 @@ return [
LockFactory::class,
GeolocationDbUpdater::class,
],
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class],
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],

View file

@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
use function str_shuffle;
/** @deprecated */
class GenerateCharsetCommand extends Command
{
public const NAME = 'config:generate-charset';
private const DEFAULT_CHARS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription(sprintf(
'[DEPRECATED] Generates a character set sample just by shuffling the default one, "%s". '
. 'Then it can be set in the SHORTCODE_CHARS environment variable',
self::DEFAULT_CHARS
))
->setHelp('<fg=red;options=bold>This command is deprecated. Better leave shlink generate the charset.</>');
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$charSet = str_shuffle(self::DEFAULT_CHARS);
(new SymfonyStyle($input, $output))->success(sprintf('Character set: "%s"', $charSet));
return ExitCodes::EXIT_SUCCESS;
}
}

View file

@ -1,39 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
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 Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
/** @deprecated */
class GenerateSecretCommand extends Command
{
use StringUtilsTrait;
public const NAME = 'config:generate-secret';
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('[DEPRECATED] Generates a random secret string that can be used for JWT token encryption')
->setHelp(
'<fg=red;options=bold>This command is deprecated. Better leave shlink generate the secret key.</>'
);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$secret = $this->generateRandomString(32);
(new SymfonyStyle($input, $output))->success(sprintf('Secret key: "%s"', $secret));
return ExitCodes::EXIT_SUCCESS;
}
}

View file

@ -19,7 +19,6 @@ use function sprintf;
class DeleteShortUrlCommand extends Command
{
public const NAME = 'short-url:delete';
private const ALIASES = ['short-code:delete'];
private DeleteShortUrlServiceInterface $deleteShortUrlService;
@ -33,7 +32,6 @@ class DeleteShortUrlCommand extends Command
{
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('Deletes a short URL')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code for the short URL to be deleted')
->addOption(

View file

@ -1,76 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\PreviewGenerator\Exception\PreviewGenerationException;
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGeneratorInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
/** @deprecated */
class GeneratePreviewCommand extends Command
{
public const NAME = 'short-url:process-previews';
private const ALIASES = ['shortcode:process-previews', 'short-code:process-previews'];
/** @var PreviewGeneratorInterface */
private $previewGenerator;
/** @var ShortUrlServiceInterface */
private $shortUrlService;
public function __construct(ShortUrlServiceInterface $shortUrlService, PreviewGeneratorInterface $previewGenerator)
{
parent::__construct();
$this->shortUrlService = $shortUrlService;
$this->previewGenerator = $previewGenerator;
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription(
'[DEPRECATED] Processes and generates the previews for every URL, improving performance for later web '
. 'requests.'
);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$page = 1;
do {
$shortUrls = $this->shortUrlService->listShortUrls($page);
$page += 1;
foreach ($shortUrls as $shortUrl) {
$this->processUrl($shortUrl->getLongUrl(), $output);
}
} while ($page <= $shortUrls->count());
(new SymfonyStyle($input, $output))->success('Finished processing all URLs');
return ExitCodes::EXIT_SUCCESS;
}
private function processUrl($url, OutputInterface $output): void
{
try {
$output->write(sprintf('Processing URL %s...', $url));
$this->previewGenerator->generatePreview($url);
$output->writeln(' <info>Success!</info>');
} catch (PreviewGenerationException $e) {
$output->writeln(' <error>Error</error>');
if ($output->isVerbose()) {
$this->getApplication()->renderThrowable($e, $output);
}
}
}
}

View file

@ -26,7 +26,6 @@ use function sprintf;
class GenerateShortUrlCommand extends Command
{
public const NAME = 'short-url:generate';
private const ALIASES = ['shortcode:generate', 'short-code:generate'];
private UrlShortenerInterface $urlShortener;
private array $domainConfig;
@ -42,7 +41,6 @@ class GenerateShortUrlCommand extends Command
{
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('Generates a short URL for provided long URL and returns it')
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse')
->addOption(

View file

@ -22,7 +22,6 @@ use function Functional\select_keys;
class GetVisitsCommand extends AbstractWithDateRangeCommand
{
public const NAME = 'short-url:visits';
private const ALIASES = ['shortcode:visits', 'short-code:visits'];
private VisitsTrackerInterface $visitsTracker;
@ -36,7 +35,6 @@ class GetVisitsCommand extends AbstractWithDateRangeCommand
{
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('Returns the detailed visits information for provided short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get');
}

View file

@ -32,7 +32,6 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
use PaginatorUtilsTrait;
public const NAME = 'short-url:list';
private const ALIASES = ['shortcode:list', 'short-code:list'];
private const COLUMNS_WHITELIST = [
'shortCode',
'shortUrl',
@ -56,7 +55,6 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
{
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('List all short URLs')
->addOption(
'page',

View file

@ -19,7 +19,6 @@ use function sprintf;
class ResolveUrlCommand extends Command
{
public const NAME = 'short-url:parse';
private const ALIASES = ['shortcode:parse', 'short-code:parse'];
private UrlShortenerInterface $urlShortener;
@ -33,7 +32,6 @@ class ResolveUrlCommand extends Command
{
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('Returns the long URL behind a short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse')
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain to which the short URL is attached.');

View file

@ -30,7 +30,6 @@ use function sprintf;
class LocateVisitsCommand extends AbstractLockedCommand
{
public const NAME = 'visit:locate';
public const ALIASES = ['visit:process'];
private VisitServiceInterface $visitService;
private IpLocationResolverInterface $ipLocationResolver;
@ -55,7 +54,6 @@ class LocateVisitsCommand extends AbstractLockedCommand
{
$this
->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('Resolves visits origin locations.');
}

View file

@ -1,91 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
/** @deprecated */
class UpdateDbCommand extends Command
{
public const NAME = 'visit:update-db';
/** @var DbUpdaterInterface */
private $geoLiteDbUpdater;
public function __construct(DbUpdaterInterface $geoLiteDbUpdater)
{
parent::__construct();
$this->geoLiteDbUpdater = $geoLiteDbUpdater;
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('[DEPRECATED] Updates the GeoLite2 database file used to geolocate IP addresses')
->setHelp(
'The GeoLite2 database is updated first Tuesday every month, so this command should be ideally run '
. 'every first Wednesday'
)
->addOption(
'ignoreErrors',
'i',
InputOption::VALUE_NONE,
'Makes the command success even iof the update fails.'
);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
$progressBar = new ProgressBar($output);
$progressBar->start();
try {
$this->geoLiteDbUpdater->downloadFreshCopy(function (int $total, int $downloaded) use ($progressBar) {
$progressBar->setMaxSteps($total);
$progressBar->setProgress($downloaded);
});
$progressBar->finish();
$io->newLine();
$io->success('GeoLite2 database properly updated');
return ExitCodes::EXIT_SUCCESS;
} catch (RuntimeException $e) {
$progressBar->finish();
$io->newLine();
return $this->handleError($e, $io, $input);
}
}
private function handleError(RuntimeException $e, SymfonyStyle $io, InputInterface $input): int
{
$ignoreErrors = $input->getOption('ignoreErrors');
$baseErrorMsg = 'An error occurred while updating GeoLite2 database';
if ($ignoreErrors) {
$io->warning(sprintf('%s, but it was ignored', $baseErrorMsg));
return ExitCodes::EXIT_SUCCESS;
}
$io->error($baseErrorMsg);
if ($io->isVerbose()) {
$this->getApplication()->renderThrowable($e, $io);
}
return ExitCodes::EXIT_FAILURE;
}
}

View file

@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Config;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use function implode;
use function sort;
use function str_split;
class GenerateCharsetCommandTest extends TestCase
{
private CommandTester $commandTester;
public function setUp(): void
{
$command = new GenerateCharsetCommand();
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/** @test */
public function charactersAreGeneratedFromDefault()
{
$prefix = 'Character set: ';
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
// Both default character set and the new one should have the same length
$this->assertStringContainsString($prefix, $output);
}
protected function orderStringLetters($string)
{
$letters = str_split($string);
sort($letters);
return implode('', $letters);
}
}

View file

@ -1,92 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\GeneratePreviewCommand;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\PreviewGenerator\Exception\PreviewGenerationException;
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
use function count;
use function substr_count;
class GeneratePreviewCommandTest extends TestCase
{
private CommandTester $commandTester;
private ObjectProphecy $previewGenerator;
private ObjectProphecy $shortUrlService;
public function setUp(): void
{
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
$command = new GeneratePreviewCommand($this->shortUrlService->reveal(), $this->previewGenerator->reveal());
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/** @test */
public function previewsForEveryUrlAreGenerated()
{
$paginator = $this->createPaginator([
new ShortUrl('http://foo.com'),
new ShortUrl('https://bar.com'),
new ShortUrl('http://baz.com/something'),
]);
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledOnce();
$generatePreview1 = $this->previewGenerator->generatePreview('http://foo.com')->willReturn('');
$generatePreview2 = $this->previewGenerator->generatePreview('https://bar.com')->willReturn('');
$generatePreview3 = $this->previewGenerator->generatePreview('http://baz.com/something')->willReturn('');
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Processing URL http://foo.com', $output);
$this->assertStringContainsString('Processing URL https://bar.com', $output);
$this->assertStringContainsString('Processing URL http://baz.com/something', $output);
$this->assertStringContainsString('Finished processing all URLs', $output);
$generatePreview1->shouldHaveBeenCalledOnce();
$generatePreview2->shouldHaveBeenCalledOnce();
$generatePreview3->shouldHaveBeenCalledOnce();
}
/** @test */
public function exceptionWillOutputError()
{
$items = [
new ShortUrl('http://foo.com'),
new ShortUrl('https://bar.com'),
new ShortUrl('http://baz.com/something'),
];
$paginator = $this->createPaginator($items);
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledOnce();
$this->previewGenerator->generatePreview(Argument::any())->willThrow(PreviewGenerationException::class)
->shouldBeCalledTimes(count($items));
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertEquals(count($items), substr_count($output, 'Error'));
}
protected function createPaginator(array $items)
{
$paginator = new Paginator(new ArrayAdapter($items));
$paginator->setItemCountPerPage(count($items));
return $paginator;
}
}

View file

@ -1,75 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\UpdateDbCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\IpGeolocation\Exception\RuntimeException;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
class UpdateDbCommandTest extends TestCase
{
private CommandTester $commandTester;
private ObjectProphecy $dbUpdater;
public function setUp(): void
{
$this->dbUpdater = $this->prophesize(DbUpdaterInterface::class);
$command = new UpdateDbCommand($this->dbUpdater->reveal());
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/** @test */
public function successMessageIsPrintedIfEverythingWorks(): void
{
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->will(function () {
});
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$exitCode = $this->commandTester->getStatusCode();
$this->assertStringContainsString('GeoLite2 database properly updated', $output);
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
$download->shouldHaveBeenCalledOnce();
}
/** @test */
public function errorMessageIsPrintedIfAnExceptionIsThrown(): void
{
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->willThrow(RuntimeException::class);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$exitCode = $this->commandTester->getStatusCode();
$this->assertStringContainsString('An error occurred while updating GeoLite2 database', $output);
$this->assertEquals(ExitCodes::EXIT_FAILURE, $exitCode);
$download->shouldHaveBeenCalledOnce();
}
/** @test */
public function warningMessageIsPrintedIfAnExceptionIsThrownAndErrorsAreIgnored(): void
{
$download = $this->dbUpdater->downloadFreshCopy(Argument::type('callable'))->willThrow(RuntimeException::class);
$this->commandTester->execute(['--ignoreErrors' => true]);
$output = $this->commandTester->getDisplay();
$exitCode = $this->commandTester->getStatusCode();
$this->assertStringContainsString('ignored', $output);
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $exitCode);
$download->shouldHaveBeenCalledOnce();
}
}

View file

@ -1,9 +0,0 @@
<?php
declare(strict_types=1);
return [
'app_options' => [],
];

View file

@ -8,7 +8,6 @@ use Doctrine\Common\Cache\Cache;
use Psr\EventDispatcher\EventDispatcherInterface;
use Shlinkio\Shlink\Core\ErrorHandler;
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Template\TemplateRendererInterface;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
@ -37,7 +36,6 @@ return [
Action\RedirectAction::class => ConfigAbstractFactory::class,
Action\PixelAction::class => ConfigAbstractFactory::class,
Action\QrCodeAction::class => ConfigAbstractFactory::class,
Action\PreviewAction::class => ConfigAbstractFactory::class,
Middleware\QrCodeCacheMiddleware::class => ConfigAbstractFactory::class,
],
@ -74,7 +72,6 @@ return [
'Logger_Shlink',
],
Action\QrCodeAction::class => [RouterInterface::class, Service\UrlShortener::class, 'Logger_Shlink'],
Action\PreviewAction::class => [PreviewGenerator::class, Service\UrlShortener::class, 'Logger_Shlink'],
Middleware\QrCodeCacheMiddleware::class => [Cache::class],
],

View file

@ -37,23 +37,6 @@ return [
],
'allowed_methods' => [RequestMethod::METHOD_GET],
],
// Deprecated routes
[
'name' => 'short-url-preview',
'path' => '/{shortCode}/preview',
'middleware' => Action\PreviewAction::class,
'allowed_methods' => [RequestMethod::METHOD_GET],
],
[
'name' => 'short-url-qr-code-old',
'path' => '/qr/{shortCode}[/{size:[0-9]+}]',
'middleware' => [
Middleware\QrCodeCacheMiddleware::class,
Action\QrCodeAction::class,
],
'allowed_methods' => [RequestMethod::METHOD_GET],
],
],
];

View file

@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Action;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Common\Response\ResponseUtilsTrait;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Shlinkio\Shlink\PreviewGenerator\Exception\PreviewGenerationException;
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGeneratorInterface;
/** @deprecated */
class PreviewAction implements MiddlewareInterface
{
use ResponseUtilsTrait;
/** @var PreviewGeneratorInterface */
private $previewGenerator;
/** @var UrlShortenerInterface */
private $urlShortener;
/** @var LoggerInterface */
private $logger;
public function __construct(
PreviewGeneratorInterface $previewGenerator,
UrlShortenerInterface $urlShortener,
?LoggerInterface $logger = null
) {
$this->previewGenerator = $previewGenerator;
$this->urlShortener = $urlShortener;
$this->logger = $logger ?: new NullLogger();
}
/**
* Process an incoming server request and return a response, optionally delegating
* to the next middleware component to create the response.
*
* @param Request $request
* @param RequestHandlerInterface $handler
*
* @return Response
*/
public function process(Request $request, RequestHandlerInterface $handler): Response
{
$shortCode = $request->getAttribute('shortCode');
try {
$url = $this->urlShortener->shortCodeToUrl($shortCode);
$imagePath = $this->previewGenerator->generatePreview($url->getLongUrl());
return $this->generateImageResponse($imagePath);
} catch (ShortUrlNotFoundException | PreviewGenerationException $e) {
$this->logger->warning('An error occurred while generating preview image. {e}', ['e' => $e]);
return $handler->handle($request);
}
}
}

View file

@ -22,7 +22,6 @@ class SimplifiedConfigParser
'short_domain_schema' => ['url_shortener', 'domain', 'schema'],
'short_domain_host' => ['url_shortener', 'domain', 'hostname'],
'validate_url' => ['url_shortener', 'validate_url'],
'not_found_redirect_to' => ['not_found_redirects', 'invalid_short_url'], // Deprecated
'invalid_short_url_redirect_to' => ['not_found_redirects', 'invalid_short_url'],
'regular_404_redirect_to' => ['not_found_redirects', 'regular_404'],
'base_url_redirect_to' => ['not_found_redirects', 'base_path'],

View file

@ -90,9 +90,6 @@ class Visit extends AbstractEntity implements JsonSerializable
'date' => $this->date->toAtomString(),
'userAgent' => $this->userAgent,
'visitLocation' => $this->visitLocation,
// Deprecated
'remoteAddr' => null,
];
}

View file

@ -14,8 +14,8 @@ class VisitLocation extends AbstractEntity implements VisitLocationInterface
private string $countryName;
private string $regionName;
private string $cityName;
private string $latitude; // FIXME Should be float
private string $longitude; // FIXME Should be float
private float $latitude;
private float $longitude;
private string $timezone;
public function __construct(Location $location)
@ -25,22 +25,22 @@ class VisitLocation extends AbstractEntity implements VisitLocationInterface
public function getCountryName(): string
{
return $this->countryName ?? '';
return $this->countryName;
}
public function getLatitude(): string
public function getLatitude(): float
{
return $this->latitude ?? '';
return $this->latitude;
}
public function getLongitude(): string
public function getLongitude(): float
{
return $this->longitude ?? '';
return $this->longitude;
}
public function getCityName(): string
{
return $this->cityName ?? '';
return $this->cityName;
}
private function exchangeLocationInfo(Location $info): void
@ -49,8 +49,8 @@ class VisitLocation extends AbstractEntity implements VisitLocationInterface
$this->countryName = $info->countryName();
$this->regionName = $info->regionName();
$this->cityName = $info->city();
$this->latitude = (string) $info->latitude();
$this->longitude = (string) $info->longitude();
$this->latitude = $info->latitude();
$this->longitude = $info->longitude();
$this->timezone = $info->timeZone();
}
@ -74,8 +74,8 @@ class VisitLocation extends AbstractEntity implements VisitLocationInterface
$this->countryName === '' &&
$this->regionName === '' &&
$this->cityName === '' &&
((float) $this->latitude) === 0.0 &&
((float) $this->longitude) === 0.0 &&
$this->latitude === 0.0 &&
$this->longitude === 0.0 &&
$this->timezone === '';
}
}

View file

@ -78,7 +78,7 @@ class NotifyVisitToWebHooks
'User-Agent' => (string) $this->appOptions,
],
RequestOptions::JSON => [
'shortUrl' => $this->transformer->transform($visit->getShortUrl(), false),
'shortUrl' => $this->transformer->transform($visit->getShortUrl()),
'visit' => $visit->jsonSerialize(),
],
];

View file

@ -15,8 +15,6 @@ class AppOptions extends AbstractOptions
private string $name = '';
private string $version = '1.0';
/** @deprecated */
private string $secretKey = '';
private ?string $disableTrackParam = null;
public function getName(): string
@ -41,23 +39,6 @@ class AppOptions extends AbstractOptions
return $this;
}
/**
* @deprecated
*/
public function getSecretKey(): string
{
return $this->secretKey;
}
/**
* @deprecated
*/
protected function setSecretKey(string $secretKey): self
{
$this->secretKey = $secretKey;
return $this;
}
/**
* @return string|null
*/

View file

@ -22,11 +22,11 @@ class ShortUrlDataTransformer implements DataTransformerInterface
/**
* @param ShortUrl $shortUrl
*/
public function transform($shortUrl, bool $includeDeprecated = true): array
public function transform($shortUrl): array
{
$longUrl = $shortUrl->getLongUrl();
$rawData = [
return [
'shortCode' => $shortUrl->getShortCode(),
'shortUrl' => $shortUrl->toString($this->domainConfig),
'longUrl' => $longUrl,
@ -35,12 +35,6 @@ class ShortUrlDataTransformer implements DataTransformerInterface
'tags' => invoke($shortUrl->getTags(), '__toString'),
'meta' => $this->buildMeta($shortUrl),
];
if ($includeDeprecated) {
$rawData['originalUrl'] = $longUrl;
}
return $rawData;
}
private function buildMeta(ShortUrl $shortUrl): array

View file

@ -11,14 +11,14 @@ final class UnknownVisitLocation implements VisitLocationInterface
return 'Unknown';
}
public function getLatitude(): string
public function getLatitude(): float
{
return '0.0';
return 0.0;
}
public function getLongitude(): string
public function getLongitude(): float
{
return '0.0';
return 0.0;
}
public function getCityName(): string
@ -33,8 +33,8 @@ final class UnknownVisitLocation implements VisitLocationInterface
'countryName' => 'Unknown',
'regionName' => 'Unknown',
'cityName' => 'Unknown',
'latitude' => '0.0',
'longitude' => '0.0',
'latitude' => 0.0,
'longitude' => 0.0,
'timezone' => 'Unknown',
];
}

View file

@ -10,9 +10,9 @@ interface VisitLocationInterface extends JsonSerializable
{
public function getCountryName(): string;
public function getLatitude(): string;
public function getLatitude(): float;
public function getLongitude(): string;
public function getLongitude(): float;
public function getCityName(): string;
}

View file

@ -1,76 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Action;
use finfo;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Core\Action\PreviewAction;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
use function filesize;
use const FILEINFO_MIME;
/** @deprecated */
class PreviewActionTest extends TestCase
{
/** @var PreviewAction */
private $action;
/** @var ObjectProphecy */
private $previewGenerator;
/** @var ObjectProphecy */
private $urlShortener;
public function setUp(): void
{
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
$this->urlShortener = $this->prophesize(UrlShortener::class);
$this->action = new PreviewAction($this->previewGenerator->reveal(), $this->urlShortener->reveal());
}
/** @test */
public function correctShortCodeReturnsImageResponse(): void
{
$shortCode = 'abc123';
$url = 'foobar.com';
$shortUrl = new ShortUrl($url);
$path = __FILE__;
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($shortUrl)->shouldBeCalledOnce();
$this->previewGenerator->generatePreview($url)->willReturn($path)->shouldBeCalledOnce();
$resp = $this->action->process(
(new ServerRequest())->withAttribute('shortCode', $shortCode),
$this->prophesize(RequestHandlerInterface::class)->reveal()
);
$this->assertEquals(filesize($path), $resp->getHeaderLine('Content-length'));
$this->assertEquals((new finfo(FILEINFO_MIME))->file($path), $resp->getHeaderLine('Content-type'));
}
/** @test */
public function invalidShortCodeExceptionFallsBackToNextMiddleware(): void
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(ShortUrlNotFoundException::class)
->shouldBeCalledOnce();
$delegate = $this->prophesize(RequestHandlerInterface::class);
$process = $delegate->handle(Argument::any())->willReturn(new Response());
$this->action->process(
(new ServerRequest())->withAttribute('shortCode', $shortCode),
$delegate->reveal()
);
$process->shouldHaveBeenCalledOnce();
}
}

View file

@ -11,7 +11,7 @@ use function array_merge;
class SimplifiedConfigParserTest extends TestCase
{
private $postProcessor;
private SimplifiedConfigParser $postProcessor;
public function setUp(): void
{
@ -38,9 +38,9 @@ class SimplifiedConfigParserTest extends TestCase
'disable_track_param' => 'bar',
'short_domain_schema' => 'https',
'short_domain_host' => 'doma.in',
'validate_url' => false,
'validate_url' => true,
'delete_short_url_threshold' => 50,
'not_found_redirect_to' => 'foobar.com',
'invalid_short_url_redirect_to' => 'foobar.com',
'redis_servers' => [
'tcp://1.1.1.1:1111',
'tcp://1.2.2.2:2222',
@ -79,7 +79,7 @@ class SimplifiedConfigParserTest extends TestCase
'schema' => 'https',
'hostname' => 'doma.in',
],
'validate_url' => false,
'validate_url' => true,
'visits_webhooks' => [
'http://my-api.com/api/v2.3/notify',
'https://third-party.io/foo',
@ -125,28 +125,4 @@ class SimplifiedConfigParserTest extends TestCase
$this->assertEquals(array_merge($expected, $simplified), $result);
}
/**
* @test
* @dataProvider provideConfigWithDeprecates
*/
public function properlyMapsDeprecatedConfigs(array $config, string $expected): void
{
$result = ($this->postProcessor)($config);
$this->assertEquals($expected, $result['not_found_redirects']['invalid_short_url']);
}
public function provideConfigWithDeprecates(): iterable
{
yield 'only deprecated config' => [['not_found_redirect_to' => 'old_value'], 'old_value'];
yield 'only new config' => [['invalid_short_url_redirect_to' => 'new_value'], 'new_value'];
yield 'both configs, new first' => [
['invalid_short_url_redirect_to' => 'new_value', 'not_found_redirect_to' => 'old_value'],
'new_value',
];
yield 'both configs, deprecated first' => [
['not_found_redirect_to' => 'old_value', 'invalid_short_url_redirect_to' => 'new_value'],
'new_value',
];
}
}

View file

@ -10,16 +10,6 @@ use Shlinkio\Shlink\IpGeolocation\Model\Location;
class VisitLocationTest extends TestCase
{
/** @test */
public function valuesFoundWhenExchangingArrayAreCastToString(): void
{
$payload = new Location('', '', '', '', 1000.7, -2000.4, '');
$location = new VisitLocation($payload);
$this->assertSame('1000.7', $location->getLatitude());
$this->assertSame('-2000.4', $location->getLongitude());
}
/**
* @test
* @dataProvider provideArgs

View file

@ -25,9 +25,6 @@ class VisitTest extends TestCase
'date' => ($date ?? $visit->getDate())->toAtomString(),
'userAgent' => 'Chrome',
'visitLocation' => null,
// Deprecated
'remoteAddr' => null,
], $visit->jsonSerialize());
}

View file

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\PreviewGenerator;
use Symfony\Component\Filesystem\Filesystem;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
return [
'dependencies' => [
'factories' => [
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
Service\PreviewGenerator::class => ConfigAbstractFactory::class,
],
],
ConfigAbstractFactory::class => [
Service\PreviewGenerator::class => [
Image\ImageBuilder::class,
Filesystem::class,
'config.preview_generation.files_location',
],
],
];

View file

@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\PreviewGenerator;
use function Shlinkio\Shlink\Common\loadConfigFromGlob;
/** @deprecated */
class ConfigProvider
{
public function __invoke(): array
{
return loadConfigFromGlob(__DIR__ . '/../config/{,*.}config.php');
}
}

View file

@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\PreviewGenerator\Exception;
use RuntimeException;
use function sprintf;
/** @deprecated */
class PreviewGenerationException extends RuntimeException
{
public static function fromImageError(string $error): self
{
return new self(sprintf('Error generating a preview image with error: %s', $error));
}
}

View file

@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\PreviewGenerator\Image;
use mikehaertl\wkhtmlto\Image;
use Zend\ServiceManager\AbstractPluginManager;
/** @deprecated */
class ImageBuilder extends AbstractPluginManager implements ImageBuilderInterface
{
protected $instanceOf = Image::class;
}

View file

@ -1,19 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\PreviewGenerator\Image;
use mikehaertl\wkhtmlto\Image;
use Psr\Container\ContainerInterface;
/** @deprecated */
class ImageBuilderFactory
{
public function __invoke(ContainerInterface $container)
{
return new ImageBuilder($container, ['factories' => [
Image::class => ImageFactory::class,
]]);
}
}

View file

@ -1,12 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\PreviewGenerator\Image;
use Zend\ServiceManager\ServiceLocatorInterface;
/** @deprecated */
interface ImageBuilderInterface extends ServiceLocatorInterface
{
}

View file

@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\PreviewGenerator\Image;
use mikehaertl\wkhtmlto\Image;
use Psr\Container\ContainerInterface;
/** @deprecated */
class ImageFactory
{
public function __invoke(ContainerInterface $container, string $requestedName, ?array $options = null)
{
$config = $container->get('config')['wkhtmltopdf'];
$image = new Image($config['images'] ?? null);
if ($options['url'] ?? null) {
$image->setPage($options['url']);
}
return $image;
}
}

View file

@ -1,58 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\PreviewGenerator\Service;
use mikehaertl\wkhtmlto\Image;
use Shlinkio\Shlink\PreviewGenerator\Exception\PreviewGenerationException;
use Shlinkio\Shlink\PreviewGenerator\Image\ImageBuilderInterface;
use Symfony\Component\Filesystem\Filesystem;
use function sprintf;
use function urlencode;
/** @deprecated */
class PreviewGenerator implements PreviewGeneratorInterface
{
/** @var string */
private $location;
/** @var ImageBuilderInterface */
private $imageBuilder;
/** @var Filesystem */
private $filesystem;
public function __construct(ImageBuilderInterface $imageBuilder, Filesystem $filesystem, string $location)
{
$this->location = $location;
$this->imageBuilder = $imageBuilder;
$this->filesystem = $filesystem;
}
/**
* Generates and stores preview for provided website and returns the path to the image file
*
* @throws PreviewGenerationException
*/
public function generatePreview(string $url): string
{
$image = $this->imageBuilder->build(Image::class, ['url' => $url]);
// If the file already exists, return its path
$cacheId = sprintf('preview_%s.%s', urlencode($url), $image->type);
$path = $this->location . '/' . $cacheId;
if ($this->filesystem->exists($path)) {
return $path;
}
// Save and check if an error occurred
$image->saveAs($path);
$error = $image->getError();
if (! empty($error)) {
throw PreviewGenerationException::fromImageError($error);
}
// Cache the path and return it
return $path;
}
}

View file

@ -1,18 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\PreviewGenerator\Service;
use Shlinkio\Shlink\PreviewGenerator\Exception\PreviewGenerationException;
/** @deprecated */
interface PreviewGeneratorInterface
{
/**
* Generates and stores preview for provided website and returns the path to the image file
*
* @throws PreviewGenerationException
*/
public function generatePreview(string $url): string;
}

View file

@ -1,27 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\PreviewGenerator;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\PreviewGenerator\ConfigProvider;
class ConfigProviderTest extends TestCase
{
/** @var ConfigProvider */
private $configProvider;
public function setUp(): void
{
$this->configProvider = new ConfigProvider();
}
/** @test */
public function configIsReturned(): void
{
$config = ($this->configProvider)();
$this->assertArrayHasKey('dependencies', $config);
}
}

View file

@ -1,28 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\PreviewGenerator\Image;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\PreviewGenerator\Image\ImageBuilder;
use Shlinkio\Shlink\PreviewGenerator\Image\ImageBuilderFactory;
use Zend\ServiceManager\ServiceManager;
class ImageBuilderFactoryTest extends TestCase
{
/** @var ImageBuilderFactory */
private $factory;
public function setUp(): void
{
$this->factory = new ImageBuilderFactory();
}
/** @test */
public function serviceIsCreated()
{
$instance = $this->factory->__invoke(new ServiceManager(), '');
$this->assertInstanceOf(ImageBuilder::class, $instance);
}
}

View file

@ -1,54 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\PreviewGenerator\Image;
use mikehaertl\wkhtmlto\Image;
use PHPUnit\Framework\TestCase;
use ReflectionObject;
use Shlinkio\Shlink\PreviewGenerator\Image\ImageFactory;
use Zend\ServiceManager\ServiceManager;
class ImageFactoryTest extends TestCase
{
/** @var ImageFactory */
private $factory;
public function setUp(): void
{
$this->factory = new ImageFactory();
}
/** @test */
public function noPageIsSetWhenOptionsAreNotProvided()
{
/** @var Image $image */
$image = $this->factory->__invoke(new ServiceManager(['services' => [
'config' => ['wkhtmltopdf' => []],
]]), '');
$this->assertInstanceOf(Image::class, $image);
$ref = new ReflectionObject($image);
$page = $ref->getProperty('_page');
$page->setAccessible(true);
$this->assertNull($page->getValue($image));
}
/** @test */
public function aPageIsSetWhenOptionsIncludeTheUrl()
{
$expectedPage = 'foo/bar.html';
/** @var Image $image */
$image = $this->factory->__invoke(new ServiceManager(['services' => [
'config' => ['wkhtmltopdf' => []],
]]), '', ['url' => $expectedPage]);
$this->assertInstanceOf(Image::class, $image);
$ref = new ReflectionObject($image);
$page = $ref->getProperty('_page');
$page->setAccessible(true);
$this->assertEquals($expectedPage, $page->getValue($image));
}
}

View file

@ -1,85 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\PreviewGenerator\Service;
use mikehaertl\wkhtmlto\Image;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\PreviewGenerator\Exception\PreviewGenerationException;
use Shlinkio\Shlink\PreviewGenerator\Image\ImageBuilder;
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
use Symfony\Component\Filesystem\Filesystem;
use Zend\ServiceManager\ServiceManager;
use function sprintf;
use function urlencode;
class PreviewGeneratorTest extends TestCase
{
/** @var PreviewGenerator */
private $generator;
/** @var ObjectProphecy */
private $image;
/** @var ObjectProphecy */
private $filesystem;
public function setUp(): void
{
$this->image = $this->prophesize(Image::class);
$this->filesystem = $this->prophesize(Filesystem::class);
$this->generator = new PreviewGenerator(new ImageBuilder(new ServiceManager(), [
'factories' => [
Image::class => function () {
return $this->image->reveal();
},
],
]), $this->filesystem->reveal(), 'dir');
}
/** @test */
public function alreadyProcessedElementsAreNotProcessed()
{
$url = 'http://foo.com';
$this->filesystem->exists(sprintf('dir/preview_%s.png', urlencode($url)))->willReturn(true)
->shouldBeCalledOnce();
$this->image->saveAs(Argument::cetera())->shouldBeCalledTimes(0);
$this->assertEquals(sprintf('dir/preview_%s.png', urlencode($url)), $this->generator->generatePreview($url));
}
/** @test */
public function nonProcessedElementsAreProcessed()
{
$url = 'http://foo.com';
$cacheId = sprintf('preview_%s.png', urlencode($url));
$expectedPath = 'dir/' . $cacheId;
$this->filesystem->exists(sprintf('dir/preview_%s.png', urlencode($url)))->willReturn(false)
->shouldBeCalledOnce();
$this->image->saveAs($expectedPath)->shouldBeCalledOnce();
$this->image->getError()->willReturn('')->shouldBeCalledOnce();
$this->assertEquals($expectedPath, $this->generator->generatePreview($url));
}
/** @test */
public function errorWhileGeneratingPreviewThrowsException()
{
$url = 'http://foo.com';
$cacheId = sprintf('preview_%s.png', urlencode($url));
$expectedPath = 'dir/' . $cacheId;
$this->filesystem->exists(sprintf('dir/preview_%s.png', urlencode($url)))->willReturn(false)
->shouldBeCalledOnce();
$this->image->saveAs($expectedPath)->shouldBeCalledOnce();
$this->image->getError()->willReturn('Error!!')->shouldBeCalledOnce();
$this->expectException(PreviewGenerationException::class);
$this->generator->generatePreview($url);
}
}

View file

@ -10,7 +10,6 @@ return [
'auth' => [
'routes_whitelist' => [
Action\AuthenticateAction::class,
Action\HealthAction::class,
Action\ShortUrl\SingleStepCreateShortUrlAction::class,
],
@ -18,13 +17,10 @@ return [
'plugins' => [
'factories' => [
Authentication\Plugin\ApiKeyHeaderPlugin::class => ConfigAbstractFactory::class,
Authentication\Plugin\AuthorizationHeaderPlugin::class => ConfigAbstractFactory::class,
],
'aliases' => [
Authentication\Plugin\ApiKeyHeaderPlugin::HEADER_NAME =>
Authentication\Plugin\ApiKeyHeaderPlugin::class,
Authentication\Plugin\AuthorizationHeaderPlugin::HEADER_NAME =>
Authentication\Plugin\AuthorizationHeaderPlugin::class,
],
],
],
@ -40,7 +36,6 @@ return [
],
ConfigAbstractFactory::class => [
Authentication\Plugin\AuthorizationHeaderPlugin::class => [Authentication\JWTService::class],
Authentication\Plugin\ApiKeyHeaderPlugin::class => [Service\ApiKeyService::class],
Authentication\RequestToHttpAuthPlugin::class => [Authentication\AuthenticationPluginManager::class],

View file

@ -17,10 +17,8 @@ return [
'dependencies' => [
'factories' => [
Authentication\JWTService::class => ConfigAbstractFactory::class,
ApiKeyService::class => ConfigAbstractFactory::class,
Action\AuthenticateAction::class => ConfigAbstractFactory::class,
Action\HealthAction::class => ConfigAbstractFactory::class,
Action\ShortUrl\CreateShortUrlAction::class => ConfigAbstractFactory::class,
Action\ShortUrl\SingleStepCreateShortUrlAction::class => ConfigAbstractFactory::class,
@ -38,18 +36,13 @@ return [
ImplicitOptionsMiddleware::class => Middleware\EmptyResponseImplicitOptionsMiddlewareFactory::class,
Middleware\BodyParserMiddleware::class => InvokableFactory::class,
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\PathVersionMiddleware::class => InvokableFactory::class,
Middleware\BackwardsCompatibleProblemDetailsMiddleware::class => ConfigAbstractFactory::class,
Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class => InvokableFactory::class,
Middleware\ShortUrl\ShortCodePathMiddleware::class => InvokableFactory::class,
],
],
ConfigAbstractFactory::class => [
Authentication\JWTService::class => [AppOptions::class],
ApiKeyService::class => ['em'],
Action\AuthenticateAction::class => [ApiKeyService::class, Authentication\JWTService::class, 'Logger_Shlink'],
Action\HealthAction::class => [Connection::class, AppOptions::class, 'Logger_Shlink'],
Action\ShortUrl\CreateShortUrlAction::class => [
Service\UrlShortener::class,
@ -76,10 +69,6 @@ return [
Action\Tag\DeleteTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Action\Tag\CreateTagsAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Action\Tag\UpdateTagAction::class => [Service\Tag\TagService::class, LoggerInterface::class],
Middleware\BackwardsCompatibleProblemDetailsMiddleware::class => [
'config.backwards_compatible_problem_details.json_flags',
],
],
];

View file

@ -7,7 +7,6 @@ namespace Shlinkio\Shlink\Rest;
return [
'routes' => [
Action\AuthenticateAction::getRouteDef(),
Action\HealthAction::getRouteDef(),
// Short codes

View file

@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Action;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Zend\Diactoros\Response\JsonResponse;
/** @deprecated */
class AuthenticateAction extends AbstractRestAction
{
protected const ROUTE_PATH = '/authenticate';
protected const ROUTE_ALLOWED_METHODS = [self::METHOD_POST];
/** @var ApiKeyService|ApiKeyServiceInterface */
private $apiKeyService;
/** @var JWTServiceInterface */
private $jwtService;
public function __construct(
ApiKeyServiceInterface $apiKeyService,
JWTServiceInterface $jwtService,
?LoggerInterface $logger = null
) {
parent::__construct($logger);
$this->apiKeyService = $apiKeyService;
$this->jwtService = $jwtService;
}
/**
* @param Request $request
* @return Response
* @throws \InvalidArgumentException
*/
public function handle(Request $request): Response
{
$authData = $request->getParsedBody();
if (! isset($authData['apiKey'])) {
return new JsonResponse([
'error' => 'INVALID_ARGUMENT',
'message' => 'You have to provide a valid API key under the "apiKey" param name.',
], self::STATUS_BAD_REQUEST);
}
// Authenticate using provided API key
$apiKey = $this->apiKeyService->getByKey($authData['apiKey']);
if ($apiKey === null || ! $apiKey->isValid()) {
return new JsonResponse([
'error' => 'INVALID_API_KEY',
'message' => 'Provided API key does not exist or is invalid.',
], self::STATUS_UNAUTHORIZED);
}
// Generate a JSON Web Token that will be used for authorization in next requests
$token = $this->jwtService->create($apiKey);
return new JsonResponse(['token' => $token]);
}
}

View file

@ -1,111 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Authentication;
use Firebase\JWT\JWT;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
use UnexpectedValueException;
use function time;
/** @deprecated */
class JWTService implements JWTServiceInterface
{
/** @var AppOptions */
private $appOptions;
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): string
{
$currentTimestamp = time();
return $this->encode([
'iss' => (string) $this->appOptions,
'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(string $jwt, $lifetime = self::DEFAULT_LIFETIME): string
{
$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(string $jwt): bool
{
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(string $jwt): array
{
try {
return $this->decode($jwt);
} catch (UnexpectedValueException $e) {
throw AuthenticationException::expiredJWT($e);
}
}
/**
* @param array $data
* @return string
*/
private function encode(array $data): string
{
return JWT::encode($data, $this->appOptions->getSecretKey(), self::DEFAULT_ENCRYPTION_ALG);
}
/**
* @param string $jwt
* @return array
*/
private function decode(string $jwt): array
{
return (array) JWT::decode($jwt, $this->appOptions->getSecretKey(), [self::DEFAULT_ENCRYPTION_ALG]);
}
}

View file

@ -1,50 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Authentication;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
interface JWTServiceInterface
{
public const DEFAULT_LIFETIME = 604800; // 1 week
public 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): string;
/**
* 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(string $jwt, $lifetime = self::DEFAULT_LIFETIME): string;
/**
* Verifies that certain JWT is valid
*
* @param string $jwt
* @return bool
*/
public function verify(string $jwt): bool;
/**
* Decodes certain token and returns the payload
*
* @param string $jwt
* @return array
* @throws AuthenticationException If the token has expired
*/
public function getPayload(string $jwt): array;
}

View file

@ -1,71 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Authentication\Plugin;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
use Throwable;
use function count;
use function explode;
use function sprintf;
use function strtolower;
/** @deprecated */
class AuthorizationHeaderPlugin implements AuthenticationPluginInterface
{
public const HEADER_NAME = 'Authorization';
/** @var JWTServiceInterface */
private $jwtService;
public function __construct(JWTServiceInterface $jwtService)
{
$this->jwtService = $jwtService;
}
/**
* @throws VerifyAuthenticationException
*/
public function verify(ServerRequestInterface $request): void
{
// Get token making sure the an authorization type is provided
$authToken = $request->getHeaderLine(self::HEADER_NAME);
$authTokenParts = explode(' ', $authToken);
if (count($authTokenParts) === 1) {
throw VerifyAuthenticationException::forMissingAuthType();
}
// Make sure the authorization type is Bearer
[$authType, $jwt] = $authTokenParts;
if (strtolower($authType) !== 'bearer') {
throw VerifyAuthenticationException::forInvalidAuthType($authType);
}
try {
if (! $this->jwtService->verify($jwt)) {
throw $this->createInvalidTokenError();
}
} catch (Throwable $e) {
throw $this->createInvalidTokenError();
}
}
private function createInvalidTokenError(): VerifyAuthenticationException
{
return VerifyAuthenticationException::forInvalidAuthToken();
}
public function update(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
$authToken = $request->getHeaderLine(self::HEADER_NAME);
[, $jwt] = explode(' ', $authToken);
$jwt = $this->jwtService->refresh($jwt);
return $response->withHeader(self::HEADER_NAME, sprintf('Bearer %s', $jwt));
}
}

View file

@ -17,7 +17,6 @@ class RequestToHttpAuthPlugin implements RequestToHttpAuthPluginInterface
// When more than one is matched, the first one to be found will take precedence.
public const SUPPORTED_AUTH_HEADERS = [
Plugin\ApiKeyHeaderPlugin::HEADER_NAME,
Plugin\AuthorizationHeaderPlugin::HEADER_NAME,
];
private AuthenticationPluginManagerInterface $authPluginManager;

View file

@ -12,6 +12,7 @@ use function sprintf;
class ConfigProvider
{
private const ROUTES_PREFIX = '/rest/v{version:1|2}';
private const UNVERSIONED_ROUTES_PREFIX = '/rest';
private Closure $loadConfig;
@ -33,7 +34,11 @@ class ConfigProvider
// Prepend the routes prefix to every path
foreach ($routes as $key => $route) {
['path' => $path] = $route;
$routes[$key]['path'] = sprintf('%s%s', self::ROUTES_PREFIX, $path);
$routes[$key]['path'] = sprintf(
'%s%s',
$path === '/health' ? self::UNVERSIONED_ROUTES_PREFIX : self::ROUTES_PREFIX,
$path,
);
}
return $config;

View file

@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Exception;
use Throwable;
/** @deprecated */
class AuthenticationException extends RuntimeException
{
public static function expiredJWT(?Throwable $prev = null): self
{
return new self('The token has expired.', -1, $prev);
}
}

View file

@ -8,8 +8,6 @@ use Fig\Http\Message\StatusCodeInterface;
use Zend\ProblemDetails\Exception\CommonProblemDetailsExceptionTrait;
use Zend\ProblemDetails\Exception\ProblemDetailsExceptionInterface;
use function sprintf;
class VerifyAuthenticationException extends RuntimeException implements ProblemDetailsExceptionInterface
{
use CommonProblemDetailsExceptionTrait;
@ -25,46 +23,4 @@ class VerifyAuthenticationException extends RuntimeException implements ProblemD
return $e;
}
/** @deprecated */
public static function forInvalidAuthToken(): self
{
$e = new self(
'Missing or invalid auth token provided. Perform a new authentication request and send provided '
. 'token on every new request on the Authorization header'
);
$e->detail = $e->getMessage();
$e->title = 'Invalid auth token';
$e->type = 'INVALID_AUTH_TOKEN';
$e->status = StatusCodeInterface::STATUS_UNAUTHORIZED;
return $e;
}
/** @deprecated */
public static function forMissingAuthType(): self
{
$e = new self('You need to provide the Bearer type in the Authorization header.');
$e->detail = $e->getMessage();
$e->title = 'Invalid authorization';
$e->type = 'INVALID_AUTHORIZATION';
$e->status = StatusCodeInterface::STATUS_UNAUTHORIZED;
return $e;
}
/** @deprecated */
public static function forInvalidAuthType(string $providedType): self
{
$e = new self(sprintf('Provided authorization type %s is not supported. Use Bearer instead.', $providedType));
$e->detail = $e->getMessage();
$e->title = 'Invalid authorization';
$e->type = 'INVALID_AUTHORIZATION';
$e->status = StatusCodeInterface::STATUS_UNAUTHORIZED;
return $e;
}
}

View file

@ -1,63 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use Zend\Diactoros\Response\JsonResponse;
use function Functional\reduce_left;
use function Shlinkio\Shlink\Common\json_decode;
use function strpos;
/** @deprecated */
class BackwardsCompatibleProblemDetailsMiddleware implements MiddlewareInterface
{
private const BACKWARDS_COMPATIBLE_FIELDS = [
'error' => 'type',
'message' => 'detail',
];
private int $jsonFlags;
public function __construct(int $jsonFlags)
{
$this->jsonFlags = $jsonFlags;
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$resp = $handler->handle($request);
if ($resp->getHeaderLine('Content-type') !== 'application/problem+json' || ! $this->isVersionOne($request)) {
return $resp;
}
try {
$body = (string) $resp->getBody();
$payload = $this->makePayloadBackwardsCompatible(json_decode($body));
} catch (Throwable $e) {
return $resp;
}
return new JsonResponse($payload, $resp->getStatusCode(), $resp->getHeaders(), $this->jsonFlags);
}
private function isVersionOne(ServerRequestInterface $request): bool
{
$path = $request->getUri()->getPath();
return strpos($path, '/v') === false || strpos($path, '/v1') === 0;
}
private function makePayloadBackwardsCompatible(array $payload): array
{
return reduce_left(self::BACKWARDS_COMPATIBLE_FIELDS, function (string $newKey, string $oldKey, $c, $acc) {
$acc[$oldKey] = $acc[$newKey];
return $acc;
}, $payload);
}
}

View file

@ -27,7 +27,6 @@ class CrossDomainMiddleware implements MiddlewareInterface, RequestMethodInterfa
$response = $response->withHeader('Access-Control-Allow-Origin', $request->getHeader('Origin'))
->withHeader('Access-Control-Expose-Headers', implode(', ', [
Authentication\Plugin\ApiKeyHeaderPlugin::HEADER_NAME,
Authentication\Plugin\AuthorizationHeaderPlugin::HEADER_NAME,
]));
if ($request->getMethod() !== self::METHOD_OPTIONS) {
return $response;

View file

@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function strpos;
class PathVersionMiddleware implements MiddlewareInterface
{
// TODO The /health endpoint needs this middleware in order to work without the version.
// Take it into account if this middleware is ever removed.
public function process(Request $request, RequestHandlerInterface $handler): Response
{
$uri = $request->getUri();
$path = $uri->getPath();
// If the path does not begin with the version number, prepend v1 by default for BC purposes
if (strpos($path, '/v') !== 0) {
$request = $request->withUri($uri->withPath('/v1' . $uri->getPath()));
}
return $handler->handle($request);
}
}

View file

@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\Middleware\ShortUrl;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function str_replace;
/** @deprecated */
class ShortCodePathMiddleware implements MiddlewareInterface
{
private const OLD_PATH_PREFIX = '/short-codes'; // Old path is deprecated. Remove this middleware on v2
private const NEW_PATH_PREFIX = '/short-urls';
/**
* Process an incoming server request and return a response, optionally delegating
* response creation to a handler.
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$uri = $request->getUri();
$path = $uri->getPath();
// If the path starts with the old prefix, replace it by the new one
return $handler->handle(
$request->withUri($uri->withPath(str_replace(self::OLD_PATH_PREFIX, self::NEW_PATH_PREFIX, $path)))
);
}
}

View file

@ -49,9 +49,7 @@ class CreateShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals($detail, $payload['detail']);
$this->assertEquals($detail, $payload['message']); // Deprecated
$this->assertEquals('INVALID_SLUG', $payload['type']);
$this->assertEquals('INVALID_SLUG', $payload['error']); // Deprecated
$this->assertEquals('Invalid custom slug', $payload['title']);
$this->assertEquals($slug, $payload['customSlug']);
@ -215,7 +213,7 @@ class CreateShortUrlActionTest extends ApiTestCase
}
/** @test */
public function failsToCreateShortUrlWithInvalidOriginalUrl(): void
public function failsToCreateShortUrlWithInvalidLongUrl(): void
{
$url = 'https://this-has-to-be-invalid.com';
$expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url);
@ -225,9 +223,7 @@ class CreateShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_URL', $payload['type']);
$this->assertEquals('INVALID_URL', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid URL', $payload['title']);
$this->assertEquals($url, $payload['url']);
}

View file

@ -19,9 +19,7 @@ class DeleteShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']);
}
@ -41,9 +39,7 @@ class DeleteShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNPROCESSABLE_ENTITY, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE_DELETION', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE_DELETION', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Cannot delete short URL', $payload['title']);
}
}

View file

@ -20,9 +20,7 @@ class EditShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']);
}
@ -40,9 +38,7 @@ class EditShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_ARGUMENT', $payload['type']);
$this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid data', $payload['title']);
}
}

View file

@ -20,9 +20,7 @@ class EditShortUrlTagsActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_ARGUMENT', $payload['type']);
$this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid data', $payload['title']);
}
@ -39,9 +37,7 @@ class EditShortUrlTagsActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']);
}

View file

@ -19,9 +19,7 @@ class GetVisitsActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']);
}

View file

@ -24,7 +24,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null,
'maxVisits' => null,
],
'originalUrl' => 'https://shlink.io',
];
private const SHORT_URL_CUSTOM_SLUG_AND_DOMAIN = [
'shortCode' => 'custom-with-domain',
@ -38,7 +37,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null,
'maxVisits' => null,
],
'originalUrl' => 'https://google.com',
];
private const SHORT_URL_META = [
'shortCode' => 'def456',
@ -54,9 +52,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null,
'maxVisits' => null,
],
'originalUrl' =>
'https://blog.alejandrocelaya.com/2017/12/09'
. '/acmailer-7-0-the-most-important-release-in-a-long-time/',
];
private const SHORT_URL_CUSTOM_SLUG = [
'shortCode' => 'custom',
@ -70,7 +65,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null,
'maxVisits' => 2,
],
'originalUrl' => 'https://shlink.io',
];
private const SHORT_URL_CUSTOM_DOMAIN = [
'shortCode' => 'ghi789',
@ -86,9 +80,6 @@ class ListShortUrlsTest extends ApiTestCase
'validUntil' => null,
'maxVisits' => null,
],
'originalUrl' =>
'https://blog.alejandrocelaya.com/2019/04/27'
. '/considerations-to-properly-use-open-source-software-projects/',
];
/**

View file

@ -19,9 +19,7 @@ class ResolveShortUrlActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('INVALID_SHORTCODE', $payload['type']);
$this->assertEquals('INVALID_SHORTCODE', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Short URL not found', $payload['title']);
$this->assertEquals('invalid', $payload['shortCode']);
}

View file

@ -23,9 +23,7 @@ class UpdateTagActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_BAD_REQUEST, $resp->getStatusCode());
$this->assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
$this->assertEquals('INVALID_ARGUMENT', $payload['type']);
$this->assertEquals('INVALID_ARGUMENT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid data', $payload['title']);
}
@ -50,9 +48,7 @@ class UpdateTagActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_NOT_FOUND, $resp->getStatusCode());
$this->assertEquals(self::STATUS_NOT_FOUND, $payload['status']);
$this->assertEquals('TAG_NOT_FOUND', $payload['type']);
$this->assertEquals('TAG_NOT_FOUND', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Tag not found', $payload['title']);
}
@ -70,9 +66,7 @@ class UpdateTagActionTest extends ApiTestCase
$this->assertEquals(self::STATUS_CONFLICT, $resp->getStatusCode());
$this->assertEquals(self::STATUS_CONFLICT, $payload['status']);
$this->assertEquals('TAG_CONFLICT', $payload['type']);
$this->assertEquals('TAG_CONFLICT', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Tag conflict', $payload['title']);
}

View file

@ -21,15 +21,13 @@ class AuthenticationTest extends ApiTestCase
implode('", "', RequestToHttpAuthPlugin::SUPPORTED_AUTH_HEADERS)
);
$resp = $this->callApi(self::METHOD_GET, '/short-codes');
$resp = $this->callApi(self::METHOD_GET, '/short-urls');
$payload = $this->getJsonResponsePayload($resp);
$this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']);
$this->assertEquals('INVALID_AUTHORIZATION', $payload['type']);
$this->assertEquals('INVALID_AUTHORIZATION', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid authorization', $payload['title']);
}
@ -41,7 +39,7 @@ class AuthenticationTest extends ApiTestCase
{
$expectedDetail = 'Provided API key does not exist or is invalid.';
$resp = $this->callApi(self::METHOD_GET, '/short-codes', [
$resp = $this->callApi(self::METHOD_GET, '/short-urls', [
'headers' => [
Plugin\ApiKeyHeaderPlugin::HEADER_NAME => $apiKey,
],
@ -51,9 +49,7 @@ class AuthenticationTest extends ApiTestCase
$this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']);
$this->assertEquals('INVALID_API_KEY', $payload['type']);
$this->assertEquals('INVALID_API_KEY', $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals('Invalid API key', $payload['title']);
}
@ -63,53 +59,4 @@ class AuthenticationTest extends ApiTestCase
yield 'key which is expired' => ['expired_api_key'];
yield 'key which is disabled' => ['disabled_api_key'];
}
/**
* @test
* @dataProvider provideInvalidAuthorizations
*/
public function authorizationErrorIsReturnedIfInvalidDataIsProvided(
string $authValue,
string $expectedDetail,
string $expectedType,
string $expectedTitle
): void {
$resp = $this->callApi(self::METHOD_GET, '/short-codes', [
'headers' => [
Plugin\AuthorizationHeaderPlugin::HEADER_NAME => $authValue,
],
]);
$payload = $this->getJsonResponsePayload($resp);
$this->assertEquals(self::STATUS_UNAUTHORIZED, $resp->getStatusCode());
$this->assertEquals(self::STATUS_UNAUTHORIZED, $payload['status']);
$this->assertEquals($expectedType, $payload['type']);
$this->assertEquals($expectedType, $payload['error']); // Deprecated
$this->assertEquals($expectedDetail, $payload['detail']);
$this->assertEquals($expectedDetail, $payload['message']); // Deprecated
$this->assertEquals($expectedTitle, $payload['title']);
}
public function provideInvalidAuthorizations(): iterable
{
yield 'no type' => [
'invalid',
'You need to provide the Bearer type in the Authorization header.',
'INVALID_AUTHORIZATION',
'Invalid authorization',
];
yield 'invalid type' => [
'Basic invalid',
'Provided authorization type Basic is not supported. Use Bearer instead.',
'INVALID_AUTHORIZATION',
'Invalid authorization',
];
yield 'invalid JWT' => [
'Bearer invalid',
'Missing or invalid auth token provided. Perform a new authentication request and send provided '
. 'token on every new request on the Authorization header',
'INVALID_AUTH_TOKEN',
'Invalid auth token',
];
}
}

View file

@ -1,72 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Action;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Rest\Action\AuthenticateAction;
use Shlinkio\Shlink\Rest\Authentication\JWTService;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Zend\Diactoros\ServerRequest;
use function strpos;
/** @deprecated */
class AuthenticateActionTest extends TestCase
{
/** @var AuthenticateAction */
private $action;
/** @var ObjectProphecy */
private $apiKeyService;
/** @var ObjectProphecy */
private $jwtService;
public function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$this->jwtService = $this->prophesize(JWTService::class);
$this->jwtService->create(Argument::cetera())->willReturn('');
$this->action = new AuthenticateAction($this->apiKeyService->reveal(), $this->jwtService->reveal());
}
/** @test */
public function notProvidingAuthDataReturnsError()
{
$resp = $this->action->handle(new ServerRequest());
$this->assertEquals(400, $resp->getStatusCode());
}
/** @test */
public function properApiKeyReturnsTokenInResponse()
{
$this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->setId('5'))
->shouldBeCalledOnce();
$request = (new ServerRequest())->withParsedBody([
'apiKey' => 'foo',
]);
$response = $this->action->handle($request);
$this->assertEquals(200, $response->getStatusCode());
$response->getBody()->rewind();
$this->assertTrue(strpos($response->getBody()->getContents(), '"token"') > 0);
}
/** @test */
public function invalidApiKeyReturnsErrorResponse()
{
$this->apiKeyService->getByKey('foo')->willReturn((new ApiKey())->disable())
->shouldBeCalledOnce();
$request = (new ServerRequest())->withParsedBody([
'apiKey' => 'foo',
]);
$response = $this->action->handle($request);
$this->assertEquals(401, $response->getStatusCode());
}
}

View file

@ -1,86 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Authentication;
use Firebase\JWT\JWT;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Shlinkio\Shlink\Rest\Authentication\JWTService;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Exception\AuthenticationException;
use function time;
/** @deprecated */
class JWTServiceTest extends TestCase
{
/** @var JWTService */
private $service;
public function setUp(): void
{
$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 */
public function getPayloadThrowsExceptionWithIncorrectTokens()
{
$this->expectException(AuthenticationException::class);
$this->service->getPayload('invalidToken');
}
}

View file

@ -1,119 +0,0 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest\Authentication\Plugin;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Rest\Authentication\JWTServiceInterface;
use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthorizationHeaderPlugin;
use Shlinkio\Shlink\Rest\Exception\VerifyAuthenticationException;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
use function sprintf;
/** @deprecated */
class AuthorizationHeaderPluginTest extends TestCase
{
/** @var AuthorizationHeaderPlugin */
private $plugin;
/** @var ObjectProphecy */
private $jwtService;
public function setUp(): void
{
$this->jwtService = $this->prophesize(JWTServiceInterface::class);
$this->plugin = new AuthorizationHeaderPlugin($this->jwtService->reveal());
}
/** @test */
public function verifyAnAuthorizationWithoutBearerTypeThrowsException()
{
$authToken = 'ABC-abc';
$request = (new ServerRequest())->withHeader(
AuthorizationHeaderPlugin::HEADER_NAME,
$authToken
);
$this->expectException(VerifyAuthenticationException::class);
$this->expectExceptionMessage(sprintf(
'You need to provide the Bearer type in the %s header.',
AuthorizationHeaderPlugin::HEADER_NAME
));
$this->plugin->verify($request);
}
/** @test */
public function verifyAnAuthorizationWithWrongTypeThrowsException()
{
$authToken = 'Basic ABC-abc';
$request = (new ServerRequest())->withHeader(
AuthorizationHeaderPlugin::HEADER_NAME,
$authToken
);
$this->expectException(VerifyAuthenticationException::class);
$this->expectExceptionMessage(
'Provided authorization type Basic is not supported. Use Bearer instead.'
);
$this->plugin->verify($request);
}
/** @test */
public function verifyAnExpiredTokenThrowsException()
{
$authToken = 'Bearer ABC-abc';
$request = (new ServerRequest())->withHeader(
AuthorizationHeaderPlugin::HEADER_NAME,
$authToken
);
$jwtVerify = $this->jwtService->verify('ABC-abc')->willReturn(false);
$this->expectException(VerifyAuthenticationException::class);
$this->expectExceptionMessage(sprintf(
'Missing or invalid auth token provided. Perform a new authentication request and send provided '
. 'token on every new request on the %s header',
AuthorizationHeaderPlugin::HEADER_NAME
));
$this->plugin->verify($request);
$jwtVerify->shouldHaveBeenCalledOnce();
}
/** @test */
public function verifyValidTokenDoesNotThrowException()
{
$authToken = 'Bearer ABC-abc';
$request = (new ServerRequest())->withHeader(
AuthorizationHeaderPlugin::HEADER_NAME,
$authToken
);
$jwtVerify = $this->jwtService->verify('ABC-abc')->willReturn(true);
$this->plugin->verify($request);
$jwtVerify->shouldHaveBeenCalledOnce();
}
/** @test */
public function updateReturnsAnUpdatedResponseWithNewJwt()
{
$authToken = 'Bearer ABC-abc';
$request = (new ServerRequest())->withHeader(
AuthorizationHeaderPlugin::HEADER_NAME,
$authToken
);
$jwtRefresh = $this->jwtService->refresh('ABC-abc')->willReturn('DEF-def');
$response = $this->plugin->update($request, new Response());
$this->assertTrue($response->hasHeader(AuthorizationHeaderPlugin::HEADER_NAME));
$this->assertEquals('Bearer DEF-def', $response->getHeaderLine(AuthorizationHeaderPlugin::HEADER_NAME));
$jwtRefresh->shouldHaveBeenCalledOnce();
}
}

View file

@ -9,7 +9,6 @@ use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Rest\Authentication\AuthenticationPluginManagerInterface;
use Shlinkio\Shlink\Rest\Authentication\Plugin\ApiKeyHeaderPlugin;
use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthenticationPluginInterface;
use Shlinkio\Shlink\Rest\Authentication\Plugin\AuthorizationHeaderPlugin;
use Shlinkio\Shlink\Rest\Authentication\RequestToHttpAuthPlugin;
use Shlinkio\Shlink\Rest\Exception\MissingAuthenticationException;
use Zend\Diactoros\ServerRequest;
@ -63,14 +62,7 @@ class RequestToAuthPluginTest extends TestCase
public function provideHeaders(): iterable
{
yield 'API key header only' => [[
ApiKeyHeaderPlugin::HEADER_NAME => 'foobar',
], ApiKeyHeaderPlugin::HEADER_NAME];
yield 'Authorization header only' => [[
AuthorizationHeaderPlugin::HEADER_NAME => 'foobar',
], AuthorizationHeaderPlugin::HEADER_NAME];
yield 'Both headers' => [[
AuthorizationHeaderPlugin::HEADER_NAME => 'foobar',
yield 'API key header' => [[
ApiKeyHeaderPlugin::HEADER_NAME => 'foobar',
], ApiKeyHeaderPlugin::HEADER_NAME];
}

View file

@ -33,6 +33,7 @@ class ConfigProviderTest extends TestCase
['path' => '/foo'],
['path' => '/bar'],
['path' => '/baz/foo'],
['path' => '/health'],
],
]);
@ -42,6 +43,7 @@ class ConfigProviderTest extends TestCase
['path' => '/rest/v{version:1|2}/foo'],
['path' => '/rest/v{version:1|2}/bar'],
['path' => '/rest/v{version:1|2}/baz/foo'],
['path' => '/rest/health'],
], $config['routes']);
}
}

Some files were not shown because too many files have changed in this diff Show more