Merge pull request #604 from shlinkio/develop

Release v2
This commit is contained in:
Alejandro Celaya 2020-01-08 19:46:53 +01:00 committed by GitHub
commit 3d2932782d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
310 changed files with 1344 additions and 4155 deletions

2
.gitattributes vendored
View File

@ -5,8 +5,6 @@
/module/CLI/test-resources export-ignore /module/CLI/test-resources export-ignore
/module/Core/test export-ignore /module/Core/test export-ignore
/module/Core/test-db 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 export-ignore
/module/Rest/test-api export-ignore /module/Rest/test-api export-ignore
.gitattributes export-ignore .gitattributes export-ignore

View File

@ -2,7 +2,7 @@
namespace PHPSTORM_META; namespace PHPSTORM_META;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Zend\ServiceManager\ServiceLocatorInterface; use Laminas\ServiceManager\ServiceLocatorInterface;
/** /**
* PhpStorm Container Interop code completion * PhpStorm Container Interop code completion

View File

@ -5,14 +5,8 @@ branches:
- /.*/ - /.*/
php: php:
- '7.2'
- '7.3'
- '7.4' - '7.4'
matrix:
allow_failures:
- php: '7.4'
services: services:
- mysql - mysql
- postgresql - postgresql
@ -39,7 +33,7 @@ before_script:
script: script:
- composer ci - composer ci
- if [[ ! -z "$DOCKERFILE_CHANGED" && "${TRAVIS_PHP_VERSION}" == "7.2" ]]; then docker build -t shlink-docker-image:temp . ; fi - if [[ ! -z "$DOCKERFILE_CHANGED" && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then docker build -t shlink-docker-image:temp . ; fi
after_success: after_success:
- rm -f build/clover.xml - rm -f build/clover.xml
@ -61,4 +55,4 @@ deploy:
skip_cleanup: true skip_cleanup: true
on: on:
tags: true tags: true
php: '7.2' php: '7.4'

View File

@ -4,6 +4,39 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org). The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
## 2.0.0 - 2020-01-08
#### Added
* [#429](https://github.com/shlinkio/shlink/issues/429) Added support for PHP 7.4
* [#529](https://github.com/shlinkio/shlink/issues/529) Created an UPGRADING.md file explaining how to upgrade from v1.x to v2.x
* [#594](https://github.com/shlinkio/shlink/issues/594) Updated external shlink packages, including installer v4.0, which adds the option to ask for the redis cluster config.
#### Changed
* [#592](https://github.com/shlinkio/shlink/issues/592) Updated coding styles to use [shlinkio/php-coding-standard](https://github.com/shlinkio/php-coding-standard) v2.1.0.
* [#530](https://github.com/shlinkio/shlink/issues/530) Migrated project from deprecated `zendframework` components to the new `laminas` and `mezzio` ones.
#### Deprecated
* *Nothing*
#### Removed
* [#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 [UPGRADE](UPGRADE.md) doc in order to get details on how to migrate to this version.
#### Fixed
* [#600](https://github.com/shlinkio/shlink/issues/600) Fixed health action so that it works with and without version in the path.
## 1.21.1 - 2020-01-02 ## 1.21.1 - 2020-01-02
#### Added #### Added

View File

@ -1,7 +1,7 @@
FROM php:7.3.11-alpine3.10 FROM php:7.4.1-alpine3.10
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>" LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
ARG SHLINK_VERSION=1.20.2 ARG SHLINK_VERSION=2.0.0
ENV SHLINK_VERSION ${SHLINK_VERSION} ENV SHLINK_VERSION ${SHLINK_VERSION}
ENV SWOOLE_VERSION 4.4.12 ENV SWOOLE_VERSION 4.4.12
ENV COMPOSER_VERSION 1.9.1 ENV COMPOSER_VERSION 1.9.1

View File

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016-2019 Alejandro Celaya Copyright (c) 2016-2020 Alejandro Celaya
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -9,6 +9,8 @@
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own custom domain. A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own custom domain.
> This document references Shlink 2.x. If you are using an older version and want to upgrade, follow the [UPGRADE](UPGRADE.md) doc.
## Table of Contents ## Table of Contents
- [Installation](#installation) - [Installation](#installation)
@ -29,7 +31,7 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u
First, make sure the host where you are going to run shlink fulfills these requirements: First, make sure the host where you are going to run shlink fulfills these requirements:
* PHP 7.2 or greater with JSON, APCu, intl, curl, PDO and gd extensions enabled. * PHP 7.4 or greater with JSON, APCu, intl, curl, PDO and gd extensions enabled.
* MySQL, MariaDB, PostgreSQL or SQLite. * MySQL, MariaDB, PostgreSQL or SQLite.
* The web server of your choice with PHP integration (Apache or Nginx recommended). * The web server of your choice with PHP integration (Apache or Nginx recommended).
@ -121,7 +123,7 @@ Once Shlink is configured, you need to expose it to the web, either by using a t
First you need to install the swoole PHP extension with [pecl](https://pecl.php.net/package/swoole), `pecl install swoole`. First you need to install the swoole PHP extension with [pecl](https://pecl.php.net/package/swoole), `pecl install swoole`.
Once installed, it's actually pretty easy to get shlink up and running with swoole. Run `./vendor/bin/zend-expressive-swoole start -d` and you will get shlink running on port 8080. Once installed, it's actually pretty easy to get shlink up and running with swoole. Run `./vendor/bin/mezzio-swoole start -d` and you will get shlink running on port 8080.
However, by doing it this way, you are loosing all the access logs, and the service won't be automatically run if the server has to be restarted. However, by doing it this way, you are loosing all the access logs, and the service won't be automatically run if the server has to be restarted.
@ -138,7 +140,7 @@ Once Shlink is configured, you need to expose it to the web, either by using a t
# Description: Shlink non-blocking server with swoole # Description: Shlink non-blocking server with swoole
### END INIT INFO ### END INIT INFO
SCRIPT=/path/to/shlink/vendor/bin/zend-expressive-swoole\ start SCRIPT=/path/to/shlink/vendor/bin/mezzio-swoole\ start
RUNAS=root RUNAS=root
PIDFILE=/var/run/shlink_swoole.pid PIDFILE=/var/run/shlink_swoole.pid
@ -197,31 +199,11 @@ Finally access to [https://app.shlink.io](https://app.shlink.io) and configure y
### Bonus ### Bonus
Depending on the shlink version you installed and how you serve it, there are a couple of time-consuming tasks that shlink expects you to do manually, or at least it is recommended, since it will improve runtime performance. Geo-locating visits to your short links is a time-consuming task. When serving Shlink with swoole, the geo-location task is automatically run asynchronously just after a visit to a short URL happens.
Those tasks can be performed using shlink's CLI tool, so it should be easy to schedule them to be run in the background (for example, using cron jobs): However, if you are not serving Shlink with swoole, you will have to schedule the geo-location task to be run regularly in the background (for example, using cron jobs):
* **For shlink older than 1.18.0 or not using swoole to serve it**: Resolve IP address locations: `/path/to/shlink/bin/cli visit:locate` The command you need to run is `/path/to/shlink/bin/cli visit:locate`, and you can optionally provide the `-q` flag to remove any output and avoid your cron logs to be polluted.
If you don't run this command regularly, the stats will say all visits come from *unknown* locations.
> If you serve Shlink with swoole and use v1.18.0 at least, visit location is automatically scheduled by Shlink just after the visit occurs, using swoole's task system.
* **For shlink older than v1.17.0**: Update IP geolocation database: `/path/to/shlink/bin/cli visit:update-db`
When shlink is installed it downloads a fresh [GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) db file. Running this command will update this file.
The file is updated the first Tuesday of every month, so it should be enough running this command the first Wednesday.
> 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 ## Update to new version
@ -274,33 +256,28 @@ Options:
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Available commands: Available commands:
help Displays help for a command help Displays help for a command
list Lists commands list Lists commands
api-key api-key
api-key:disable Disables an API key. api-key:disable Disables an API key.
api-key:generate Generates a new valid API key. api-key:generate Generates a new valid API key.
api-key:list Lists all the available API keys. 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
db db
db:create Creates the database needed for shlink to work. It will do nothing if the database already exists 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:migrate Runs database migrations, which will ensure the shlink database is up to date.
short-url short-url
short-url:delete [short-code:delete] Deletes a short URL short-url: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: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:list List all short URLs
short-url:parse [shortcode:parse|short-code:parse] Returns the long URL behind a short code short-url: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 Returns the detailed visits information for provided short code
short-url:visits [shortcode:visits|short-code:visits] Returns the detailed visits information for provided short code
tag tag
tag:create Creates one or more tags. tag:create Creates one or more tags.
tag:delete Deletes one or more tags. tag:delete Deletes one or more tags.
tag:list Lists existing tags. tag:list Lists existing tags.
tag:rename Renames one existing tag. tag:rename Renames one existing tag.
visit visit
visit:locate [visit:process] Resolves visits origin locations. visit:locate Resolves visits origin locations.
visit:update-db [DEPRECATED] Updates the GeoLite2 database file used to geolocate IP addresses
``` ```
> This product includes GeoLite2 data created by MaxMind, available from [https://www.maxmind.com](https://www.maxmind.com) > This product includes GeoLite2 data created by MaxMind, available from [https://www.maxmind.com](https://www.maxmind.com)

70
UPGRADE.md Normal file
View File

@ -0,0 +1,70 @@
# Upgrading
## From v1.x to v2.x
### Preview generation
The ability to generate website previews has been completely removed and has no replacement.
The feature never properly worked, and it wasn't really useful. Because of that, the feature is no longer available on Shlink 2.x
Removing this feature has these implications:
* The `short-url:process-previews` CLI command no longer exists, and an error will be thrown if executed.
* The `/{shortCode}/preview` path is no longer valid, and will return a 404 status.
### Removed paths
These routes have been removed, but have a direct replacement:
* `/qr/{shortCode}[/{size}]` -> `/{shortCode}/qr-code[/{size}]`
* `PUT /rest/v{version}/short-urls/{shortCode}` -> `PATCH /rest/v{version}/short-urls/{shortCode}`
When using the old ones, a 404 status will me returned now.
### Removed command and route aliases
All the aliases for the CLI commands in the `short-urls` namespace have been removed. If you were using any of those commands with the `shortcode` or `short-code` prefixes, make sure to update them to use the `short-urls` prefix instead.
The same happens for all REST endpoints starting with `/short-code`. They were previously aliased to `/short-urls` ones, but they will return a 404 now. Make sure to update them accordingly.
### JWT authentication removed
Shlink's REST API no longer accepts authentication using a JWT token. The API key has to be passed now in the `x-api-key` header.
Removing this feature has these implications:
* Shlink will no longer introspect the `Authorization` header for Bearer tokens.
* The `POST /rest/v{version}/authenticate` endpoint no longer exists and will return a 404.
### API version is now required
Endpoints need to provide a version in the path now. Previously, not providing a version used to fall back to v1. Now, it will return a 404 status, as no route will match.
The only exception is the `/rest/health` endpoint, which will continue working without the version.
### Changes in models
The next REST API models have changed:
* **ShortUrl**: The `originalUrl` property was deprecated and has been removed. Use `longUrl` instead.
* **Visit**: The `remoteAddr` property was deprecated and has been removed. It has no replacement.
* **VisitLocation**: The `latitude` and `longitude` properties are no longer strings, but float.
### URL validation
Shlink can verify provided long URLs are valid before trying to shorten them. Starting with v2, it no longer does it by default and needs to be explicitly enabled instead of explicitly disabled.
### Removed config options
The `not_found_redirect_to` config option and the `NOT_FOUND_REDIRECT_TO` env var are no longer taken into consideration for the docker image.
Instead, use `invalid_short_url_redirect_to` and `INVALID_SHORT_URL_REDIRECT_TO` respectively.
### Migrated to Laminas
The project has been using Zend Framework components since the beginning. Since it has been re-branded as [Laminas](https://getlaminas.org/), this version updates to the new set of components.
Updating to Laminas components has these implications:
* If you were manually serving Shlink with swoole, the entry script has to be changed from `/path/to/shlink/vendor/bin/zend-expressive-swoole` to `/path/to/shlink/vendor/bin/mezzio-swoole`

View File

@ -8,5 +8,5 @@ use function chdir;
use function dirname; use function dirname;
chdir(dirname(__DIR__)); chdir(dirname(__DIR__));
$run = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php'; [$install] = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
$run(false); $install();

View File

@ -3,16 +3,16 @@ export APP_ENV=test
export DB_DRIVER=mysql export DB_DRIVER=mysql
# Try to stop server just in case it hanged in last execution # Try to stop server just in case it hanged in last execution
vendor/bin/zend-expressive-swoole stop vendor/bin/mezzio-swoole stop
echo 'Starting server...' echo 'Starting server...'
vendor/bin/zend-expressive-swoole start -d vendor/bin/mezzio-swoole start -d
sleep 2 sleep 2
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always $* vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always $*
testsExitCode=$? testsExitCode=$?
vendor/bin/zend-expressive-swoole stop vendor/bin/mezzio-swoole stop
# Exit this script with the same code as the tests. If tests failed, this script has to fail # Exit this script with the same code as the tests. If tests failed, this script has to fail
exit $testsExitCode exit $testsExitCode

View File

@ -8,5 +8,5 @@ use function chdir;
use function dirname; use function dirname;
chdir(dirname(__DIR__)); chdir(dirname(__DIR__));
$run = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php'; [, $update] = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
$run(true); $update();

Binary file not shown.

View File

@ -12,7 +12,7 @@
} }
], ],
"require": { "require": {
"php": "^7.2", "php": "^7.4",
"ext-json": "*", "ext-json": "*",
"ext-pdo": "*", "ext-pdo": "*",
"akrabat/ip-address-middleware": "^1.0", "akrabat/ip-address-middleware": "^1.0",
@ -26,53 +26,52 @@
"firebase/php-jwt": "^4.0", "firebase/php-jwt": "^4.0",
"geoip2/geoip2": "^2.9", "geoip2/geoip2": "^2.9",
"guzzlehttp/guzzle": "^6.5.1", "guzzlehttp/guzzle": "^6.5.1",
"laminas/laminas-config": "^3.3",
"laminas/laminas-config-aggregator": "^1.1",
"laminas/laminas-dependency-plugin": "^1.0",
"laminas/laminas-diactoros": "^2.1.3",
"laminas/laminas-inputfilter": "^2.10",
"laminas/laminas-paginator": "^2.8",
"laminas/laminas-servicemanager": "^3.4",
"laminas/laminas-stdlib": "^3.2",
"lstrojny/functional-php": "^1.9", "lstrojny/functional-php": "^1.9",
"mikehaertl/phpwkhtmltopdf": "^2.2", "mezzio/mezzio": "^3.2",
"mezzio/mezzio-fastroute": "^3.0",
"mezzio/mezzio-helpers": "^5.3",
"mezzio/mezzio-platesrenderer": "^2.1",
"mezzio/mezzio-problem-details": "^1.1",
"mezzio/mezzio-swoole": "^2.4",
"monolog/monolog": "^2.0", "monolog/monolog": "^2.0",
"nikolaposa/monolog-factory": "^3.0", "nikolaposa/monolog-factory": "^3.0",
"ocramius/proxy-manager": "~2.2.2", "ocramius/proxy-manager": "^2.6.0",
"phly/phly-event-dispatcher": "^1.0", "phly/phly-event-dispatcher": "^1.0",
"predis/predis": "^1.1", "predis/predis": "^1.1",
"pugx/shortid-php": "^0.5", "pugx/shortid-php": "^0.5",
"shlinkio/shlink-common": "^2.4", "shlinkio/shlink-common": "^2.5",
"shlinkio/shlink-event-dispatcher": "^1.1", "shlinkio/shlink-event-dispatcher": "^1.3",
"shlinkio/shlink-installer": "^3.3", "shlinkio/shlink-installer": "^4.0",
"shlinkio/shlink-ip-geolocation": "^1.2", "shlinkio/shlink-ip-geolocation": "^1.3",
"symfony/console": "^5.0", "symfony/console": "^5.0",
"symfony/filesystem": "^5.0", "symfony/filesystem": "^5.0",
"symfony/lock": "^5.0", "symfony/lock": "^5.0",
"symfony/process": "^5.0", "symfony/process": "^5.0"
"zendframework/zend-config": "^3.3",
"zendframework/zend-config-aggregator": "^1.1",
"zendframework/zend-diactoros": "^2.1.3",
"zendframework/zend-expressive": "^3.2",
"zendframework/zend-expressive-fastroute": "^3.0",
"zendframework/zend-expressive-helpers": "^5.3",
"zendframework/zend-expressive-platesrenderer": "^2.1",
"zendframework/zend-expressive-swoole": "^2.4",
"zendframework/zend-inputfilter": "^2.10",
"zendframework/zend-paginator": "^2.8",
"zendframework/zend-problem-details": "^1.0",
"zendframework/zend-servicemanager": "^3.4",
"zendframework/zend-stdlib": "^3.2"
}, },
"require-dev": { "require-dev": {
"devster/ubench": "^2.0", "devster/ubench": "^2.0",
"eaglewu/swoole-ide-helper": "dev-master", "eaglewu/swoole-ide-helper": "dev-master",
"infection/infection": "^0.15.0", "infection/infection": "^0.15.0",
"phpstan/phpstan-shim": "^0.11.16", "phpstan/phpstan": "^0.12.3",
"phpunit/phpunit": "^8.3", "phpunit/phpunit": "^8.3",
"roave/security-advisories": "dev-master", "roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.0.0", "shlinkio/php-coding-standard": "~2.1.0",
"shlinkio/shlink-test-utils": "^1.2", "shlinkio/shlink-test-utils": "^1.3",
"symfony/var-dumper": "^5.0" "symfony/var-dumper": "^5.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src", "Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src", "Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
"Shlinkio\\Shlink\\Core\\": "module/Core/src", "Shlinkio\\Shlink\\Core\\": "module/Core/src"
"Shlinkio\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/src/"
}, },
"files": [ "files": [
"module/Core/functions/functions.php" "module/Core/functions/functions.php"
@ -86,8 +85,7 @@
"ShlinkioTest\\Shlink\\Core\\": [ "ShlinkioTest\\Shlink\\Core\\": [
"module/Core/test", "module/Core/test",
"module/Core/test-db" "module/Core/test-db"
], ]
"ShlinkioTest\\Shlink\\PreviewGenerator\\": "module/PreviewGenerator/test"
} }
}, },
"scripts": { "scripts": {

View File

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

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use Zend\ConfigAggregator\ConfigAggregator; use Laminas\ConfigAggregator\ConfigAggregator;
return [ return [

View File

@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use Zend\ConfigAggregator\ConfigAggregator; use Laminas\ConfigAggregator\ConfigAggregator;
return [ return [

View File

@ -2,14 +2,13 @@
declare(strict_types=1); declare(strict_types=1);
use Zend\Expressive; use Mezzio\Container;
use Zend\Expressive\Container;
return [ return [
'dependencies' => [ 'dependencies' => [
'delegators' => [ 'delegators' => [
Expressive\Application::class => [ Mezzio\Application::class => [
Container\ApplicationConfigInjectionDelegator::class, Container\ApplicationConfigInjectionDelegator::class,
], ],
], ],

View File

@ -1,4 +1,5 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
@ -12,7 +13,7 @@ return [
], ],
'initializers' => [ 'initializers' => [
function (ContainerInterface $container, $instance) { function (ContainerInterface $container, $instance): void {
if ($instance instanceof Log\LoggerAwareInterface) { if ($instance instanceof Log\LoggerAwareInterface) {
$instance->setLogger($container->get(Log\LoggerInterface::class)); $instance->setLogger($container->get(Log\LoggerInterface::class));
} }

View File

@ -2,18 +2,17 @@
declare(strict_types=1); declare(strict_types=1);
use Laminas\Stratigility\Middleware\ErrorHandler;
use Mezzio\ProblemDetails\ProblemDetailsMiddleware;
use Shlinkio\Shlink\Common\Logger; use Shlinkio\Shlink\Common\Logger;
use Zend\ProblemDetails\ProblemDetailsMiddleware;
use Zend\Stratigility\Middleware\ErrorHandler;
return [ return [
'backwards_compatible_problem_details' => [ 'problem-details' => [
'default_type_fallbacks' => [ 'default_types_map' => [
404 => 'NOT_FOUND', 404 => 'NOT_FOUND',
500 => 'INTERNAL_SERVER_ERROR', 500 => 'INTERNAL_SERVER_ERROR',
], ],
'json_flags' => JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION,
], ],
'error_handler' => [ 'error_handler' => [

View File

@ -2,51 +2,43 @@
declare(strict_types=1); declare(strict_types=1);
use Shlinkio\Shlink\Installer\Config\Plugin; use Shlinkio\Shlink\Installer\Config\Option;
return [ return [
'installer_plugins_expected_config' => [ 'installer' => [
Plugin\UrlShortenerConfigCustomizer::class => [ 'enabled_options' => [
Plugin\UrlShortenerConfigCustomizer::SCHEMA, Option\DatabaseDriverConfigOption::class,
Plugin\UrlShortenerConfigCustomizer::HOSTNAME, Option\DatabaseNameConfigOption::class,
Plugin\UrlShortenerConfigCustomizer::VALIDATE_URL, Option\DatabaseHostConfigOption::class,
Plugin\UrlShortenerConfigCustomizer::NOTIFY_VISITS_WEBHOOKS, Option\DatabasePortConfigOption::class,
Plugin\UrlShortenerConfigCustomizer::VISITS_WEBHOOKS, Option\DatabaseUserConfigOption::class,
Option\DatabasePasswordConfigOption::class,
Option\DatabaseSqlitePathConfigOption::class,
Option\DatabaseMySqlOptionsConfigOption::class,
Option\ShortDomainHostConfigOption::class,
Option\ShortDomainSchemaConfigOption::class,
Option\ValidateUrlConfigOption::class,
Option\VisitsWebhooksConfigOption::class,
Option\BaseUrlRedirectConfigOption::class,
Option\InvalidShortUrlRedirectConfigOption::class,
Option\Regular404RedirectConfigOption::class,
Option\DisableTrackParamConfigOption::class,
Option\CheckVisitsThresholdConfigOption::class,
Option\VisitsThresholdConfigOption::class,
Option\BasePathConfigOption::class,
Option\TaskWorkerNumConfigOption::class,
Option\WebWorkerNumConfigOption::class,
Option\RedisServersConfigOption::class,
], ],
Plugin\ApplicationConfigCustomizer::class => [ 'installation_commands' => [
Plugin\ApplicationConfigCustomizer::SECRET, 'db_create_schema' => [
Plugin\ApplicationConfigCustomizer::DISABLE_TRACK_PARAM, 'command' => 'bin/cli db:create',
Plugin\ApplicationConfigCustomizer::CHECK_VISITS_THRESHOLD, ],
Plugin\ApplicationConfigCustomizer::VISITS_THRESHOLD, 'db_migrate' => [
Plugin\ApplicationConfigCustomizer::BASE_PATH, 'command' => 'bin/cli db:migrate',
Plugin\ApplicationConfigCustomizer::WEB_WORKER_NUM, ],
Plugin\ApplicationConfigCustomizer::TASK_WORKER_NUM,
],
Plugin\DatabaseConfigCustomizer::class => [
Plugin\DatabaseConfigCustomizer::DRIVER,
Plugin\DatabaseConfigCustomizer::NAME,
Plugin\DatabaseConfigCustomizer::USER,
Plugin\DatabaseConfigCustomizer::PASSWORD,
Plugin\DatabaseConfigCustomizer::HOST,
Plugin\DatabaseConfigCustomizer::PORT,
],
Plugin\RedirectsConfigCustomizer::class => [
Plugin\RedirectsConfigCustomizer::INVALID_SHORT_URL_REDIRECT_TO,
Plugin\RedirectsConfigCustomizer::REGULAR_404_REDIRECT_TO,
Plugin\RedirectsConfigCustomizer::BASE_URL_REDIRECT_TO,
],
],
'installation_commands' => [
'db_create_schema' => [
'command' => 'bin/cli db:create',
],
'db_migrate' => [
'command' => 'bin/cli db:migrate',
], ],
], ],

View File

@ -2,11 +2,11 @@
declare(strict_types=1); declare(strict_types=1);
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Shlinkio\Shlink\Common\Cache\RedisFactory; use Shlinkio\Shlink\Common\Cache\RedisFactory;
use Shlinkio\Shlink\Common\Lock\RetryLockStoreDelegatorFactory; use Shlinkio\Shlink\Common\Lock\RetryLockStoreDelegatorFactory;
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory; use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
use Symfony\Component\Lock; use Symfony\Component\Lock;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
$localLockFactory = 'Shlinkio\Shlink\LocalLockFactory'; $localLockFactory = 'Shlinkio\Shlink\LocalLockFactory';

View File

@ -75,7 +75,7 @@ return [
], ],
], ],
'zend-expressive-swoole' => [ 'mezzio-swoole' => [
'swoole-http-server' => [ 'swoole-http-server' => [
'logger' => [ 'logger' => [
'logger-name' => 'Logger_Access', 'logger-name' => 'Logger_Access',

View File

@ -4,16 +4,16 @@ declare(strict_types=1);
namespace Shlinkio\Shlink; namespace Shlinkio\Shlink;
use Zend\Expressive; use Laminas\Stratigility\Middleware\ErrorHandler;
use Zend\ProblemDetails; use Mezzio;
use Zend\Stratigility\Middleware\ErrorHandler; use Mezzio\ProblemDetails;
return [ return [
'middleware_pipeline' => [ 'middleware_pipeline' => [
'error-handler' => [ 'error-handler' => [
'middleware' => [ 'middleware' => [
Expressive\Helper\ContentLengthMiddleware::class, Mezzio\Helper\ContentLengthMiddleware::class,
ErrorHandler::class, ErrorHandler::class,
], ],
], ],
@ -21,7 +21,6 @@ return [
'path' => '/rest', 'path' => '/rest',
'middleware' => [ 'middleware' => [
Rest\Middleware\CrossDomainMiddleware::class, Rest\Middleware\CrossDomainMiddleware::class,
Rest\Middleware\BackwardsCompatibleProblemDetailsMiddleware::class,
ProblemDetails\ProblemDetailsMiddleware::class, ProblemDetails\ProblemDetailsMiddleware::class,
], ],
], ],
@ -31,24 +30,17 @@ return [
Common\Middleware\CloseDbConnectionMiddleware::class, Common\Middleware\CloseDbConnectionMiddleware::class,
], ],
], ],
'pre-routing-rest' => [
'path' => '/rest',
'middleware' => [
Rest\Middleware\PathVersionMiddleware::class,
Rest\Middleware\ShortUrl\ShortCodePathMiddleware::class,
],
],
'routing' => [ 'routing' => [
'middleware' => [ 'middleware' => [
Expressive\Router\Middleware\RouteMiddleware::class, Mezzio\Router\Middleware\RouteMiddleware::class,
], ],
], ],
'rest' => [ 'rest' => [
'path' => '/rest', 'path' => '/rest',
'middleware' => [ 'middleware' => [
Expressive\Router\Middleware\ImplicitOptionsMiddleware::class, Mezzio\Router\Middleware\ImplicitOptionsMiddleware::class,
Rest\Middleware\BodyParserMiddleware::class, Rest\Middleware\BodyParserMiddleware::class,
Rest\Middleware\AuthenticationMiddleware::class, Rest\Middleware\AuthenticationMiddleware::class,
], ],
@ -56,7 +48,7 @@ return [
'dispatch' => [ 'dispatch' => [
'middleware' => [ 'middleware' => [
Expressive\Router\Middleware\DispatchMiddleware::class, Mezzio\Router\Middleware\DispatchMiddleware::class,
], ],
], ],

View File

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

View File

@ -1,13 +1,16 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
return [ return [
'redis' => [ 'cache' => [
'servers' => 'tcp://shlink_redis:6379', 'redis' => [
// 'servers' => [ 'servers' => 'tcp://shlink_redis:6379',
// 'tcp://shlink_redis:6379', // 'servers' => [
// ], // 'tcp://shlink_redis:6379',
// ],
],
], ],
'dependencies' => [ 'dependencies' => [

View File

@ -2,7 +2,7 @@
declare(strict_types=1); declare(strict_types=1);
use Zend\Expressive\Router\FastRouteRouter; use Mezzio\Router\FastRouteRouter;
return [ return [

View File

@ -1,5 +1,5 @@
<?php <?php
use Zend\Expressive\Router\FastRouteRouter; use Mezzio\Router\FastRouteRouter;
return [ return [

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
return [ return [
'zend-expressive-swoole' => [ 'mezzio-swoole' => [
'enable_coroutine' => true, 'enable_coroutine' => true,
'swoole-http-server' => [ 'swoole-http-server' => [

View File

@ -1,12 +1,12 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
use Zend\Expressive\Swoole\HotCodeReload\FileWatcher\InotifyFileWatcher; use Mezzio\Swoole\HotCodeReload\FileWatcher\InotifyFileWatcher;
use Zend\ServiceManager\Factory\InvokableFactory; use Laminas\ServiceManager\Factory\InvokableFactory;
return [ return [
'zend-expressive-swoole' => [ 'mezzio-swoole' => [
'hot-code-reload' => [ 'hot-code-reload' => [
'enable' => true, 'enable' => true,
], ],

View File

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

View File

@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
return [
'wkhtmltopdf' => [
'images' => [
'binary' => __DIR__ . '/../../bin/wkhtmltoimage',
'type' => 'jpg',
],
],
];

View File

@ -4,8 +4,8 @@ declare(strict_types=1);
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Console\ConsoleRunner; use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Laminas\ServiceManager\ServiceManager;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Zend\ServiceManager\ServiceManager;
return (function () { return (function () {
/** @var ContainerInterface|ServiceManager $container */ /** @var ContainerInterface|ServiceManager $container */

View File

@ -4,18 +4,19 @@ declare(strict_types=1);
namespace Shlinkio\Shlink; namespace Shlinkio\Shlink;
use Zend\ConfigAggregator; use Laminas\ConfigAggregator;
use Zend\Expressive; use Laminas\ZendFrameworkBridge;
use Zend\ProblemDetails; use Mezzio;
use Mezzio\ProblemDetails;
use function Shlinkio\Shlink\Common\env; use function Shlinkio\Shlink\Common\env;
return (new ConfigAggregator\ConfigAggregator([ return (new ConfigAggregator\ConfigAggregator([
Expressive\ConfigProvider::class, Mezzio\ConfigProvider::class,
Expressive\Router\ConfigProvider::class, Mezzio\Router\ConfigProvider::class,
Expressive\Router\FastRouteRouter\ConfigProvider::class, Mezzio\Router\FastRouteRouter\ConfigProvider::class,
Expressive\Plates\ConfigProvider::class, Mezzio\Plates\ConfigProvider::class,
Expressive\Swoole\ConfigProvider::class, Mezzio\Swoole\ConfigProvider::class,
ProblemDetails\ConfigProvider::class, ProblemDetails\ConfigProvider::class,
Common\ConfigProvider::class, Common\ConfigProvider::class,
IpGeolocation\ConfigProvider::class, IpGeolocation\ConfigProvider::class,
@ -23,12 +24,12 @@ return (new ConfigAggregator\ConfigAggregator([
CLI\ConfigProvider::class, CLI\ConfigProvider::class,
Rest\ConfigProvider::class, Rest\ConfigProvider::class,
EventDispatcher\ConfigProvider::class, EventDispatcher\ConfigProvider::class,
PreviewGenerator\ConfigProvider::class,
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'), new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
env('APP_ENV') === 'test' env('APP_ENV') === 'test'
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php') ? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
: new ConfigAggregator\ZendConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'), : new ConfigAggregator\LaminasConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
], 'data/cache/app_config.php', [ ], 'data/cache/app_config.php', [
ZendFrameworkBridge\ConfigPostProcessor::class,
Core\Config\SimplifiedConfigParser::class, Core\Config\SimplifiedConfigParser::class,
Core\Config\BasePathPrefixer::class, Core\Config\BasePathPrefixer::class,
Core\Config\DeprecatedConfigParser::class, Core\Config\DeprecatedConfigParser::class,

View File

@ -2,8 +2,8 @@
declare(strict_types=1); declare(strict_types=1);
use Laminas\ServiceManager\ServiceManager;
use Symfony\Component\Lock; use Symfony\Component\Lock;
use Zend\ServiceManager\ServiceManager;
chdir(dirname(__DIR__)); chdir(dirname(__DIR__));

View File

@ -2,9 +2,9 @@
declare(strict_types=1); declare(strict_types=1);
use Mezzio\Application;
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Application as CliApp; use Symfony\Component\Console\Application as CliApp;
use Zend\Expressive\Application;
return function (bool $isCli = false): void { return function (bool $isCli = false): void {
/** @var ContainerInterface $container */ /** @var ContainerInterface $container */

View File

@ -15,6 +15,4 @@ $em = $container->get(EntityManager::class);
$testHelper->createTestDb(); $testHelper->createTestDb();
ApiTest\ApiTestCase::setApiClient($container->get('shlink_test_api_client')); ApiTest\ApiTestCase::setApiClient($container->get('shlink_test_api_client'));
ApiTest\ApiTestCase::setSeedFixturesCallback(function () use ($testHelper, $em, $config) { ApiTest\ApiTestCase::setSeedFixturesCallback(fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []));
$testHelper->seedFixtures($em, $config['data_fixtures'] ?? []);
});

View File

@ -5,9 +5,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink; namespace Shlinkio\Shlink;
use GuzzleHttp\Client; use GuzzleHttp\Client;
use Laminas\ConfigAggregator\ConfigAggregator;
use Laminas\ServiceManager\Factory\InvokableFactory;
use PDO; use PDO;
use Zend\ConfigAggregator\ConfigAggregator;
use Zend\ServiceManager\Factory\InvokableFactory;
use function Shlinkio\Shlink\Common\env; use function Shlinkio\Shlink\Common\env;
use function sprintf; use function sprintf;
@ -19,9 +19,7 @@ $swooleTestingPort = 9999;
$buildDbConnection = function (): array { $buildDbConnection = function (): array {
$driver = env('DB_DRIVER', 'sqlite'); $driver = env('DB_DRIVER', 'sqlite');
$isCi = env('TRAVIS', false); $isCi = env('TRAVIS', false);
$getMysqlHost = function (string $driver) { $getMysqlHost = fn (string $driver) => sprintf('shlink_db%s', $driver === 'mysql' ? '' : '_maria');
return sprintf('shlink_db%s', $driver === 'mysql' ? '' : '_maria');
};
$driverConfigMap = [ $driverConfigMap = [
'sqlite' => [ 'sqlite' => [
@ -63,9 +61,10 @@ return [
'schema' => 'http', 'schema' => 'http',
'hostname' => 'doma.in', 'hostname' => 'doma.in',
], ],
'validate_url' => true,
], ],
'zend-expressive-swoole' => [ 'mezzio-swoole' => [
'enable_coroutine' => false, 'enable_coroutine' => false,
'swoole-http-server' => [ 'swoole-http-server' => [
'host' => $swooleTestingHost, 'host' => $swooleTestingHost,

View File

@ -11,7 +11,7 @@ server {
location ~ \.php$ { location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$; fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass unix:/var/run/php/php7.2-fpm.sock; fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php; fastcgi_index index.php;
include fastcgi.conf; include fastcgi.conf;
} }

View File

@ -8,7 +8,7 @@
# Description: Shlink non-blocking server with swoole # Description: Shlink non-blocking server with swoole
### END INIT INFO ### END INIT INFO
SCRIPT=/path/to/shlink/vendor/bin/zend-expressive-swoole\ start SCRIPT=/path/to/shlink/vendor/bin/mezzio-swoole\ start
RUNAS=root RUNAS=root
PIDFILE=/var/run/shlink_swoole.pid PIDFILE=/var/run/shlink_swoole.pid

View File

@ -1,18 +1,20 @@
FROM php:7.3.11-fpm-alpine3.10 FROM php:7.4.1-fpm-alpine3.10
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com> MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.18 ENV APCU_VERSION 5.1.18
ENV APCU_BC_VERSION 1.0.5 ENV APCU_BC_VERSION 1.0.5
ENV XDEBUG_VERSION 2.8.0 ENV XDEBUG_VERSION 2.9.0
RUN apk update RUN apk update
# Install common php extensions # Install common php extensions
RUN docker-php-ext-install pdo_mysql RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install iconv RUN docker-php-ext-install iconv
RUN docker-php-ext-install mbstring
RUN docker-php-ext-install calendar RUN docker-php-ext-install calendar
RUN apk add --no-cache oniguruma-dev
RUN docker-php-ext-install mbstring
RUN apk add --no-cache sqlite-libs RUN apk add --no-cache sqlite-libs
RUN apk add --no-cache sqlite-dev RUN apk add --no-cache sqlite-dev
RUN docker-php-ext-install pdo_sqlite RUN docker-php-ext-install pdo_sqlite

View File

@ -1,4 +1,4 @@
FROM php:7.3.11-alpine3.10 FROM php:7.4.1-alpine3.10
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com> MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.18 ENV APCU_VERSION 5.1.18
@ -11,9 +11,11 @@ RUN apk update
# Install common php extensions # Install common php extensions
RUN docker-php-ext-install pdo_mysql RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install iconv RUN docker-php-ext-install iconv
RUN docker-php-ext-install mbstring
RUN docker-php-ext-install calendar RUN docker-php-ext-install calendar
RUN apk add --no-cache oniguruma-dev
RUN docker-php-ext-install mbstring
RUN apk add --no-cache sqlite-libs RUN apk add --no-cache sqlite-libs
RUN apk add --no-cache sqlite-dev RUN apk add --no-cache sqlite-dev
RUN docker-php-ext-install pdo_sqlite RUN docker-php-ext-install pdo_sqlite
@ -90,4 +92,4 @@ CMD \
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \ if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
# When restarting the container, swoole might think it is already in execution # When restarting the container, swoole might think it is already in execution
# This forces the app to be started every second until the exit code is 0 # This forces the app to be started every second until the exit code is 0
until php ./vendor/bin/zend-expressive-swoole start; do sleep 1 ; done until php ./vendor/bin/mezzio-swoole start; do sleep 1 ; done

View File

@ -5,7 +5,7 @@ declare(strict_types=1);
namespace ShlinkMigrations; namespace ShlinkMigrations;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
/** /**
@ -30,12 +30,12 @@ class Version20160820191203 extends AbstractMigration
private function createTagsTable(Schema $schema): void private function createTagsTable(Schema $schema): void
{ {
$table = $schema->createTable('tags'); $table = $schema->createTable('tags');
$table->addColumn('id', Type::BIGINT, [ $table->addColumn('id', Types::BIGINT, [
'unsigned' => true, 'unsigned' => true,
'autoincrement' => true, 'autoincrement' => true,
'notnull' => true, 'notnull' => true,
]); ]);
$table->addColumn('name', Type::STRING, [ $table->addColumn('name', Types::STRING, [
'length' => 255, 'length' => 255,
'notnull' => true, 'notnull' => true,
]); ]);
@ -47,11 +47,11 @@ class Version20160820191203 extends AbstractMigration
private function createShortUrlsInTagsTable(Schema $schema): void private function createShortUrlsInTagsTable(Schema $schema): void
{ {
$table = $schema->createTable('short_urls_in_tags'); $table = $schema->createTable('short_urls_in_tags');
$table->addColumn('short_url_id', Type::BIGINT, [ $table->addColumn('short_url_id', Types::BIGINT, [
'unsigned' => true, 'unsigned' => true,
'notnull' => true, 'notnull' => true,
]); ]);
$table->addColumn('tag_id', Type::BIGINT, [ $table->addColumn('tag_id', Types::BIGINT, [
'unsigned' => true, 'unsigned' => true,
'notnull' => true, 'notnull' => true,
]); ]);

View File

@ -6,7 +6,7 @@ namespace ShlinkMigrations;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
/** /**
@ -24,10 +24,10 @@ class Version20171021093246 extends AbstractMigration
return; return;
} }
$shortUrls->addColumn('valid_since', Type::DATETIME, [ $shortUrls->addColumn('valid_since', Types::DATETIME, [
'notnull' => false, 'notnull' => false,
]); ]);
$shortUrls->addColumn('valid_until', Type::DATETIME, [ $shortUrls->addColumn('valid_until', Types::DATETIME, [
'notnull' => false, 'notnull' => false,
]); ]);
} }

View File

@ -6,7 +6,7 @@ namespace ShlinkMigrations;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
/** /**
@ -24,7 +24,7 @@ class Version20171022064541 extends AbstractMigration
return; return;
} }
$shortUrls->addColumn('max_visits', Type::INTEGER, [ $shortUrls->addColumn('max_visits', Types::INTEGER, [
'unsigned' => true, 'unsigned' => true,
'notnull' => false, 'notnull' => false,
]); ]);

View File

@ -17,7 +17,6 @@ final class Version20180801183328 extends AbstractMigration
private const OLD_SIZE = 10; private const OLD_SIZE = 10;
/** /**
* @param Schema $schema
* @throws SchemaException * @throws SchemaException
*/ */
public function up(Schema $schema): void public function up(Schema $schema): void
@ -26,7 +25,6 @@ final class Version20180801183328 extends AbstractMigration
} }
/** /**
* @param Schema $schema
* @throws SchemaException * @throws SchemaException
*/ */
public function down(Schema $schema): void public function down(Schema $schema): void
@ -35,8 +33,6 @@ final class Version20180801183328 extends AbstractMigration
} }
/** /**
* @param Schema $schema
* @param int $size
* @throws SchemaException * @throws SchemaException
*/ */
private function setSize(Schema $schema, int $size): void private function setSize(Schema $schema, int $size): void

View File

@ -17,7 +17,6 @@ use Shlinkio\Shlink\Common\Util\IpAddress;
final class Version20180913205455 extends AbstractMigration final class Version20180913205455 extends AbstractMigration
{ {
/** /**
* @param Schema $schema
*/ */
public function up(Schema $schema): void public function up(Schema $schema): void
{ {
@ -25,7 +24,6 @@ final class Version20180913205455 extends AbstractMigration
} }
/** /**
* @param Schema $schema
* @throws DBALException * @throws DBALException
*/ */
public function postUp(Schema $schema): void public function postUp(Schema $schema): void
@ -67,7 +65,6 @@ final class Version20180913205455 extends AbstractMigration
} }
/** /**
* @param Schema $schema
*/ */
public function down(Schema $schema): void public function down(Schema $schema): void
{ {

View File

@ -19,7 +19,6 @@ final class Version20180915110857 extends AbstractMigration
]; ];
/** /**
* @param Schema $schema
* @throws SchemaException * @throws SchemaException
*/ */
public function up(Schema $schema): void public function up(Schema $schema): void
@ -39,7 +38,7 @@ final class Version20180915110857 extends AbstractMigration
[ [
'onDelete' => self::ON_DELETE_MAP[$foreignTable], 'onDelete' => self::ON_DELETE_MAP[$foreignTable],
'onUpdate' => 'RESTRICT', 'onUpdate' => 'RESTRICT',
] ],
); );
} }
} }

View File

@ -8,7 +8,7 @@ use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Schema\Table;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
/** /**
@ -24,7 +24,6 @@ final class Version20181020060559 extends AbstractMigration
]; ];
/** /**
* @param Schema $schema
* @throws SchemaException * @throws SchemaException
*/ */
public function up(Schema $schema): void public function up(Schema $schema): void
@ -36,7 +35,7 @@ final class Version20181020060559 extends AbstractMigration
{ {
foreach ($columnNames as $name) { foreach ($columnNames as $name) {
if (! $visitLocations->hasColumn($name)) { if (! $visitLocations->hasColumn($name)) {
$visitLocations->addColumn($name, Type::STRING, ['notnull' => false]); $visitLocations->addColumn($name, Types::STRING, ['notnull' => false]);
} }
} }
} }

View File

@ -6,7 +6,7 @@ namespace ShlinkMigrations;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException; use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
final class Version20190930165521 extends AbstractMigration final class Version20190930165521 extends AbstractMigration
@ -22,19 +22,19 @@ final class Version20190930165521 extends AbstractMigration
} }
$domains = $schema->createTable('domains'); $domains = $schema->createTable('domains');
$domains->addColumn('id', Type::BIGINT, [ $domains->addColumn('id', Types::BIGINT, [
'unsigned' => true, 'unsigned' => true,
'autoincrement' => true, 'autoincrement' => true,
'notnull' => true, 'notnull' => true,
]); ]);
$domains->addColumn('authority', Type::STRING, [ $domains->addColumn('authority', Types::STRING, [
'length' => 512, 'length' => 512,
'notnull' => true, 'notnull' => true,
]); ]);
$domains->addUniqueIndex(['authority']); $domains->addUniqueIndex(['authority']);
$domains->setPrimaryKey(['id']); $domains->setPrimaryKey(['id']);
$shortUrls->addColumn('domain_id', Type::BIGINT, [ $shortUrls->addColumn('domain_id', Types::BIGINT, [
'unsigned' => true, 'unsigned' => true,
'notnull' => false, 'notnull' => false,
]); ]);

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
final class Version20200105165647 extends AbstractMigration
{
private const COLUMNS = ['lat' => 'latitude', 'lon' => 'longitude'];
public function preUp(Schema $schema): void
{
foreach (self::COLUMNS as $columnName) {
$qb = $this->connection->createQueryBuilder();
$qb->update('visit_locations')
->set($columnName, '"0"')
->where($columnName . '=""')
->orWhere($columnName . ' IS NULL')
->execute();
}
}
/**
* @throws DBALException
*/
public function up(Schema $schema): void
{
$visitLocations = $schema->getTable('visit_locations');
foreach (self::COLUMNS as $newName => $oldName) {
$visitLocations->addColumn($newName, Types::FLOAT);
}
}
public function postUp(Schema $schema): void
{
foreach (self::COLUMNS as $newName => $oldName) {
$qb = $this->connection->createQueryBuilder();
$qb->update('visit_locations')
->set($newName, $oldName)
->execute();
}
}
public function preDown(Schema $schema): void
{
foreach (self::COLUMNS as $newName => $oldName) {
$qb = $this->connection->createQueryBuilder();
$qb->update('visit_locations')
->set($oldName, $newName)
->execute();
}
}
/**
* @throws DBALException
*/
public function down(Schema $schema): void
{
$visitLocations = $schema->getTable('visit_locations');
foreach (self::COLUMNS as $colName => $oldName) {
$visitLocations->dropColumn($colName);
}
}
}

View File

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\DBALException;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
final class Version20200106215144 extends AbstractMigration
{
private const COLUMNS = ['latitude', 'longitude'];
/**
* @throws DBALException
*/
public function up(Schema $schema): void
{
$visitLocations = $schema->getTable('visit_locations');
foreach (self::COLUMNS as $colName) {
$visitLocations->dropColumn($colName);
}
}
/**
* @throws DBALException
*/
public function down(Schema $schema): void
{
$visitLocations = $schema->getTable('visit_locations');
foreach (self::COLUMNS as $colName) {
$visitLocations->addColumn($colName, Types::STRING, [
'notnull' => false,
]);
}
}
}

View File

@ -103,7 +103,7 @@ This is the complete list of supported env vars:
* **postgres** -> `5432` * **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. * `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`. * `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. * `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. * `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. * `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,9 +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. 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: An example using all env vars could look like this:
```bash ```bash
@ -138,7 +135,7 @@ docker run \
-e DB_PORT=3306 \ -e DB_PORT=3306 \
-e DISABLE_TRACK_PARAM="no-track" \ -e DISABLE_TRACK_PARAM="no-track" \
-e DELETE_SHORT_URL_THRESHOLD=30 \ -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 "INVALID_SHORT_URL_REDIRECT_TO=https://my-landing-page.com" \
-e "REGULAR_404_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" \ -e "BASE_URL_REDIRECT_TO=https://my-landing-page.com" \
@ -164,7 +161,7 @@ The whole configuration should have this format, but it can be split into multip
"delete_short_url_threshold": 30, "delete_short_url_threshold": 30,
"short_domain_schema": "https", "short_domain_schema": "https",
"short_domain_host": "doma.in", "short_domain_host": "doma.in",
"validate_url": false, "validate_url": true,
"invalid_short_url_redirect_to": "https://my-landing-page.com", "invalid_short_url_redirect_to": "https://my-landing-page.com",
"regular_404_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", "base_url_redirect_to": "https://my-landing-page.com",
@ -186,15 +183,12 @@ The whole configuration should have this format, but it can be split into multip
"password": "123abc", "password": "123abc",
"host": "something.rds.amazonaws.com", "host": "something.rds.amazonaws.com",
"port": "3306" "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). > 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: Once created just run shlink with the volume:
```bash ```bash
@ -211,20 +205,12 @@ These are some considerations to take into account when running multiple instanc
You can (and should) make the locks to be shared by all Shlink instances by using a redis server/cluster. Just define the `REDIS_SERVERS` env var with the list of servers. You can (and should) make the locks to be shared by all Shlink instances by using a redis server/cluster. Just define the `REDIS_SERVERS` env var with the list of servers.
* **Ignore this if using Shlink 1.20 or newer**. The first time shlink is run, it generates a charset used to generate short codes, which is a shuffled base62 charset.
If you are using several shlink instances, you will probably want all of them to use the same charset.
You can get a shuffled base62 charset by going to [https://shlink.io/short-code-chars](https://shlink.io/short-code-chars), and then you just need to pass it to all shlink instances using the `SHORTCODE_CHARS` env var.
If you don't do this, each shlink instance will use a different charset. However this shouldn't be a problem in practice, since the chances to get a collision will be very low.
## Versions ## Versions
Versioning on this docker image works as follows: Versioning on this docker image works as follows:
* `X.X.X`: when providing a specific version number, the image version will match the shlink version it contains. For example, installing `shlinkio/shlink:1.15.0`, you will get an image containing shlink v1.15.0. * `X.X.X`: when providing a specific version number, the image version will match the shlink version it contains. For example, installing `shlinkio/shlink:1.15.0`, you will get an image containing shlink v1.15.0.
* `stable`: always holds the latest stable tag. For example, if latest shlink version is 1.20.0, installing `shlinkio/shlink:stable`, you will get an image containing shlink v1.20.0 * `stable`: always holds the latest stable tag. For example, if latest shlink version is 2.0.0, installing `shlinkio/shlink:stable`, you will get an image containing shlink v2.0.0
* `latest`: always holds the latest contents in master, and it's considered unstable and not suitable for production. * `latest`: always holds the latest contents in master, and it's considered unstable and not suitable for production.
> **Important**: The docker image was introduced with shlink v1.15.0, so there are no official images previous to that versions. > **Important**: The docker image was introduced with shlink v1.15.0, so there are no official images previous to that versions.

View File

@ -8,19 +8,10 @@ use Monolog\Handler\StreamHandler;
use Monolog\Logger; use Monolog\Logger;
use function explode; use function explode;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function Functional\contains; use function Functional\contains;
use function implode;
use function Shlinkio\Shlink\Common\env; 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 { $helper = new class {
private const BASE62 = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
private const DB_DRIVERS_MAP = [ private const DB_DRIVERS_MAP = [
'mysql' => 'pdo_mysql', 'mysql' => 'pdo_mysql',
'maria' => 'pdo_mysql', 'maria' => 'pdo_mysql',
@ -32,40 +23,6 @@ $helper = new class {
'postgres' => '5432', '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 public function getDbConfig(): array
{ {
$driver = env('DB_DRIVER'); $driver = env('DB_DRIVER');
@ -94,7 +51,7 @@ $helper = new class {
public function getNotFoundRedirectsConfig(): array public function getNotFoundRedirectsConfig(): array
{ {
return [ 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'), 'regular_404' => env('REGULAR_404_REDIRECT_TO'),
'base_url' => env('BASE_URL_REDIRECT_TO'), 'base_url' => env('BASE_URL_REDIRECT_TO'),
]; ];
@ -105,6 +62,12 @@ $helper = new class {
$webhooks = env('VISITS_WEBHOOKS'); $webhooks = env('VISITS_WEBHOOKS');
return $webhooks === null ? [] : explode(',', $webhooks); return $webhooks === null ? [] : explode(',', $webhooks);
} }
public function getRedisConfig(): ?array
{
$redisServers = env('REDIS_SERVERS');
return $redisServers === null ? null : ['servers' => $redisServers];
}
}; };
return [ return [
@ -112,7 +75,6 @@ return [
'config_cache_enabled' => false, 'config_cache_enabled' => false,
'app_options' => [ 'app_options' => [
'secret_key' => $helper->getSecretKey(),
'disable_track_param' => env('DISABLE_TRACK_PARAM'), 'disable_track_param' => env('DISABLE_TRACK_PARAM'),
], ],
@ -130,7 +92,7 @@ return [
'schema' => env('SHORT_DOMAIN_SCHEMA', 'http'), 'schema' => env('SHORT_DOMAIN_SCHEMA', 'http'),
'hostname' => env('SHORT_DOMAIN_HOST', ''), 'hostname' => env('SHORT_DOMAIN_HOST', ''),
], ],
'validate_url' => (bool) env('VALIDATE_URLS', true), 'validate_url' => (bool) env('VALIDATE_URLS', false),
'visits_webhooks' => $helper->getVisitsWebhooks(), 'visits_webhooks' => $helper->getVisitsWebhooks(),
], ],
@ -156,15 +118,15 @@ return [
], ],
], ],
'redis' => [ 'cache' => [
'servers' => env('REDIS_SERVERS'), 'redis' => $helper->getRedisConfig(),
], ],
'router' => [ 'router' => [
'base_path' => env('BASE_PATH', ''), 'base_path' => env('BASE_PATH', ''),
], ],
'zend-expressive-swoole' => [ 'mezzio-swoole' => [
'swoole-http-server' => [ 'swoole-http-server' => [
'options' => [ 'options' => [
'worker_num' => (int) env('WEB_WORKER_NUM', 16), 'worker_num' => (int) env('WEB_WORKER_NUM', 16),

View File

@ -14,4 +14,4 @@ php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies -n -q
# When restarting the container, swoole might think it is already in execution # When restarting the container, swoole might think it is already in execution
# This forces the app to be started every second until the exit code is 0 # This forces the app to be started every second until the exit code is 0
until php vendor/zendframework/zend-expressive-swoole/bin/zend-expressive-swoole start; do sleep 1 ; done until php vendor/mezzio/mezzio-swoole/bin/mezzio-swoole start; do sleep 1 ; done

View File

@ -17,16 +17,6 @@
"status": { "status": {
"type": "number", "type": "number",
"description": "HTTP response status code" "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": { "meta": {
"$ref": "./ShortUrlMeta.json" "$ref": "./ShortUrlMeta.json"
},
"originalUrl": {
"deprecated": true,
"type": "string",
"description": "The original long URL. [DEPRECATED. Use longUrl instead]"
} }
} }
} }

View File

@ -16,11 +16,6 @@
}, },
"visitLocation": { "visitLocation": {
"$ref": "./VisitLocation.json" "$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" "type": "string"
}, },
"latitude": { "latitude": {
"type": "string" "type": "number"
}, },
"longitude": { "longitude": {
"type": "string" "type": "number"
}, },
"regionName": { "regionName": {
"type": "string" "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" "Short URLs"
], ],
"summary": "List 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": [ "parameters": [
{ {
"$ref": "../parameters/version.json" "$ref": "../parameters/version.json"
@ -77,9 +77,6 @@
"security": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"responses": { "responses": {
@ -187,13 +184,10 @@
"Short URLs" "Short URLs"
], ],
"summary": "Create short URL", "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": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"parameters": [ "parameters": [

View File

@ -5,7 +5,7 @@
"Short URLs" "Short URLs"
], ],
"summary": "Create a short URL", "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": [ "parameters": [
{ {
"$ref": "../parameters/version.json" "$ref": "../parameters/version.json"

View File

@ -5,7 +5,7 @@
"Short URLs" "Short URLs"
], ],
"summary": "Parse short code", "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": [ "parameters": [
{ {
"$ref": "../parameters/version.json" "$ref": "../parameters/version.json"
@ -32,9 +32,6 @@
"security": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"responses": { "responses": {
@ -94,7 +91,7 @@
"Short URLs" "Short URLs"
], ],
"summary": "Edit short URL", "summary": "Edit short URL",
"description": "Update certain meta arguments from an existing short URL.<br><br>**Important note**: Before shlink v1.13, this endpoint used to use the `/short-codes` path instead of `/short-urls`. Both of them will keep working, while the first one is considered deprecated.", "description": "Update certain meta arguments from an existing short URL.",
"parameters": [ "parameters": [
{ {
"$ref": "../parameters/version.json" "$ref": "../parameters/version.json"
@ -137,9 +134,6 @@
"security": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"responses": { "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": { "delete": {
"operationId": "deleteShortUrl", "operationId": "deleteShortUrl",
"tags": [ "tags": [
"Short URLs" "Short URLs"
], ],
"summary": "Delete short URL", "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": [ "parameters": [
{ {
"$ref": "../parameters/version.json" "$ref": "../parameters/version.json"
@ -317,9 +219,6 @@
"security": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"responses": { "responses": {

View File

@ -5,7 +5,7 @@
"Short URLs" "Short URLs"
], ],
"summary": "Edit tags on short URL", "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": [ "parameters": [
{ {
"$ref": "../parameters/version.json" "$ref": "../parameters/version.json"
@ -46,9 +46,6 @@
"security": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"responses": { "responses": {

View File

@ -5,7 +5,7 @@
"Visits" "Visits"
], ],
"summary": "List visits for short URL", "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": [ "parameters": [
{ {
"$ref": "../parameters/version.json" "$ref": "../parameters/version.json"
@ -59,9 +59,6 @@
"security": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"responses": { "responses": {
@ -108,8 +105,8 @@
"cityName": "Cupertino", "cityName": "Cupertino",
"countryCode": "US", "countryCode": "US",
"countryName": "United States", "countryName": "United States",
"latitude": "37.3042", "latitude": 37.3042,
"longitude": "-122.0946", "longitude": -122.0946,
"regionName": "California", "regionName": "California",
"timezone": "America/Los_Angeles" "timezone": "America/Los_Angeles"
} }

View File

@ -9,9 +9,6 @@
"security": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"parameters": [ "parameters": [
@ -78,9 +75,6 @@
"security": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"parameters": [ "parameters": [
@ -170,9 +164,6 @@
"security": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"parameters": [ "parameters": [
@ -279,9 +270,6 @@
"security": [ "security": [
{ {
"ApiKey": [] "ApiKey": []
},
{
"Bearer": []
} }
], ],
"responses": { "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", "type": "apiKey",
"in": "header", "in": "header",
"name": "X-Api-Key" "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", "name": "URL Shortener",
"description": "Non-rest endpoints, used to be publicly exposed" "description": "Non-rest endpoints, used to be publicly exposed"
},
{
"name": "Authentication",
"description": "**[DEPRECATED]** Authentication-related endpoints"
} }
], ],
@ -104,13 +94,6 @@
}, },
"/{shortCode}/qr-code": { "/{shortCode}/qr-code": {
"$ref": "paths/{shortCode}_qr-code.json" "$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\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class, Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
Command\ShortUrl\GetVisitsCommand::NAME => Command\ShortUrl\GetVisitsCommand::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\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::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\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class, Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,

View File

@ -6,19 +6,18 @@ namespace Shlinkio\Shlink\CLI;
use Doctrine\DBAL\Connection; use Doctrine\DBAL\Connection;
use GeoIp2\Database\Reader; use GeoIp2\Database\Reader;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater; use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory; use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
use Shlinkio\Shlink\Core\Service; use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory; use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater; use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface; use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
use Shlinkio\Shlink\PreviewGenerator\Service\PreviewGenerator;
use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console as SymfonyCli; use Symfony\Component\Console as SymfonyCli;
use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\PhpExecutableFinder;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;
return [ return [
@ -34,14 +33,9 @@ return [
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\GetVisitsCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class, Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
Command\Visit\LocateVisitsCommand::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\GenerateKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class, Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
@ -64,7 +58,6 @@ return [
Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class], Command\ShortUrl\ResolveUrlCommand::class => [Service\UrlShortener::class],
Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'], Command\ShortUrl\ListShortUrlsCommand::class => [Service\ShortUrlService::class, 'config.url_shortener.domain'],
Command\ShortUrl\GetVisitsCommand::class => [Service\VisitsTracker::class], 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\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
Command\Visit\LocateVisitsCommand::class => [ Command\Visit\LocateVisitsCommand::class => [
@ -73,7 +66,6 @@ return [
LockFactory::class, LockFactory::class,
GeolocationDbUpdater::class, GeolocationDbUpdater::class,
], ],
Command\Visit\UpdateDbCommand::class => [DbUpdater::class],
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class], Command\Api\GenerateKeyCommand::class => [ApiKeyService::class],
Command\Api\DisableKeyCommand::class => [ApiKeyService::class], Command\Api\DisableKeyCommand::class => [ApiKeyService::class],

View File

@ -19,8 +19,7 @@ class DisableKeyCommand extends Command
{ {
public const NAME = 'api-key:disable'; public const NAME = 'api-key:disable';
/** @var ApiKeyServiceInterface */ private ApiKeyServiceInterface $apiKeyService;
private $apiKeyService;
public function __construct(ApiKeyServiceInterface $apiKeyService) public function __construct(ApiKeyServiceInterface $apiKeyService)
{ {

View File

@ -19,8 +19,7 @@ class GenerateKeyCommand extends Command
{ {
public const NAME = 'api-key:generate'; public const NAME = 'api-key:generate';
/** @var ApiKeyServiceInterface */ private ApiKeyServiceInterface $apiKeyService;
private $apiKeyService;
public function __construct(ApiKeyServiceInterface $apiKeyService) public function __construct(ApiKeyServiceInterface $apiKeyService)
{ {
@ -37,7 +36,7 @@ class GenerateKeyCommand extends Command
'expirationDate', 'expirationDate',
'e', 'e',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'The date in which the API key should expire. Use any valid PHP format.' 'The date in which the API key should expire. Use any valid PHP format.',
); );
} }

View File

@ -25,8 +25,7 @@ class ListKeysCommand extends Command
public const NAME = 'api-key:list'; public const NAME = 'api-key:list';
/** @var ApiKeyServiceInterface */ private ApiKeyServiceInterface $apiKeyService;
private $apiKeyService;
public function __construct(ApiKeyServiceInterface $apiKeyService) public function __construct(ApiKeyServiceInterface $apiKeyService)
{ {
@ -43,7 +42,7 @@ class ListKeysCommand extends Command
'enabledOnly', 'enabledOnly',
'e', 'e',
InputOption::VALUE_NONE, InputOption::VALUE_NONE,
'Tells if only enabled API keys should be returned.' 'Tells if only enabled API keys should be returned.',
); );
} }
@ -82,8 +81,6 @@ class ListKeysCommand extends Command
} }
/** /**
* @param ApiKey $apiKey
* @return string
*/ */
private function getEnabledSymbol(ApiKey $apiKey): string private function getEnabledSymbol(ApiKey $apiKey): string
{ {

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

@ -15,10 +15,8 @@ use function array_unshift;
abstract class AbstractDatabaseCommand extends AbstractLockedCommand abstract class AbstractDatabaseCommand extends AbstractLockedCommand
{ {
/** @var ProcessHelper */ private ProcessHelper $processHelper;
private $processHelper; private string $phpBinary;
/** @var string */
private $phpBinary;
public function __construct(LockFactory $locker, ProcessHelper $processHelper, PhpExecutableFinder $phpFinder) public function __construct(LockFactory $locker, ProcessHelper $processHelper, PhpExecutableFinder $phpFinder)
{ {

View File

@ -21,10 +21,8 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
public const DOCTRINE_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php'; public const DOCTRINE_SCRIPT = 'vendor/doctrine/orm/bin/doctrine.php';
public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create'; public const DOCTRINE_CREATE_SCHEMA_COMMAND = 'orm:schema-tool:create';
/** @var Connection */ private Connection $regularConn;
private $regularConn; private Connection $noDbNameConn;
/** @var Connection */
private $noDbNameConn;
public function __construct( public function __construct(
LockFactory $locker, LockFactory $locker,
@ -43,7 +41,7 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
$this $this
->setName(self::NAME) ->setName(self::NAME)
->setDescription( ->setDescription(
'Creates the database needed for shlink to work. It will do nothing if the database already exists' 'Creates the database needed for shlink to work. It will do nothing if the database already exists',
); );
} }

View File

@ -19,10 +19,8 @@ use function sprintf;
class DeleteShortUrlCommand extends Command class DeleteShortUrlCommand extends Command
{ {
public const NAME = 'short-url:delete'; public const NAME = 'short-url:delete';
private const ALIASES = ['short-code:delete'];
/** @var DeleteShortUrlServiceInterface */ private DeleteShortUrlServiceInterface $deleteShortUrlService;
private $deleteShortUrlService;
public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService) public function __construct(DeleteShortUrlServiceInterface $deleteShortUrlService)
{ {
@ -34,7 +32,6 @@ class DeleteShortUrlCommand extends Command
{ {
$this $this
->setName(self::NAME) ->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('Deletes a short URL') ->setDescription('Deletes a short URL')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code for the short URL to be deleted') ->addArgument('shortCode', InputArgument::REQUIRED, 'The short code for the short URL to be deleted')
->addOption( ->addOption(
@ -42,7 +39,7 @@ class DeleteShortUrlCommand extends Command
'i', 'i',
InputOption::VALUE_NONE, InputOption::VALUE_NONE,
'Ignores the safety visits threshold check, which could make short URLs with many visits to be ' 'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
. 'accidentally deleted' . 'accidentally deleted',
); );
} }

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

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl; namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Laminas\Diactoros\Uri;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException; use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
@ -15,7 +16,6 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\Diactoros\Uri;
use function array_map; use function array_map;
use function Functional\curry; use function Functional\curry;
@ -26,12 +26,9 @@ use function sprintf;
class GenerateShortUrlCommand extends Command class GenerateShortUrlCommand extends Command
{ {
public const NAME = 'short-url:generate'; public const NAME = 'short-url:generate';
private const ALIASES = ['shortcode:generate', 'short-code:generate'];
/** @var UrlShortenerInterface */ private UrlShortenerInterface $urlShortener;
private $urlShortener; private array $domainConfig;
/** @var array */
private $domainConfig;
public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig) public function __construct(UrlShortenerInterface $urlShortener, array $domainConfig)
{ {
@ -44,52 +41,51 @@ class GenerateShortUrlCommand extends Command
{ {
$this $this
->setName(self::NAME) ->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('Generates a short URL for provided long URL and returns it') ->setDescription('Generates a short URL for provided long URL and returns it')
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse') ->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse')
->addOption( ->addOption(
'tags', 'tags',
't', 't',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
'Tags to apply to the new short URL' 'Tags to apply to the new short URL',
) )
->addOption( ->addOption(
'validSince', 'validSince',
's', 's',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'The date from which this short URL will be valid. ' 'The date from which this short URL will be valid. '
. 'If someone tries to access it before this date, it will not be found.' . 'If someone tries to access it before this date, it will not be found.',
) )
->addOption( ->addOption(
'validUntil', 'validUntil',
'u', 'u',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'The date until which this short URL will be valid. ' 'The date until which this short URL will be valid. '
. 'If someone tries to access it after this date, it will not be found.' . 'If someone tries to access it after this date, it will not be found.',
) )
->addOption( ->addOption(
'customSlug', 'customSlug',
'c', 'c',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'If provided, this slug will be used instead of generating a short code' 'If provided, this slug will be used instead of generating a short code',
) )
->addOption( ->addOption(
'maxVisits', 'maxVisits',
'm', 'm',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'This will limit the number of visits for this short URL.' 'This will limit the number of visits for this short URL.',
) )
->addOption( ->addOption(
'findIfExists', 'findIfExists',
'f', 'f',
InputOption::VALUE_NONE, InputOption::VALUE_NONE,
'This will force existing matching URL to be returned if found, instead of creating a new one.' 'This will force existing matching URL to be returned if found, instead of creating a new one.',
) )
->addOption( ->addOption(
'domain', 'domain',
'd', 'd',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'The domain to which this short URL will be attached.' 'The domain to which this short URL will be attached.',
); );
} }
@ -131,8 +127,8 @@ class GenerateShortUrlCommand extends Command
$customSlug, $customSlug,
$maxVisits !== null ? (int) $maxVisits : null, $maxVisits !== null ? (int) $maxVisits : null,
$input->getOption('findIfExists'), $input->getOption('findIfExists'),
$input->getOption('domain') $input->getOption('domain'),
) ),
); );
$io->writeln([ $io->writeln([

View File

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

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl; namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos; use Cake\Chronos\Chronos;
use Laminas\Paginator\Paginator;
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand; use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\CLI\Util\ShlinkTable;
@ -17,7 +18,6 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\Paginator\Paginator;
use function array_flip; use function array_flip;
use function array_intersect_key; use function array_intersect_key;
@ -32,7 +32,6 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
use PaginatorUtilsTrait; use PaginatorUtilsTrait;
public const NAME = 'short-url:list'; public const NAME = 'short-url:list';
private const ALIASES = ['shortcode:list', 'short-code:list'];
private const COLUMNS_WHITELIST = [ private const COLUMNS_WHITELIST = [
'shortCode', 'shortCode',
'shortUrl', 'shortUrl',
@ -42,10 +41,8 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
'tags', 'tags',
]; ];
/** @var ShortUrlServiceInterface */ private ShortUrlServiceInterface $shortUrlService;
private $shortUrlService; private ShortUrlDataTransformer $transformer;
/** @var ShortUrlDataTransformer */
private $transformer;
public function __construct(ShortUrlServiceInterface $shortUrlService, array $domainConfig) public function __construct(ShortUrlServiceInterface $shortUrlService, array $domainConfig)
{ {
@ -58,32 +55,31 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
{ {
$this $this
->setName(self::NAME) ->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('List all short URLs') ->setDescription('List all short URLs')
->addOption( ->addOption(
'page', 'page',
'p', 'p',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
sprintf('The first page to list (%s items per page)', ShortUrlRepositoryAdapter::ITEMS_PER_PAGE), sprintf('The first page to list (%s items per page)', ShortUrlRepositoryAdapter::ITEMS_PER_PAGE),
'1' '1',
) )
->addOption( ->addOption(
'searchTerm', 'searchTerm',
'st', 'st',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'A query used to filter results by searching for it on the longUrl and shortCode fields' 'A query used to filter results by searching for it on the longUrl and shortCode fields',
) )
->addOption( ->addOption(
'tags', 'tags',
't', 't',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'A comma-separated list of tags to filter results' 'A comma-separated list of tags to filter results',
) )
->addOption( ->addOption(
'orderBy', 'orderBy',
'o', 'o',
InputOption::VALUE_REQUIRED, InputOption::VALUE_REQUIRED,
'The field from which we want to order by. Pass ASC or DESC separated by a comma' 'The field from which we want to order by. Pass ASC or DESC separated by a comma',
) )
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not'); ->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not');
} }
@ -126,6 +122,9 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
return ExitCodes::EXIT_SUCCESS; return ExitCodes::EXIT_SUCCESS;
} }
/**
* @param string|array|null $orderBy
*/
private function renderPage( private function renderPage(
OutputInterface $output, OutputInterface $output,
int $page, int $page,
@ -141,7 +140,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$searchTerm, $searchTerm,
$tags, $tags,
$orderBy, $orderBy,
new DateRange($startDate, $endDate) new DateRange($startDate, $endDate),
); );
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count']; $headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
@ -163,7 +162,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage( ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
$result, $result,
'Page %s of %s' 'Page %s of %s',
)); ));
return $result; return $result;

View File

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

View File

@ -16,8 +16,7 @@ class CreateTagCommand extends Command
{ {
public const NAME = 'tag:create'; public const NAME = 'tag:create';
/** @var TagServiceInterface */ private TagServiceInterface $tagService;
private $tagService;
public function __construct(TagServiceInterface $tagService) public function __construct(TagServiceInterface $tagService)
{ {
@ -34,7 +33,7 @@ class CreateTagCommand extends Command
'name', 'name',
't', 't',
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'The name of the tags to create' 'The name of the tags to create',
); );
} }

View File

@ -16,8 +16,7 @@ class DeleteTagsCommand extends Command
{ {
public const NAME = 'tag:delete'; public const NAME = 'tag:delete';
/** @var TagServiceInterface */ private TagServiceInterface $tagService;
private $tagService;
public function __construct(TagServiceInterface $tagService) public function __construct(TagServiceInterface $tagService)
{ {
@ -34,7 +33,7 @@ class DeleteTagsCommand extends Command
'name', 'name',
't', 't',
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'The name of the tags to delete' 'The name of the tags to delete',
); );
} }

View File

@ -18,8 +18,7 @@ class ListTagsCommand extends Command
{ {
public const NAME = 'tag:list'; public const NAME = 'tag:list';
/** @var TagServiceInterface */ private TagServiceInterface $tagService;
private $tagService;
public function __construct(TagServiceInterface $tagService) public function __construct(TagServiceInterface $tagService)
{ {
@ -47,8 +46,6 @@ class ListTagsCommand extends Command
return [['No tags yet']]; return [['No tags yet']];
} }
return map($tags, function (Tag $tag) { return map($tags, fn (Tag $tag) => [(string) $tag]);
return [(string) $tag];
});
} }
} }

View File

@ -18,8 +18,7 @@ class RenameTagCommand extends Command
{ {
public const NAME = 'tag:rename'; public const NAME = 'tag:rename';
/** @var TagServiceInterface */ private TagServiceInterface $tagService;
private $tagService;
public function __construct(TagServiceInterface $tagService) public function __construct(TagServiceInterface $tagService)
{ {

View File

@ -14,8 +14,7 @@ use function sprintf;
abstract class AbstractLockedCommand extends Command abstract class AbstractLockedCommand extends Command
{ {
/** @var LockFactory */ private LockFactory $locker;
private $locker;
public function __construct(LockFactory $locker) public function __construct(LockFactory $locker)
{ {
@ -30,7 +29,7 @@ abstract class AbstractLockedCommand extends Command
if (! $lock->acquire($lockConfig->isBlocking())) { if (! $lock->acquire($lockConfig->isBlocking())) {
$output->writeln( $output->writeln(
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName()) sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName()),
); );
return ExitCodes::EXIT_WARNING; return ExitCodes::EXIT_WARNING;
} }

View File

@ -36,7 +36,7 @@ abstract class AbstractWithDateRangeCommand extends Command
$output->writeln(sprintf( $output->writeln(sprintf(
'<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>', '<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>',
$key, $key,
$value $value,
)); ));
if ($output->isVeryVerbose()) { if ($output->isVeryVerbose()) {

View File

@ -8,12 +8,9 @@ final class LockedCommandConfig
{ {
private const DEFAULT_TTL = 90.0; // 1.5 minutes private const DEFAULT_TTL = 90.0; // 1.5 minutes
/** @var string */ private string $lockName;
private $lockName; private bool $isBlocking;
/** @var bool */ private float $ttl;
private $isBlocking;
/** @var float */
private $ttl;
public function __construct(string $lockName, bool $isBlocking = false, float $ttl = self::DEFAULT_TTL) public function __construct(string $lockName, bool $isBlocking = false, float $ttl = self::DEFAULT_TTL)
{ {

View File

@ -30,19 +30,13 @@ use function sprintf;
class LocateVisitsCommand extends AbstractLockedCommand class LocateVisitsCommand extends AbstractLockedCommand
{ {
public const NAME = 'visit:locate'; public const NAME = 'visit:locate';
public const ALIASES = ['visit:process'];
/** @var VisitServiceInterface */ private VisitServiceInterface $visitService;
private $visitService; private IpLocationResolverInterface $ipLocationResolver;
/** @var IpLocationResolverInterface */ private GeolocationDbUpdaterInterface $dbUpdater;
private $ipLocationResolver;
/** @var GeolocationDbUpdaterInterface */
private $dbUpdater;
/** @var SymfonyStyle */ private SymfonyStyle $io;
private $io; private ?ProgressBar $progressBar = null;
/** @var ProgressBar */
private $progressBar;
public function __construct( public function __construct(
VisitServiceInterface $visitService, VisitServiceInterface $visitService,
@ -60,7 +54,6 @@ class LocateVisitsCommand extends AbstractLockedCommand
{ {
$this $this
->setName(self::NAME) ->setName(self::NAME)
->setAliases(self::ALIASES)
->setDescription('Resolves visits origin locations.'); ->setDescription('Resolves visits origin locations.');
} }
@ -73,13 +66,13 @@ class LocateVisitsCommand extends AbstractLockedCommand
$this->visitService->locateUnlocatedVisits( $this->visitService->locateUnlocatedVisits(
[$this, 'getGeolocationDataForVisit'], [$this, 'getGeolocationDataForVisit'],
static function (VisitLocation $location) use ($output) { static function (VisitLocation $location) use ($output): void {
if (!$location->isEmpty()) { if (!$location->isEmpty()) {
$output->writeln( $output->writeln(
sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName()) sprintf(' [<info>Address located at "%s"</info>]', $location->getCountryName()),
); );
} }
} },
); );
$this->io->success('Finished processing all IPs'); $this->io->success('Finished processing all IPs');
@ -99,7 +92,7 @@ class LocateVisitsCommand extends AbstractLockedCommand
if (! $visit->hasRemoteAddr()) { if (! $visit->hasRemoteAddr()) {
$this->io->writeln( $this->io->writeln(
'<comment>Ignored visit with no IP address</comment>', '<comment>Ignored visit with no IP address</comment>',
OutputInterface::VERBOSITY_VERBOSE OutputInterface::VERBOSITY_VERBOSE,
); );
throw IpCannotBeLocatedException::forEmptyAddress(); throw IpCannotBeLocatedException::forEmptyAddress();
} }
@ -126,12 +119,12 @@ class LocateVisitsCommand extends AbstractLockedCommand
private function checkDbUpdate(): void private function checkDbUpdate(): void
{ {
try { try {
$this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) { $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists): void {
$this->io->writeln( $this->io->writeln(
sprintf('<fg=blue>%s GeoLite2 database...</>', $olderDbExists ? 'Updating' : 'Downloading') sprintf('<fg=blue>%s GeoLite2 database...</>', $olderDbExists ? 'Updating' : 'Downloading'),
); );
$this->progressBar = new ProgressBar($this->io); $this->progressBar = new ProgressBar($this->io);
}, function (int $total, int $downloaded) { }, function (int $total, int $downloaded): void {
$this->progressBar->setMaxSteps($total); $this->progressBar->setMaxSteps($total);
$this->progressBar->setProgress($downloaded); $this->progressBar->setProgress($downloaded);
}); });
@ -148,7 +141,7 @@ class LocateVisitsCommand extends AbstractLockedCommand
$this->io->newLine(); $this->io->newLine();
$this->io->writeln( $this->io->writeln(
'<fg=yellow;options=bold>[Warning] GeoLite2 database update failed. Proceeding with old version.</>' '<fg=yellow;options=bold>[Warning] GeoLite2 database update failed. Proceeding with old version.</>',
); );
} }
} }

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

@ -8,7 +8,7 @@ use function Shlinkio\Shlink\Common\loadConfigFromGlob;
class ConfigProvider class ConfigProvider
{ {
public function __invoke() public function __invoke(): array
{ {
return loadConfigFromGlob(__DIR__ . '/../config/{,*.}config.php'); return loadConfigFromGlob(__DIR__ . '/../config/{,*.}config.php');
} }

View File

@ -9,15 +9,14 @@ use Throwable;
class GeolocationDbUpdateFailedException extends RuntimeException implements ExceptionInterface class GeolocationDbUpdateFailedException extends RuntimeException implements ExceptionInterface
{ {
/** @var bool */ private bool $olderDbExists;
private $olderDbExists;
public static function create(bool $olderDbExists, ?Throwable $prev = null): self public static function create(bool $olderDbExists, ?Throwable $prev = null): self
{ {
$e = new self( $e = new self(
'An error occurred while updating geolocation database, and an older version could not be found', 'An error occurred while updating geolocation database, and an older version could not be found',
0, 0,
$prev $prev,
); );
$e->olderDbExists = $olderDbExists; $e->olderDbExists = $olderDbExists;

View File

@ -15,12 +15,9 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
{ {
private const LOCK_NAME = 'geolocation-db-update'; private const LOCK_NAME = 'geolocation-db-update';
/** @var DbUpdaterInterface */ private DbUpdaterInterface $dbUpdater;
private $dbUpdater; private Reader $geoLiteDbReader;
/** @var Reader */ private LockFactory $locker;
private $geoLiteDbReader;
/** @var LockFactory */
private $locker;
public function __construct(DbUpdaterInterface $dbUpdater, Reader $geoLiteDbReader, LockFactory $locker) public function __construct(DbUpdaterInterface $dbUpdater, Reader $geoLiteDbReader, LockFactory $locker)
{ {

View File

@ -12,8 +12,7 @@ final class ShlinkTable
private const DEFAULT_STYLE_NAME = 'default'; private const DEFAULT_STYLE_NAME = 'default';
private const TABLE_TITLE_STYLE = '<options=bold> %s </>'; private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
/** @var Table|null */ private ?Table $baseTable;
private $baseTable;
public function __construct(Table $baseTable) public function __construct(Table $baseTable)
{ {

View File

@ -8,20 +8,18 @@ use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand; use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
class DisableKeyCommandTest extends TestCase class DisableKeyCommandTest extends TestCase
{ {
/** @var CommandTester */ private CommandTester $commandTester;
private $commandTester; private ObjectProphecy $apiKeyService;
/** @var ObjectProphecy */
private $apiKeyService;
public function setUp(): void public function setUp(): void
{ {
$this->apiKeyService = $this->prophesize(ApiKeyService::class); $this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$command = new DisableKeyCommand($this->apiKeyService->reveal()); $command = new DisableKeyCommand($this->apiKeyService->reveal());
$app = new Application(); $app = new Application();
$app->add($command); $app->add($command);

View File

@ -10,20 +10,18 @@ use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand; use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
class GenerateKeyCommandTest extends TestCase class GenerateKeyCommandTest extends TestCase
{ {
/** @var CommandTester */ private CommandTester $commandTester;
private $commandTester; private ObjectProphecy $apiKeyService;
/** @var ObjectProphecy */
private $apiKeyService;
public function setUp(): void public function setUp(): void
{ {
$this->apiKeyService = $this->prophesize(ApiKeyService::class); $this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$command = new GenerateKeyCommand($this->apiKeyService->reveal()); $command = new GenerateKeyCommand($this->apiKeyService->reveal());
$app = new Application(); $app = new Application();
$app->add($command); $app->add($command);
@ -31,7 +29,7 @@ class GenerateKeyCommandTest extends TestCase
} }
/** @test */ /** @test */
public function noExpirationDateIsDefinedIfNotProvided() public function noExpirationDateIsDefinedIfNotProvided(): void
{ {
$create = $this->apiKeyService->create(null)->willReturn(new ApiKey()); $create = $this->apiKeyService->create(null)->willReturn(new ApiKey());
@ -43,7 +41,7 @@ class GenerateKeyCommandTest extends TestCase
} }
/** @test */ /** @test */
public function expirationDateIsDefinedIfProvided() public function expirationDateIsDefinedIfProvided(): void
{ {
$this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledOnce() $this->apiKeyService->create(Argument::type(Chronos::class))->shouldBeCalledOnce()
->willReturn(new ApiKey()); ->willReturn(new ApiKey());

View File

@ -8,20 +8,18 @@ use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand; use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
class ListKeysCommandTest extends TestCase class ListKeysCommandTest extends TestCase
{ {
/** @var CommandTester */ private CommandTester $commandTester;
private $commandTester; private ObjectProphecy $apiKeyService;
/** @var ObjectProphecy */
private $apiKeyService;
public function setUp(): void public function setUp(): void
{ {
$this->apiKeyService = $this->prophesize(ApiKeyService::class); $this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$command = new ListKeysCommand($this->apiKeyService->reveal()); $command = new ListKeysCommand($this->apiKeyService->reveal());
$app = new Application(); $app = new Application();
$app->add($command); $app->add($command);

View File

@ -1,48 +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
{
/** @var CommandTester */
private $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

@ -21,25 +21,19 @@ use Symfony\Component\Process\PhpExecutableFinder;
class CreateDatabaseCommandTest extends TestCase class CreateDatabaseCommandTest extends TestCase
{ {
/** @var CommandTester */ private CommandTester $commandTester;
private $commandTester; private ObjectProphecy $processHelper;
/** @var ObjectProphecy */ private ObjectProphecy $regularConn;
private $processHelper; private ObjectProphecy $noDbNameConn;
/** @var ObjectProphecy */ private ObjectProphecy $schemaManager;
private $regularConn; private ObjectProphecy $databasePlatform;
/** @var ObjectProphecy */
private $noDbNameConn;
/** @var ObjectProphecy */
private $schemaManager;
/** @var ObjectProphecy */
private $databasePlatform;
public function setUp(): void public function setUp(): void
{ {
$locker = $this->prophesize(LockFactory::class); $locker = $this->prophesize(LockFactory::class);
$lock = $this->prophesize(LockInterface::class); $lock = $this->prophesize(LockInterface::class);
$lock->acquire(Argument::any())->willReturn(true); $lock->acquire(Argument::any())->willReturn(true);
$lock->release()->will(function () { $lock->release()->will(function (): void {
}); });
$locker->createLock(Argument::cetera())->willReturn($lock->reveal()); $locker->createLock(Argument::cetera())->willReturn($lock->reveal());
@ -61,7 +55,7 @@ class CreateDatabaseCommandTest extends TestCase
$this->processHelper->reveal(), $this->processHelper->reveal(),
$phpExecutableFinder->reveal(), $phpExecutableFinder->reveal(),
$this->regularConn->reveal(), $this->regularConn->reveal(),
$this->noDbNameConn->reveal() $this->noDbNameConn->reveal(),
); );
$app = new Application(); $app = new Application();
$app->add($command); $app->add($command);
@ -75,7 +69,7 @@ class CreateDatabaseCommandTest extends TestCase
$shlinkDatabase = 'shlink_database'; $shlinkDatabase = 'shlink_database';
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase); $getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', $shlinkDatabase, 'bar']); $listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', $shlinkDatabase, 'bar']);
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () { $createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function (): void {
}); });
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']); $listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);
@ -95,7 +89,7 @@ class CreateDatabaseCommandTest extends TestCase
$shlinkDatabase = 'shlink_database'; $shlinkDatabase = 'shlink_database';
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase); $getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', 'bar']); $listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', 'bar']);
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () { $createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function (): void {
}); });
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']); $listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);
@ -113,7 +107,7 @@ class CreateDatabaseCommandTest extends TestCase
$shlinkDatabase = 'shlink_database'; $shlinkDatabase = 'shlink_database';
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase); $getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', $shlinkDatabase, 'bar']); $listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', $shlinkDatabase, 'bar']);
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () { $createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function (): void {
}); });
$listTables = $this->schemaManager->listTableNames()->willReturn([]); $listTables = $this->schemaManager->listTableNames()->willReturn([]);
$runCommand = $this->processHelper->mustRun(Argument::type(OutputInterface::class), [ $runCommand = $this->processHelper->mustRun(Argument::type(OutputInterface::class), [
@ -142,7 +136,7 @@ class CreateDatabaseCommandTest extends TestCase
$shlinkDatabase = 'shlink_database'; $shlinkDatabase = 'shlink_database';
$getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase); $getDatabase = $this->regularConn->getDatabase()->willReturn($shlinkDatabase);
$listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', 'bar']); $listDatabases = $this->schemaManager->listDatabases()->willReturn(['foo', 'bar']);
$createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function () { $createDatabase = $this->schemaManager->createDatabase($shlinkDatabase)->will(function (): void {
}); });
$listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']); $listTables = $this->schemaManager->listTableNames()->willReturn(['foo_table', 'bar_table']);

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