diff --git a/.github/workflows/ci-db-tests.yml b/.github/workflows/ci-db-tests.yml
index db3efacf..6bf9ad2c 100644
--- a/.github/workflows/ci-db-tests.yml
+++ b/.github/workflows/ci-db-tests.yml
@@ -27,7 +27,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
- php-extensions: openswoole-4.12.0, pdo_sqlsrv-5.10.1
+ php-extensions: openswoole-4.12.1, pdo_sqlsrv-5.10.1
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
- name: Create test database
if: ${{ inputs.platform == 'ms' }}
diff --git a/.github/workflows/ci-docker-image-build.yml b/.github/workflows/ci-docker-image-build.yml
new file mode 100644
index 00000000..3a055f10
--- /dev/null
+++ b/.github/workflows/ci-docker-image-build.yml
@@ -0,0 +1,14 @@
+name: Build docker image
+
+on:
+ pull_request:
+ paths:
+ - 'Dockerfile'
+
+jobs:
+ build-docker-image:
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v3
+ - run: docker build -t shlink-docker-image:temp .
diff --git a/.github/workflows/ci-mutation-tests.yml b/.github/workflows/ci-mutation-tests.yml
index ac510c7d..5d8b1660 100644
--- a/.github/workflows/ci-mutation-tests.yml
+++ b/.github/workflows/ci-mutation-tests.yml
@@ -19,7 +19,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
- php-extensions: openswoole-4.12.0
+ php-extensions: openswoole-4.12.1
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
- uses: actions/download-artifact@v3
with:
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
index f7e7b141..ba9fb991 100644
--- a/.github/workflows/ci-tests.yml
+++ b/.github/workflows/ci-tests.yml
@@ -25,7 +25,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
- php-extensions: openswoole-4.12.0
+ php-extensions: openswoole-4.12.1
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
- run: composer test:${{ inputs.test-group }}:ci
- uses: actions/upload-artifact@v3
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ca34c07d..c5749074 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,12 +1,28 @@
name: Continuous integration
on:
- pull_request: null
+ pull_request:
+ paths-ignore:
+ - 'LICENSE'
+ - '.*'
+ - '*.md'
+ - '*.xml'
+ - '*.yml*'
+ - '*.json5'
+ - '*.neon'
push:
branches:
- main
- develop
- 2.x
+ paths-ignore:
+ - 'LICENSE'
+ - '.*'
+ - '*.md'
+ - '*.xml'
+ - '*.yml*'
+ - '*.json5'
+ - '*.neon'
jobs:
static-analysis:
@@ -20,7 +36,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
- php-extensions: openswoole-4.12.0
+ php-extensions: openswoole-4.12.1
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
- run: composer ${{ matrix.command }}
@@ -44,6 +60,8 @@ jobs:
strategy:
matrix:
php-version: ['8.1', '8.2']
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically
steps:
- uses: actions/checkout@v3
- run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
@@ -157,19 +175,3 @@ jobs:
coverage-db
coverage-api
coverage-cli
-
- build-docker-image:
- runs-on: ubuntu-22.04
- steps:
- - name: Checkout code
- uses: actions/checkout@v3
- with:
- fetch-depth: 100
- - uses: marceloprado/has-changed-path@v1
- id: changed-dockerfile
- with:
- paths: ./Dockerfile
- - if: ${{ steps.changed-dockerfile.outputs.changed == 'true' }}
- run: docker build -t shlink-docker-image:temp .
- - if: ${{ steps.changed-dockerfile.outputs.changed != 'true' }}
- run: echo "Dockerfile didn't change. Skipped"
diff --git a/.github/workflows/docker-image-build.yml b/.github/workflows/publish-docker-image.yml
similarity index 83%
rename from .github/workflows/docker-image-build.yml
rename to .github/workflows/publish-docker-image.yml
index 9eb682d6..7fb52fe1 100644
--- a/.github/workflows/docker-image-build.yml
+++ b/.github/workflows/publish-docker-image.yml
@@ -4,6 +4,14 @@ on:
push:
branches:
- develop
+ paths-ignore:
+ - 'LICENSE'
+ - '.*'
+ - '*.md'
+ - '*.xml'
+ - '*.yml*'
+ - '*.json5'
+ - '*.neon'
tags:
- 'v*'
diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
index 792513be..8d8a4b0d 100644
--- a/.github/workflows/publish-release.yml
+++ b/.github/workflows/publish-release.yml
@@ -17,7 +17,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
- php-extensions: openswoole-4.12.0
+ php-extensions: openswoole-4.12.1
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
install-deps: 'no'
- if: ${{ matrix.swoole == 'yes' }}
diff --git a/.github/workflows/publish-swagger-spec.yml b/.github/workflows/publish-swagger-spec.yml
index 6e6cb925..dd5bfbde 100644
--- a/.github/workflows/publish-swagger-spec.yml
+++ b/.github/workflows/publish-swagger-spec.yml
@@ -20,7 +20,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
- php-extensions: openswoole-4.12.0
+ php-extensions: openswoole-4.12.1
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
- run: composer swagger:inline
- run: mkdir ${{ steps.determine_version.outputs.version }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c3b9b2aa..bd0c8222 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,45 @@ 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).
+## [3.5.0] - 2023-01-28
+### Added
+* [#1557](https://github.com/shlinkio/shlink/issues/1557) Added support to dynamically redirect to different long URLs based on the visitor's device type.
+
+ For the moment, only `android`, `ios` and `desktop` can have their own specific long URL, and when the visitor cannot be matched against any of them, the regular long URL will be used.
+
+ In the future, more granular device types could be added if appropriate (iOS tablet, android table, tablet, mobile phone, Linux, Mac, Windows, etc).
+
+ In order to match the visitor's device, the `User-Agent` header is used.
+
+* [#1632](https://github.com/shlinkio/shlink/issues/1632) Added amount of bots, non-bots and total visits to the visits summary endpoint.
+* [#1633](https://github.com/shlinkio/shlink/issues/1633) Added amount of bots, non-bots and total visits to the tag stats endpoint.
+* [#1653](https://github.com/shlinkio/shlink/issues/1653) Added support for all HTTP methods in short URLs, together with two new redirect status codes, 307 and 308.
+
+ Existing Shlink instances will continue to work the same. However, if you decide to set the redirect status codes as 307 or 308, Shlink will also return a redirect for short URLs even when the request method is different from `GET`.
+
+ The status 308 is equivalent to 301, and 307 is equivalent to 302. The difference is that the spec requires the client to respect the original HTTP method when performing the redirect. With 301 and 302, some old clients might perform a `GET` request during the redirect, regardless the original request method.
+
+* [#1662](https://github.com/shlinkio/shlink/issues/1662) Added support to provide openswoole-specific config options via env vars prefixed with `OPENSWOOLE_`.
+* [#1389](https://github.com/shlinkio/shlink/issues/1389) and [#706](https://github.com/shlinkio/shlink/issues/706) Added support for case-insensitive short URLs.
+
+ In order to achieve this, a new env var/config option has been implemented (`SHORT_URL_MODE`), which allows either `strict` or `loosely`.
+
+ Default value is `strict`, but if `loosely` is provided, then short URLs will be matched in a case-insensitive way, and new short URLs will be generated with short-codes in lowercase only.
+
+### Changed
+* *Nothing*
+
+### Deprecated
+* [#1676](https://github.com/shlinkio/shlink/issues/1676) Deprecated `GET /short-urls/shorten` endpoint. Use `POST /short-urls` to create short URLs instead.
+* [#1678](https://github.com/shlinkio/shlink/issues/1678) Deprecated `validateUrl` option on URL creation/edition.
+
+### Removed
+* *Nothing*
+
+### Fixed
+* [#1639](https://github.com/shlinkio/shlink/issues/1639) Fixed 500 error returned when request body is not valid JSON, instead of a proper descriptive error.
+
+
## [3.4.0] - 2022-12-16
### Added
* [#1612](https://github.com/shlinkio/shlink/issues/1612) Allowed to filter short URLs out of lists, when `validUntil` date is in the past or have reached their maximum amount of visits.
@@ -1428,7 +1467,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
Endpoints and commands which create short URLs support providing the `domain` now (via query param or CLI flag). If not provided, the short URLs will still be "attached" to the default domain.
- Custom slugs can be created on multiple domains, allowing to share links like `https://doma.in/my-campaign` and `https://example.com/my-campaign`, under the same shlink instance.
+ Custom slugs can be created on multiple domains, allowing to share links like `https://s.test/my-campaign` and `https://example.com/my-campaign`, under the same shlink instance.
When resolving a short URL to redirect end users, the following rules are applied:
@@ -1891,7 +1930,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
```json
{
"shortCode": "12Kb3",
- "shortUrl": "https://doma.in/12Kb3",
+ "shortUrl": "https://s.test/12Kb3",
"longUrl": "https://shlink.io",
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsCount": 1029,
@@ -1958,7 +1997,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
* [#174](https://github.com/shlinkio/shlink/issues/174) Fixed geolocation not working due to a deprecation on used service.
* [#172](https://github.com/shlinkio/shlink/issues/172) Documented missing filtering params for `[GET] /short-codes/{shortCode}/visits` API endpoint, which allow the list to be filtered by date range.
- For example: `https://doma.in/rest/v1/short-urls/abc123/visits?startDate=2017-05-23&endDate=2017-10-05`
+ For example: `https://s.test/rest/v1/short-urls/abc123/visits?startDate=2017-05-23&endDate=2017-10-05`
* [#169](https://github.com/shlinkio/shlink/issues/169) Fixed unhandled error when parsing `ShortUrlMeta` and date fields are already `DateTime` instances.
@@ -2030,7 +2069,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
This eases integration with third party services.
- With this feature, a simple request to a URL like `https://doma.in/rest/v1/short-codes/shorten?apiKey=[YOUR_API_KEY]&longUrl=[URL_TO_BE_SHORTENED]` would return the shortened one in JSON or plain text format.
+ With this feature, a simple request to a URL like `https://s.test/rest/v1/short-codes/shorten?apiKey=[YOUR_API_KEY]&longUrl=[URL_TO_BE_SHORTENED]` would return the shortened one in JSON or plain text format.
### Changed
* *Nothing*
@@ -2066,7 +2105,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
### Added
* [#125](https://github.com/shlinkio/shlink/issues/125) Implemented a path which returns a 1px image instead of a redirection.
- Useful to track emails. Just add an image pointing to a URL like `https://doma.in/abc123/track` to any email and an invisible image will be generated tracking every time the email is opened.
+ Useful to track emails. Just add an image pointing to a URL like `https://s.test/abc123/track` to any email and an invisible image will be generated tracking every time the email is opened.
* [#132](https://github.com/shlinkio/shlink/issues/132) Added infection to improve tests
@@ -2347,7 +2386,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
### Added
* [#46](https://github.com/shlinkio/shlink/issues/46) Defined a route that returns a QR code representing the shortened URL.
- In order to get the QR code URL, use a pattern like `https://doma.in/abc123/qr-code`
+ In order to get the QR code URL, use a pattern like `https://s.test/abc123/qr-code`
* [#32](https://github.com/shlinkio/shlink/issues/32) Added support for other cache adapters by improving the Cache factory
* [#14](https://github.com/shlinkio/shlink/issues/14) Added logger and enabled errors logging
diff --git a/Dockerfile b/Dockerfile
index 8c38653b..935c3d44 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@ ARG SHLINK_VERSION=latest
ENV SHLINK_VERSION ${SHLINK_VERSION}
ARG SHLINK_RUNTIME=openswoole
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
-ENV OPENSWOOLE_VERSION 4.12.0
+ENV OPENSWOOLE_VERSION 4.12.1
ENV PDO_SQLSRV_VERSION 5.10.1
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
diff --git a/LICENSE b/LICENSE
index 2a381d83..c245a4e0 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License (MIT)
-Copyright (c) 2016-2021 Alejandro Celaya
+Copyright (c) 2016-2023 Alejandro Celaya
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index c6dfa953..e721d8a1 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-[](https://github.com/shlinkio/shlink/actions?query=workflow%3A%22Continuous+integration%22)
+[](https://github.com/shlinkio/shlink/actions/workflows/ci.yml?query=workflow%3A%22Continuous+integration%22)
[](https://app.codecov.io/gh/shlinkio/shlink)
[](https://dashboard.stryker-mutator.io/reports/github.com/shlinkio/shlink/develop)
[](https://packagist.org/packages/shlinkio/shlink)
@@ -36,7 +36,7 @@ The idea is that you can just generate a container using the image and provide t
First, make sure the host where you are going to run shlink fulfills these requirements:
-* PHP 8.1
+* PHP 8.1 or 8.2
* The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath.
* apcu extension is recommended if you don't plan to use openswoole.
* xml extension is required if you want to generate QR codes in svg format.
diff --git a/composer.json b/composer.json
index ddf41fa5..ef47eced 100644
--- a/composer.json
+++ b/composer.json
@@ -40,16 +40,17 @@
"mezzio/mezzio-problem-details": "^1.7",
"mezzio/mezzio-swoole": "^4.5",
"mlocati/ip-lib": "^1.18",
+ "mobiledetect/mobiledetectlib": "^3.74",
"ocramius/proxy-manager": "^2.14",
"pagerfanta/core": "^3.6",
"php-middleware/request-id": "^4.1",
"pugx/shortid-php": "^1.1",
"ramsey/uuid": "^4.5",
- "shlinkio/shlink-common": "^5.2",
- "shlinkio/shlink-config": "^2.3",
+ "shlinkio/shlink-common": "^5.3",
+ "shlinkio/shlink-config": "^2.4",
"shlinkio/shlink-event-dispatcher": "^2.6",
"shlinkio/shlink-importer": "^5.0",
- "shlinkio/shlink-installer": "^8.2",
+ "shlinkio/shlink-installer": "^8.3",
"shlinkio/shlink-ip-geolocation": "^3.2",
"spiral/roadrunner": "^2.11",
"spiral/roadrunner-jobs": "^2.5",
@@ -73,7 +74,7 @@
"phpunit/phpunit": "^9.5",
"roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.3.0",
- "shlinkio/shlink-test-utils": "^3.3",
+ "shlinkio/shlink-test-utils": "^3.4",
"symfony/var-dumper": "^6.1",
"veewee/composer-run-parallel": "^1.1"
},
@@ -96,7 +97,8 @@
"ShlinkioApiTest\\Shlink\\Rest\\": "module/Rest/test-api",
"ShlinkioDbTest\\Shlink\\Rest\\": "module/Rest/test-db",
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
- "ShlinkioDbTest\\Shlink\\Core\\": "module/Core/test-db"
+ "ShlinkioDbTest\\Shlink\\Core\\": "module/Core/test-db",
+ "ShlinkioApiTest\\Shlink\\Core\\": "module/Core/test-api"
},
"files": [
"config/test/constants.php"
diff --git a/config/autoload/installer.global.php b/config/autoload/installer.global.php
index fbc5fa03..029a50d6 100644
--- a/config/autoload/installer.global.php
+++ b/config/autoload/installer.global.php
@@ -45,6 +45,7 @@ return [
Option\UrlShortener\AppendExtraPathConfigOption::class,
Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class,
Option\UrlShortener\EnableTrailingSlashConfigOption::class,
+ Option\UrlShortener\ShortUrlModeConfigOption::class,
Option\Tracking\IpAnonymizationConfigOption::class,
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
Option\Tracking\DisableTrackParamConfigOption::class,
diff --git a/config/autoload/redirects.global.php b/config/autoload/redirects.global.php
index 426bb2ac..26a3c032 100644
--- a/config/autoload/redirects.global.php
+++ b/config/autoload/redirects.global.php
@@ -16,7 +16,7 @@ return [
],
'redirects' => [
- 'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE->loadFromEnv(DEFAULT_REDIRECT_STATUS_CODE),
+ 'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE->loadFromEnv(DEFAULT_REDIRECT_STATUS_CODE->value),
'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME->loadFromEnv(
DEFAULT_REDIRECT_CACHE_LIFETIME,
),
diff --git a/config/autoload/swoole.global.php b/config/autoload/swoole.global.php
index 36cba24f..494e3cf2 100644
--- a/config/autoload/swoole.global.php
+++ b/config/autoload/swoole.global.php
@@ -4,6 +4,8 @@ declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
+use function Shlinkio\Shlink\Config\getOpenswooleConfigFromEnv;
+
use const Shlinkio\Shlink\MIN_TASK_WORKERS;
return (static function (): array {
@@ -21,6 +23,7 @@ return (static function (): array {
'process-name' => 'shlink',
'options' => [
+ ...getOpenswooleConfigFromEnv(),
'worker_num' => (int) EnvVars::WEB_WORKER_NUM->loadFromEnv(16),
'task_worker_num' => max($taskWorkers, MIN_TASK_WORKERS),
],
diff --git a/config/autoload/url-shortener.global.php b/config/autoload/url-shortener.global.php
index ec3c1409..2816577d 100644
--- a/config/autoload/url-shortener.global.php
+++ b/config/autoload/url-shortener.global.php
@@ -3,6 +3,7 @@
declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
+use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
@@ -12,6 +13,8 @@ return (static function (): array {
(int) EnvVars::DEFAULT_SHORT_CODES_LENGTH->loadFromEnv(DEFAULT_SHORT_CODES_LENGTH),
MIN_SHORT_CODES_LENGTH,
);
+ $modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
+ $mode = ShortUrlMode::tryFrom($modeFromEnv) ?? ShortUrlMode::STRICT;
return [
@@ -25,6 +28,7 @@ return (static function (): array {
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false),
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false),
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false),
+ 'mode' => $mode,
],
];
diff --git a/config/config.php b/config/config.php
index 15a45348..e0ec6c23 100644
--- a/config/config.php
+++ b/config/config.php
@@ -15,6 +15,7 @@ use function class_exists;
use function Shlinkio\Shlink\Config\env;
use function Shlinkio\Shlink\Config\openswooleIsInstalled;
use function Shlinkio\Shlink\Config\runningInRoadRunner;
+use function Shlinkio\Shlink\Core\enumValues;
use const PHP_SAPI;
@@ -23,7 +24,7 @@ $enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoad
return (new ConfigAggregator\ConfigAggregator([
! $isTestEnv
- ? new EnvVarLoaderProvider('config/params/generated_config.php', Core\Config\EnvVars::values())
+ ? new EnvVarLoaderProvider('config/params/generated_config.php', enumValues(Core\Config\EnvVars::class))
: new ConfigAggregator\ArrayProvider([]),
Mezzio\ConfigProvider::class,
Mezzio\Router\ConfigProvider::class,
@@ -48,6 +49,7 @@ return (new ConfigAggregator\ConfigAggregator([
// Routes have to be loaded last
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
], 'data/cache/app_config.php', [
- Core\Config\BasePathPrefixer::class,
- Core\Config\MultiSegmentSlugProcessor::class,
+ Core\Config\PostProcessor\BasePathPrefixer::class,
+ Core\Config\PostProcessor\MultiSegmentSlugProcessor::class,
+ Core\Config\PostProcessor\ShortUrlMethodsProcessor::class,
]))->getMergedConfig();
diff --git a/config/constants.php b/config/constants.php
index d3d869c3..5c891a34 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -4,12 +4,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink;
-use Fig\Http\Message\StatusCodeInterface;
+use Shlinkio\Shlink\Core\Util\RedirectStatus;
const DEFAULT_DELETE_SHORT_URL_THRESHOLD = 15;
const DEFAULT_SHORT_CODES_LENGTH = 5;
const MIN_SHORT_CODES_LENGTH = 4;
-const DEFAULT_REDIRECT_STATUS_CODE = StatusCodeInterface::STATUS_FOUND;
+const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302; // Deprecated. Default to 307 for Shlink v4
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
const TITLE_TAG_VALUE = '/
]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
diff --git a/config/test/constants.php b/config/test/constants.php
index c767abc9..bce232f3 100644
--- a/config/test/constants.php
+++ b/config/test/constants.php
@@ -6,3 +6,10 @@ namespace ShlinkioTest\Shlink;
const API_TESTS_HOST = '127.0.0.1';
const API_TESTS_PORT = 9999;
+
+const ANDROID_USER_AGENT = 'Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) '
+ . 'Chrome/109.0.5414.86 Mobile Safari/537.36';
+const IOS_USER_AGENT = 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_2 like Mac OS X) AppleWebKit/605.1.15 '
+ . '(KHTML, like Gecko) FxiOS/109.0 Mobile/15E148 Safari/605.1.15';
+const DESKTOP_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like '
+ . 'Gecko) Chrome/109.0.0.0 Safari/537.36 Edg/109.0.1518.61';
diff --git a/config/test/test_config.global.php b/config/test/test_config.global.php
index 368a5f4e..ac62f8a6 100644
--- a/config/test/test_config.global.php
+++ b/config/test/test_config.global.php
@@ -84,7 +84,7 @@ $buildDbConnection = static function (): array {
return match ($driver) {
'sqlite' => [
'driver' => 'pdo_sqlite',
- 'path' => sys_get_temp_dir() . '/shlink-tests.db',
+ 'memory' => true,
],
'postgres' => [
'driver' => 'pdo_pgsql',
@@ -131,7 +131,7 @@ return [
'url_shortener' => [
'domain' => [
'schema' => 'http',
- 'hostname' => 'doma.in',
+ 'hostname' => 's.test',
],
],
diff --git a/data/infra/examples/apache-vhost.conf b/data/infra/examples/apache-vhost.conf
index fbb7a18a..872001a3 100644
--- a/data/infra/examples/apache-vhost.conf
+++ b/data/infra/examples/apache-vhost.conf
@@ -1,5 +1,5 @@
- ServerName doma.in
+ ServerName s.test
DocumentRoot "/path/to/shlink/public"
diff --git a/data/infra/examples/nginx-vhost.conf b/data/infra/examples/nginx-vhost.conf
index 5e05481a..6cd4dd4e 100644
--- a/data/infra/examples/nginx-vhost.conf
+++ b/data/infra/examples/nginx-vhost.conf
@@ -1,5 +1,5 @@
server {
- server_name doma.in;
+ server_name s.test;
listen 80;
root /path/to/shlink/public;
index index.php;
diff --git a/data/infra/swoole.Dockerfile b/data/infra/swoole.Dockerfile
index 21e7d95f..6cab2561 100644
--- a/data/infra/swoole.Dockerfile
+++ b/data/infra/swoole.Dockerfile
@@ -3,7 +3,7 @@ MAINTAINER Alejandro Celaya
ENV APCU_VERSION 5.1.21
ENV INOTIFY_VERSION 3.0.0
-ENV OPENSWOOLE_VERSION 4.12.0
+ENV OPENSWOOLE_VERSION 4.12.1
ENV PDO_SQLSRV_VERSION 5.10.1
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
diff --git a/data/migrations/Version20230103105343.php b/data/migrations/Version20230103105343.php
new file mode 100644
index 00000000..c61a8a94
--- /dev/null
+++ b/data/migrations/Version20230103105343.php
@@ -0,0 +1,53 @@
+skipIf($schema->hasTable(self::TABLE_NAME));
+
+ $table = $schema->createTable(self::TABLE_NAME);
+ $table->addColumn('id', Types::BIGINT, [
+ 'unsigned' => true,
+ 'autoincrement' => true,
+ 'notnull' => true,
+ ]);
+ $table->setPrimaryKey(['id']);
+
+ $table->addColumn('device_type', Types::STRING, ['length' => 255]);
+ $table->addColumn('long_url', Types::STRING, ['length' => 2048]);
+ $table->addColumn('short_url_id', Types::BIGINT, [
+ 'unsigned' => true,
+ 'notnull' => true,
+ ]);
+
+ $table->addForeignKeyConstraint('short_urls', ['short_url_id'], ['id'], [
+ 'onDelete' => 'CASCADE',
+ 'onUpdate' => 'RESTRICT',
+ ]);
+
+ $table->addUniqueIndex(['device_type', 'short_url_id'], 'UQ_device_type_per_short_url');
+ }
+
+ public function down(Schema $schema): void
+ {
+ $this->skipIf(! $schema->hasTable(self::TABLE_NAME));
+ $schema->dropTable(self::TABLE_NAME);
+ }
+
+ public function isTransactional(): bool
+ {
+ return ! ($this->connection->getDatabasePlatform() instanceof MySQLPlatform);
+ }
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index f3affecb..ca0064b4 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -102,7 +102,7 @@ services:
shlink_db_mysql:
container_name: shlink_db_mysql
- image: mysql:5.7
+ image: mysql:8.0
ports:
- "3307:3306"
volumes:
@@ -175,7 +175,7 @@ services:
shlink_mercure:
container_name: shlink_mercure
- image: dunglas/mercure:v0.13
+ image: dunglas/mercure:v0.14
ports:
- "3080:80"
environment:
diff --git a/docker/README.md b/docker/README.md
index c1279b2d..629a9ee1 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -11,7 +11,7 @@ It exposes a shlink instance served with [openswoole](https://openswoole.com/),
The most basic way to run Shlink's docker image is by providing these mandatory env vars.
-* `DEFAULT_DOMAIN`: The default short domain used for this shlink instance. For example **doma.in**.
+* `DEFAULT_DOMAIN`: The default short domain used for this shlink instance. For example **s.test**.
* `IS_HTTPS_ENABLED`: Either **true** or **false**. Tells if Shlink is being served with HTTPs or not.
* `GEOLITE_LICENSE_KEY`: Your GeoLite2 license key. [Learn more](https://shlink.io/documentation/geolite-license-key/) about this.
@@ -21,7 +21,7 @@ To run shlink on top of a local docker service, and using an internal SQLite dat
docker run \
--name shlink \
-p 8080:8080 \
- -e DEFAULT_DOMAIN=doma.in \
+ -e DEFAULT_DOMAIN=s.test \
-e IS_HTTPS_ENABLED=true \
-e GEOLITE_LICENSE_KEY=kjh23ljkbndskj345 \
shlinkio/shlink:stable
diff --git a/docs/adr/2023-01-06-support-any-http-method-in-short-urls.md b/docs/adr/2023-01-06-support-any-http-method-in-short-urls.md
new file mode 100644
index 00000000..d81ba9d7
--- /dev/null
+++ b/docs/adr/2023-01-06-support-any-http-method-in-short-urls.md
@@ -0,0 +1,77 @@
+# Support any HTTP method in short URLs
+
+* Status: Accepted
+* Date: 2023-01-06
+
+## Context and problem statement
+
+There has been a report that Shlink behaves as if a short URL was not found when the request HTTP method is not `GET`.
+
+They want it to accept other methods so that they can do things like POSTing stuff that then gets "redirected" to the original URL.
+
+This presents two main problems:
+
+* Changing this could be considered a breaking change, in case someone is relying on this behavior (Shlink to only redirect on `GET`).
+* Shlink currently supports two redirect statuses ([301](https://httpwg.org/specs/rfc9110.html#status.301) and [302](https://httpwg.org/specs/rfc9110.html#status.302)), which can be configured by the server admin.
+
+ For historical reasons, a client might switch from the original method to `GET` when any of these is returned, not resulting in the desired behavior anyway.
+
+ Instead, statuses [308](https://httpwg.org/specs/rfc9110.html#status.308) and [307](https://httpwg.org/specs/rfc9110.html#status.307) should be used.
+
+## Considered options
+
+There's actually two problems to solve here. Some combinations are implicitly required:
+
+* **To support other HTTP methods in short URLs**
+ * Start supporting all HTTP methods.
+ * Introduce a feature flag to allow users decide if they want to support all methods or just `GET`.
+* **To support other redirects statuses (308 and 307)**
+ * Switch to status 308 and 307 and stop using 301 and 302.
+ * Allow users to configure which of the 4 status codes they want to use, insteadof just supporting 301 and 302.
+ * Allow users to configure between two combinations: 301+308 and 302+307, using 301 or 302 for `GET` requests, and 308 or 307 for the rest.
+
+> **Note**
+> I asked on social networks, and these were the results (not too many answers though):
+> * https://fosstodon.org/@shlinkio/109626773392324128
+> * https://twitter.com/shlinkio/status/1610347091741507585
+
+## Decision outcome
+
+Because of backwards compatibility, it feels like the bets option is allowing to configure between 301, 302, 308 and 307.
+
+This has the benefit that we can keep existing behavior intact. Existing instances will continue working only on `GET`, with statuses 301 or 302.
+
+Anyone who wants to opt-in, can switch to 308 or 307, and the short URLs will transparently work on other HTTP methods in that case.
+
+The only drawback is that this difference in the behavior when 308 or 307 are configured needs to be documented, and explained in shlink-installer.
+
+## Pros and Cons of the Options
+
+### Start supporting all HTTP methods
+
+* Good: Because the change in code is pretty simple.
+* Bad: Because it would be potentially a breaking change for anyone trusting current behavior for anything.
+
+### Support HTTP methods via feature flag
+
+* Good: because it would be safer for existing instances and opt-in for anyone interested in this change of behavior.
+* Bad: Because it requires more changes in code.
+* Bad: Because it requires a new config entry in the shlink-installer.
+
+### Switch to statuses 308 and 307
+
+* Good: Because we keep supporting just two status codes.
+* Bad: Because it requires applying mapping/transformation to convert old configurations.
+* Bad: Because it requires changes in shlink-installer.
+
+### Allow users to configure between 301, 302, 308 and 307
+
+* Good: Because it's fully backwards compatible with existing configs.
+* Good: Because it would implicitly allow enabling all HTTP methods if 308 or 307 are selected, and keep only `GET` for 301 and 302, without the need for a separated feature flag.
+* Bad: Because it requires dynamically supporting only `GET` or all methods, depending on the selected status.
+
+### Allow users to configure between 301+308 or 302+307
+
+* Good: Because it would allow a more explicit redirects config, where values are not 301 and 302, but something like "permanent" and "temporary".
+* Bad: Because it implicitly changes the behavior of existing instances, making them respond to redirects with a method other than `GET`, and with a status code other than the one they explicitly configured.
+* Bad: because existing `REDIRECT_STATUS_CODE` env var might not make sense anymore, requiring a new one and logic to map from one to another.
diff --git a/docs/adr/README.md b/docs/adr/README.md
index 7cfccdf7..9d87a0fb 100644
--- a/docs/adr/README.md
+++ b/docs/adr/README.md
@@ -2,6 +2,7 @@
Here listed you will find the different architectural decisions taken in the project, including all the reasoning behind it, options considered, and final outcome.
+* [2023-01-06 Support any HTTP method in short URLs](2023-01-06-support-any-http-method-in-short-urls.md)
* [2022-08-05 Support multi-segment custom slugs](2022-08-05-support-multi-segment-custom-slugs.md)
* [2022-01-15 Update env vars behavior to have precedence over installer options](2022-01-15-update-env-vars-behavior-to-have-precedence-over-installer-options.md)
* [2021-08-05 Migrate to a new caching library](2021-08-05-migrate-to-a-new-caching-library.md)
diff --git a/docs/async-api/async-api.json b/docs/async-api/async-api.json
index 3b59e8e5..d45dae2b 100644
--- a/docs/async-api/async-api.json
+++ b/docs/async-api/async-api.json
@@ -111,12 +111,19 @@
"type": "string",
"description": "The original long URL."
},
+ "deviceLongUrls": {
+ "$ref": "#/components/schemas/DeviceLongUrls"
+ },
"dateCreated": {
"type": "string",
"format": "date-time",
"description": "The date in which the short URL was created in ISO format."
},
+ "visitsSummary": {
+ "$ref": "#/components/schemas/VisitsSummary"
+ },
"visitsCount": {
+ "deprecated": true,
"type": "integer",
"description": "The number of visits that this short URL has received."
},
@@ -146,10 +153,19 @@
},
"example": {
"shortCode": "12C18",
- "shortUrl": "https://doma.in/12C18",
+ "shortUrl": "https://s.test/12C18",
"longUrl": "https://store.steampowered.com",
+ "deviceLongUrls": {
+ "android": "https://store.steampowered.com/android",
+ "ios": "https://store.steampowered.com/ios",
+ "desktop": null
+ },
"dateCreated": "2016-08-21T20:34:16+02:00",
- "visitsCount": 328,
+ "visitsSummary": {
+ "total": 328,
+ "nonBots": 285,
+ "bots": 43
+ },
"tags": [
"games",
"tech"
@@ -189,6 +205,42 @@
}
}
},
+ "VisitsSummary": {
+ "type": "object",
+ "required": ["total", "nonBots", "bots"],
+ "properties": {
+ "total": {
+ "description": "The total amount of visits",
+ "type": "number"
+ },
+ "nonBots": {
+ "description": "The amount of visits which were not identified as bots",
+ "type": "number"
+ },
+ "bots": {
+ "description": "The amount of visits that were identified as potential bots",
+ "type": "number"
+ }
+ }
+ },
+ "DeviceLongUrls": {
+ "type": "object",
+ "required": ["android", "ios", "desktop"],
+ "properties": {
+ "android": {
+ "description": "The long URL to redirect to when the short URL is visited from a device running Android",
+ "type": "string"
+ },
+ "ios": {
+ "description": "The long URL to redirect to when the short URL is visited from a device running iOS",
+ "type": "string"
+ },
+ "desktop": {
+ "description": "The long URL to redirect to when the short URL is visited from a desktop browser",
+ "type": "string"
+ }
+ }
+ },
"Visit": {
"type": "object",
"properties": {
@@ -266,7 +318,7 @@
"timezone": "America/Los_Angeles"
},
"potentialBot": false,
- "visitedUrl": "https://doma.in",
+ "visitedUrl": "https://s.test",
"type": "base_url"
}
},
diff --git a/docs/swagger/definitions/DeviceLongUrls.json b/docs/swagger/definitions/DeviceLongUrls.json
new file mode 100644
index 00000000..1a56d9ef
--- /dev/null
+++ b/docs/swagger/definitions/DeviceLongUrls.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "properties": {
+ "android": {
+ "description": "The long URL to redirect to when the short URL is visited from a device running Android",
+ "type": "string",
+ "nullable": false
+ },
+ "ios": {
+ "description": "The long URL to redirect to when the short URL is visited from a device running iOS",
+ "type": "string",
+ "nullable": false
+ },
+ "desktop": {
+ "description": "The long URL to redirect to when the short URL is visited from a desktop browser",
+ "type": "string",
+ "nullable": false
+ }
+ }
+}
diff --git a/docs/swagger/definitions/DeviceLongUrlsEdit.json b/docs/swagger/definitions/DeviceLongUrlsEdit.json
new file mode 100644
index 00000000..78f77e46
--- /dev/null
+++ b/docs/swagger/definitions/DeviceLongUrlsEdit.json
@@ -0,0 +1,17 @@
+{
+ "type": "object",
+ "allOf": [{
+ "$ref": "./DeviceLongUrls.json"
+ }],
+ "properties": {
+ "android": {
+ "nullable": true
+ },
+ "ios": {
+ "nullable": true
+ },
+ "desktop": {
+ "nullable": true
+ }
+ }
+}
diff --git a/docs/swagger/definitions/DeviceLongUrlsResp.json b/docs/swagger/definitions/DeviceLongUrlsResp.json
new file mode 100644
index 00000000..95724581
--- /dev/null
+++ b/docs/swagger/definitions/DeviceLongUrlsResp.json
@@ -0,0 +1,7 @@
+{
+ "type": "object",
+ "required": ["android", "ios", "desktop"],
+ "allOf": [{
+ "$ref": "./DeviceLongUrlsEdit.json"
+ }]
+}
diff --git a/docs/swagger/definitions/ShortUrl.json b/docs/swagger/definitions/ShortUrl.json
index ab66f506..4060e2f2 100644
--- a/docs/swagger/definitions/ShortUrl.json
+++ b/docs/swagger/definitions/ShortUrl.json
@@ -4,6 +4,7 @@
"shortCode",
"shortUrl",
"longUrl",
+ "deviceLongUrls",
"dateCreated",
"visitsCount",
"visitsSummary",
@@ -27,6 +28,9 @@
"type": "string",
"description": "The original long URL."
},
+ "deviceLongUrls": {
+ "$ref": "./DeviceLongUrlsResp.json"
+ },
"dateCreated": {
"type": "string",
"format": "date-time",
@@ -38,7 +42,7 @@
"description": "**[DEPRECATED]** Use `visitsSummary.total` instead."
},
"visitsSummary": {
- "$ref": "./ShortUrlVisitsSummary.json"
+ "$ref": "./VisitsSummary.json"
},
"tags": {
"type": "array",
diff --git a/docs/swagger/definitions/ShortUrlEdition.json b/docs/swagger/definitions/ShortUrlEdition.json
index 94ef6135..ed3c3929 100644
--- a/docs/swagger/definitions/ShortUrlEdition.json
+++ b/docs/swagger/definitions/ShortUrlEdition.json
@@ -5,6 +5,9 @@
"description": "The long URL this short URL will redirect to",
"type": "string"
},
+ "deviceLongUrls": {
+ "$ref": "./DeviceLongUrlsEdit.json"
+ },
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": "string",
@@ -21,7 +24,8 @@
"nullable": true
},
"validateUrl": {
- "description": "Tells if the long URL (if provided) should or should not be validated as a reachable URL. If not provided, it will fall back to app-level config",
+ "deprecated": true,
+ "description": "**[DEPRECATED]** Tells if the long URL should or should not be validated as a reachable URL. Defaults to `false`",
"type": "boolean"
},
"tags": {
diff --git a/docs/swagger/definitions/TagInfo.json b/docs/swagger/definitions/TagInfo.json
index e881ce02..41de1068 100644
--- a/docs/swagger/definitions/TagInfo.json
+++ b/docs/swagger/definitions/TagInfo.json
@@ -1,5 +1,6 @@
{
"type": "object",
+ "required": ["tag", "shortUrlsCount", "visitsSummary", "visitsCount"],
"properties": {
"tag": {
"type": "string",
@@ -9,9 +10,13 @@
"type": "number",
"description": "The amount of short URLs using this tag"
},
- "userAgent": {
+ "visitsSummary": {
+ "$ref": "./VisitsSummary.json"
+ },
+ "visitsCount": {
+ "deprecated": true,
"type": "number",
- "description": "The combined amount of visits received by short URLs with this tag"
+ "description": "**[DEPRECATED]** Use visitsSummary.total instead"
}
}
}
diff --git a/docs/swagger/definitions/VisitStats.json b/docs/swagger/definitions/VisitStats.json
index 2a97f597..2ed24375 100644
--- a/docs/swagger/definitions/VisitStats.json
+++ b/docs/swagger/definitions/VisitStats.json
@@ -1,14 +1,22 @@
{
"type": "object",
- "required": ["visitsCount", "orphanVisitsCount"],
+ "required": ["nonOrphanVisits", "orphanVisits", "visitsCount", "orphanVisitsCount"],
"properties": {
+ "nonOrphanVisits": {
+ "$ref": "./VisitsSummary.json"
+ },
+ "orphanVisits": {
+ "$ref": "./VisitsSummary.json"
+ },
"visitsCount": {
+ "deprecated": true,
"type": "number",
- "description": "The total amount of visits received on any short URL."
+ "description": "**[DEPRECATED]** Use nonOrphanVisits.total instead"
},
"orphanVisitsCount": {
+ "deprecated": true,
"type": "number",
- "description": "The total amount of visits that could not be matched to a short URL (visits to the base URL, an invalid short URL or any other kind of 404)."
+ "description": "**[DEPRECATED]** Use orphanVisits.total instead"
}
}
}
diff --git a/docs/swagger/definitions/ShortUrlVisitsSummary.json b/docs/swagger/definitions/VisitsSummary.json
similarity index 83%
rename from docs/swagger/definitions/ShortUrlVisitsSummary.json
rename to docs/swagger/definitions/VisitsSummary.json
index 404b7a75..c59b2ccd 100644
--- a/docs/swagger/definitions/ShortUrlVisitsSummary.json
+++ b/docs/swagger/definitions/VisitsSummary.json
@@ -3,7 +3,7 @@
"required": ["total", "nonBots", "bots"],
"properties": {
"total": {
- "description": "The total amount of visits that this short URL has received.",
+ "description": "The total amount of visits.",
"type": "integer"
},
"nonBots": {
diff --git a/docs/swagger/paths/v1_short-urls.json b/docs/swagger/paths/v1_short-urls.json
index 8960234a..c226046f 100644
--- a/docs/swagger/paths/v1_short-urls.json
+++ b/docs/swagger/paths/v1_short-urls.json
@@ -161,8 +161,13 @@
"data": [
{
"shortCode": "12C18",
- "shortUrl": "https://doma.in/12C18",
+ "shortUrl": "https://s.test/12C18",
"longUrl": "https://store.steampowered.com",
+ "deviceLongUrls": {
+ "android": null,
+ "ios": null,
+ "desktop": null
+ },
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsSummary": {
"total": 328,
@@ -184,8 +189,13 @@
},
{
"shortCode": "12Kb3",
- "shortUrl": "https://doma.in/12Kb3",
+ "shortUrl": "https://s.test/12Kb3",
"longUrl": "https://shlink.io",
+ "deviceLongUrls": {
+ "android": null,
+ "ios": "https://shlink.io/ios",
+ "desktop": null
+ },
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsSummary": {
"total": 1029,
@@ -208,6 +218,11 @@
"shortCode": "123bA",
"shortUrl": "https://example.com/123bA",
"longUrl": "https://www.google.com",
+ "deviceLongUrls": {
+ "android": null,
+ "ios": null,
+ "desktop": null
+ },
"dateCreated": "2015-10-01T20:34:16+02:00",
"visitsSummary": {
"total": 25,
@@ -281,6 +296,9 @@
"type": "object",
"required": ["longUrl"],
"properties": {
+ "deviceLongUrls": {
+ "$ref": "../definitions/DeviceLongUrls.json"
+ },
"customSlug": {
"description": "A unique custom slug to be used instead of the generated short code",
"type": "string"
@@ -296,10 +314,6 @@
"shortCodeLength": {
"description": "The length for generated short code. It has to be at least 4 and defaults to 5. It will be ignored when customSlug is provided",
"type": "number"
- },
- "validateUrl": {
- "description": "Tells if the long URL should or should not be validated as a reachable URL. If not provided, it will fall back to app-level config",
- "type": "boolean"
}
}
}
@@ -318,8 +332,13 @@
},
"example": {
"shortCode": "12C18",
- "shortUrl": "https://doma.in/12C18",
+ "shortUrl": "https://s.test/12C18",
"longUrl": "https://store.steampowered.com",
+ "deviceLongUrls": {
+ "android": null,
+ "ios": null,
+ "desktop": null
+ },
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsSummary": {
"total": 0,
diff --git a/docs/swagger/paths/v1_short-urls_shorten.json b/docs/swagger/paths/v1_short-urls_shorten.json
index 254a88f2..cacb00bb 100644
--- a/docs/swagger/paths/v1_short-urls_shorten.json
+++ b/docs/swagger/paths/v1_short-urls_shorten.json
@@ -1,11 +1,12 @@
{
"get": {
"operationId": "shortenUrl",
+ "deprecated": true,
"tags": [
"Short URLs"
],
"summary": "Create a short URL",
- "description": "Creates a short URL in a single API call. Useful for third party integrations.",
+ "description": "**[Deprecated]** Use [Create short URL](#/Short%20URLs/createShortUrl) instead",
"parameters": [
{
"$ref": "../parameters/version.json"
@@ -52,7 +53,12 @@
},
"example": {
"longUrl": "https://github.com/shlinkio/shlink",
- "shortUrl": "https://doma.in/abc123",
+ "deviceLongUrls": {
+ "android": null,
+ "ios": null,
+ "desktop": null
+ },
+ "shortUrl": "https://s.test/abc123",
"shortCode": "abc123",
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsSummary": {
@@ -78,7 +84,7 @@
"schema": {
"type": "string"
},
- "example": "https://doma.in/abc123"
+ "example": "https://s.test/abc123"
}
}
},
diff --git a/docs/swagger/paths/v1_short-urls_{shortCode}.json b/docs/swagger/paths/v1_short-urls_{shortCode}.json
index 00577f4f..e639f362 100644
--- a/docs/swagger/paths/v1_short-urls_{shortCode}.json
+++ b/docs/swagger/paths/v1_short-urls_{shortCode}.json
@@ -38,8 +38,13 @@
},
"example": {
"shortCode": "12Kb3",
- "shortUrl": "https://doma.in/12Kb3",
+ "shortUrl": "https://s.test/12Kb3",
"longUrl": "https://shlink.io",
+ "deviceLongUrls": {
+ "android": null,
+ "ios": null,
+ "desktop": null
+ },
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsSummary": {
"total": 1029,
@@ -160,8 +165,13 @@
},
"example": {
"shortCode": "12Kb3",
- "shortUrl": "https://doma.in/12Kb3",
+ "shortUrl": "https://s.test/12Kb3",
"longUrl": "https://shlink.io",
+ "deviceLongUrls": {
+ "android": "https://shlink.io/android",
+ "ios": null,
+ "desktop": null
+ },
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsSummary": {
"total": 1029,
diff --git a/docs/swagger/paths/v2_tags_stats.json b/docs/swagger/paths/v2_tags_stats.json
index 91771335..150cf7b3 100644
--- a/docs/swagger/paths/v2_tags_stats.json
+++ b/docs/swagger/paths/v2_tags_stats.json
@@ -45,7 +45,7 @@
{
"name": "orderBy",
"in": "query",
- "description": "To determine how to order the results.
**Important!** Ordering by `shortUrlsCount` or `visitsCount` has a [known performance issue](https://github.com/shlinkio/shlink/issues/1346) which makes loading a subset of the list take as much as loading the whole list.
If you plan to order by any of these fields, it's worth loading the whole list with no pagination.",
+ "description": "To determine how to order the results.
**Important!** Ordering by `shortUrlsCount`, `visits` or `nonBotVisits` has a [known performance issue](https://github.com/shlinkio/shlink/issues/1346) which makes loading a subset of the list take as much as loading the whole list.
If you plan to order by any of these fields, it's worth loading the whole list with no pagination.",
"required": false,
"schema": {
"type": "string",
@@ -54,8 +54,10 @@
"tag-DESC",
"shortUrlsCount-ASC",
"shortUrlsCount-DESC",
- "visitsCount-ASC",
- "visitsCount-DESC"
+ "visits-ASC",
+ "visits-DESC",
+ "nonBotVisits-ASC",
+ "nonBotVisits-DESC"
]
}
}
@@ -73,7 +75,6 @@
"required": ["data"],
"properties": {
"data": {
- "description": "The tag stats will be returned only if the withStats param was provided with value 'true'",
"type": "array",
"items": {
"$ref": "../definitions/TagInfo.json"
@@ -92,12 +93,20 @@
{
"tag": "games",
"shortUrlsCount": 10,
- "visitsCount": 521
+ "visitsSummary": {
+ "total": 521,
+ "nonBots": 521,
+ "bots": 0
+ }
},
{
"tag": "shlink",
"shortUrlsCount": 7,
- "visitsCount": 1087
+ "visitsSummary": {
+ "total": 1087,
+ "nonBots": 1000,
+ "bots": 87
+ }
}
],
"pagination": {
diff --git a/docs/swagger/paths/v2_visits.json b/docs/swagger/paths/v2_visits.json
index ded6ac6b..3db0ef67 100644
--- a/docs/swagger/paths/v2_visits.json
+++ b/docs/swagger/paths/v2_visits.json
@@ -31,8 +31,16 @@
},
"example": {
"visits": {
- "visitsCount": 1569874,
- "orphanVisitsCount": 71345
+ "nonOrphanVisits": {
+ "total": 64994,
+ "nonBots": 64986,
+ "bots": 8
+ },
+ "orphanVisits": {
+ "total": 37,
+ "nonBots": 34,
+ "bots": 3
+ }
}
}
}
diff --git a/docs/swagger/paths/v2_visits_orphan.json b/docs/swagger/paths/v2_visits_orphan.json
index 03d56553..b10ac37f 100644
--- a/docs/swagger/paths/v2_visits_orphan.json
+++ b/docs/swagger/paths/v2_visits_orphan.json
@@ -95,7 +95,7 @@
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
"visitLocation": null,
"potentialBot": false,
- "visitedUrl": "https://doma.in",
+ "visitedUrl": "https://s.test",
"type": "base_url"
},
{
@@ -112,7 +112,7 @@
"timezone": "America/Los_Angeles"
},
"potentialBot": false,
- "visitedUrl": "https://doma.in/foo",
+ "visitedUrl": "https://s.test/foo",
"type": "invalid_short_url"
},
{
@@ -121,7 +121,7 @@
"userAgent": "some_web_crawler/1.4",
"visitLocation": null,
"potentialBot": true,
- "visitedUrl": "https://doma.in/foo/bar/baz",
+ "visitedUrl": "https://s.test/foo/bar/baz",
"type": "regular_404"
}
],
diff --git a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php
index f4cfc58a..6fb1001b 100644
--- a/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php
+++ b/module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php
@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
-use Shlinkio\Shlink\Core\Config\EnvVars;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
@@ -103,7 +102,7 @@ class CreateShortUrlCommand extends Command
'validate-url',
null,
InputOption::VALUE_NONE,
- 'Forces the long URL to be validated, regardless what is globally configured.',
+ '[DEPRECATED] Makes the URL to be validated as publicly accessible.',
)
->addOption(
'crawlable',
@@ -175,8 +174,7 @@ class CreateShortUrlCommand extends Command
ShortUrlInputFilter::TAGS => $tags,
ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'),
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
- EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $this->options->multiSegmentSlugsEnabled,
- ]));
+ ], $this->options));
$io->writeln([
sprintf('Processed long URL: %s', $longUrl),
diff --git a/module/CLI/src/Command/Tag/ListTagsCommand.php b/module/CLI/src/Command/Tag/ListTagsCommand.php
index cd820169..02116d79 100644
--- a/module/CLI/src/Command/Tag/ListTagsCommand.php
+++ b/module/CLI/src/Command/Tag/ListTagsCommand.php
@@ -46,7 +46,7 @@ class ListTagsCommand extends Command
return map(
$tags,
- static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsCount],
+ static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsSummary->total],
);
}
}
diff --git a/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php b/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php
index cbb8affd..ceb5cbfd 100644
--- a/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php
+++ b/module/CLI/src/Exception/GeolocationDbUpdateFailedException.php
@@ -15,7 +15,7 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
private function __construct(string $message, ?Throwable $previous = null)
{
- parent::__construct($message, 0, $previous);
+ parent::__construct($message, previous: $previous);
}
public static function withOlderDb(?Throwable $prev = null): self
diff --git a/module/CLI/test-cli/Command/ListShortUrlsTest.php b/module/CLI/test-cli/Command/ListShortUrlsTest.php
index c98573a5..8b92d2f0 100644
--- a/module/CLI/test-cli/Command/ListShortUrlsTest.php
+++ b/module/CLI/test-cli/Command/ListShortUrlsTest.php
@@ -27,11 +27,11 @@ class ListShortUrlsTest extends CliTestCase
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+---------------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
- | custom | | http://doma.in/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
- | def456 | | http://doma.in/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
+ | custom | | http://s.test/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
+ | def456 | | http://s.test/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
- | abc123 | My cool title | http://doma.in/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
- | ghi789 | | http://doma.in/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+ | abc123 | My cool title | http://s.test/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
+ | ghi789 | | http://s.test/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+---------------+-------------------------------------------+---------------------------- Page 1 of 1 ------------------------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'start date' => [['--start-date=2019-01'], <<