mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-25 18:45:27 -06:00
commit
3266a0f85c
46
.github/workflows/ci.yml
vendored
46
.github/workflows/ci.yml
vendored
@ -10,10 +10,10 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
static-analysis:
|
static-analysis:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.0']
|
php-version: ['8.1']
|
||||||
command: ['cs', 'stan', 'swagger:validate']
|
command: ['cs', 'stan', 'swagger:validate']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -25,14 +25,15 @@ jobs:
|
|||||||
tools: composer
|
tools: composer
|
||||||
extensions: openswoole-4.11.1
|
extensions: openswoole-4.11.1
|
||||||
coverage: none
|
coverage: none
|
||||||
- run: composer install --no-interaction --prefer-dist
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
- run: composer ${{ matrix.command }}
|
- run: composer ${{ matrix.command }}
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.0', '8.1']
|
php-version: ['8.1']
|
||||||
test-group: ['unit', 'api']
|
test-group: ['unit', 'api']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -48,10 +49,11 @@ jobs:
|
|||||||
extensions: openswoole-4.11.1
|
extensions: openswoole-4.11.1
|
||||||
coverage: pcov
|
coverage: pcov
|
||||||
ini-values: pcov.directory=module
|
ini-values: pcov.directory=module
|
||||||
- run: composer install --no-interaction --prefer-dist
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
- run: composer test:${{ matrix.test-group }}:ci
|
- run: composer test:${{ matrix.test-group }}:ci
|
||||||
- uses: actions/upload-artifact@v2
|
- uses: actions/upload-artifact@v2
|
||||||
if: ${{ matrix.php-version == '8.0' }}
|
if: ${{ matrix.php-version == '8.1' }}
|
||||||
with:
|
with:
|
||||||
name: coverage-${{ matrix.test-group }}
|
name: coverage-${{ matrix.test-group }}
|
||||||
path: |
|
path: |
|
||||||
@ -59,10 +61,10 @@ jobs:
|
|||||||
build/coverage-${{ matrix.test-group }}.cov
|
build/coverage-${{ matrix.test-group }}.cov
|
||||||
|
|
||||||
db-tests:
|
db-tests:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.0', '8.1']
|
php-version: ['8.1']
|
||||||
platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms']
|
platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms']
|
||||||
env:
|
env:
|
||||||
LC_ALL: C
|
LC_ALL: C
|
||||||
@ -80,10 +82,11 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
tools: composer
|
tools: composer
|
||||||
extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.0
|
extensions: openswoole-4.11.1, pdo_sqlsrv-5.10.1
|
||||||
coverage: pcov
|
coverage: pcov
|
||||||
ini-values: pcov.directory=module
|
ini-values: pcov.directory=module
|
||||||
- run: composer install --no-interaction --prefer-dist
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
- name: Create test database
|
- name: Create test database
|
||||||
if: ${{ matrix.platform == 'ms' }}
|
if: ${{ matrix.platform == 'ms' }}
|
||||||
run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
|
run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
|
||||||
@ -91,7 +94,7 @@ jobs:
|
|||||||
run: composer test:db:${{ matrix.platform }}
|
run: composer test:db:${{ matrix.platform }}
|
||||||
- name: Upload code coverage
|
- name: Upload code coverage
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
if: ${{ matrix.php-version == '8.0' && matrix.platform == 'sqlite:ci' }}
|
if: ${{ matrix.php-version == '8.1' && matrix.platform == 'sqlite:ci' }}
|
||||||
with:
|
with:
|
||||||
name: coverage-db
|
name: coverage-db
|
||||||
path: |
|
path: |
|
||||||
@ -102,10 +105,10 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- tests
|
- tests
|
||||||
- db-tests
|
- db-tests
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.0', '8.1']
|
php-version: ['8.1']
|
||||||
test-group: ['unit', 'db', 'api']
|
test-group: ['unit', 'db', 'api']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -118,7 +121,8 @@ jobs:
|
|||||||
extensions: openswoole-4.11.1
|
extensions: openswoole-4.11.1
|
||||||
coverage: pcov
|
coverage: pcov
|
||||||
ini-values: pcov.directory=module
|
ini-values: pcov.directory=module
|
||||||
- run: composer install --no-interaction --prefer-dist
|
- name: Install dependencies
|
||||||
|
run: composer install --no-interaction --prefer-dist
|
||||||
- uses: actions/download-artifact@v2
|
- uses: actions/download-artifact@v2
|
||||||
with:
|
with:
|
||||||
path: build
|
path: build
|
||||||
@ -133,10 +137,10 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- tests
|
- tests
|
||||||
- db-tests
|
- db-tests
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.0']
|
php-version: ['8.1']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@ -152,8 +156,8 @@ jobs:
|
|||||||
- run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov
|
- run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov
|
||||||
- run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov
|
- run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov
|
||||||
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
|
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
|
||||||
- run: wget https://phar.phpunit.de/phpcov-8.2.0.phar
|
- run: wget https://phar.phpunit.de/phpcov-8.2.1.phar
|
||||||
- run: php phpcov-8.2.0.phar merge build --clover build/clover.xml
|
- run: php phpcov-8.2.1.phar merge build --clover build/clover.xml
|
||||||
- name: Publish coverage
|
- name: Publish coverage
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v1
|
||||||
with:
|
with:
|
||||||
@ -163,7 +167,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- mutation-tests
|
- mutation-tests
|
||||||
- upload-coverage
|
- upload-coverage
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- uses: geekyeggo/delete-artifact@v1
|
- uses: geekyeggo/delete-artifact@v1
|
||||||
with:
|
with:
|
||||||
@ -173,7 +177,7 @@ jobs:
|
|||||||
coverage-api
|
coverage-api
|
||||||
|
|
||||||
build-docker-image:
|
build-docker-image:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
2
.github/workflows/docker-image-build.yml
vendored
2
.github/workflows/docker-image-build.yml
vendored
@ -9,7 +9,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
12
.github/workflows/publish-release.yml
vendored
12
.github/workflows/publish-release.yml
vendored
@ -7,10 +7,10 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.0', '8.1']
|
php-version: ['8.1']
|
||||||
swoole: ['yes', 'no']
|
swoole: ['yes', 'no']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
publish:
|
publish:
|
||||||
needs: ['build']
|
needs: ['build']
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
@ -50,11 +50,11 @@ jobs:
|
|||||||
|
|
||||||
delete-artifacts:
|
delete-artifacts:
|
||||||
needs: ['publish']
|
needs: ['publish']
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: [ '8.0', '8.1' ]
|
php-version: ['8.1']
|
||||||
swoole: [ 'yes', 'no' ]
|
swoole: ['yes', 'no']
|
||||||
steps:
|
steps:
|
||||||
- uses: geekyeggo/delete-artifact@v1
|
- uses: geekyeggo/delete-artifact@v1
|
||||||
with:
|
with:
|
||||||
|
4
.github/workflows/publish-swagger-spec.yml
vendored
4
.github/workflows/publish-swagger-spec.yml
vendored
@ -7,10 +7,10 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php-version: ['8.0']
|
php-version: ['8.1']
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
44
CHANGELOG.md
44
CHANGELOG.md
@ -4,6 +4,42 @@ 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).
|
||||||
|
|
||||||
|
## [3.2.0] - 2022-08-05
|
||||||
|
### Added
|
||||||
|
* [#854](https://github.com/shlinkio/shlink/issues/854) Added support for multi-segment custom slugs.
|
||||||
|
|
||||||
|
The feature is disabled by default, but you can optionally opt in. If you do, you will be able to create short URLs with multiple segments in the custom slug, like `https://example.com/foo/bar/baz`.
|
||||||
|
|
||||||
|
* [#1280](https://github.com/shlinkio/shlink/issues/1280) Added missing visit-related commands.
|
||||||
|
|
||||||
|
Now you can run `tag:visits`, `domain:visits`, `visit:orphan` or `visit:non-orphan` to get the corresponding list of visits from the command line.
|
||||||
|
|
||||||
|
* [#962](https://github.com/shlinkio/shlink/issues/962) Added new real-time update for new short URLs.
|
||||||
|
|
||||||
|
You can now subscribe to the `https://shlink.io/new-short-url` topic on any of the supported async updates technologies in order to get notified when a short URL is created.
|
||||||
|
|
||||||
|
* [#1367](https://github.com/shlinkio/shlink/issues/1367) Added support to publish real-time updates in redis pub/sub.
|
||||||
|
|
||||||
|
The publishing will happen in the same redis instance/cluster configured for caching.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* [#1452](https://github.com/shlinkio/shlink/issues/1452) Updated to monolog 3
|
||||||
|
* [#1485](https://github.com/shlinkio/shlink/issues/1485) Changed payload published in RabbitMQ for all visits events, in order to conform with the Async API spec.
|
||||||
|
|
||||||
|
Since this is a breaking change, also provided a new `RABBITMQ_LEGACY_VISITS_PUBLISHING=true` env var that can be provided in order to keep the old payload.
|
||||||
|
|
||||||
|
This env var is considered deprecated and will be removed in Shlink 4, when the legacy format will no longer be supported.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* [#1280](https://github.com/shlinkio/shlink/issues/1280) Dropped support for PHP 8.0
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1471](https://github.com/shlinkio/shlink/issues/1471) Fixed error when running `visit:locate` command with any extra parameter (like `--retry`).
|
||||||
|
|
||||||
|
|
||||||
## [3.1.2] - 2022-06-04
|
## [3.1.2] - 2022-06-04
|
||||||
### Added
|
### Added
|
||||||
* *Nothing*
|
* *Nothing*
|
||||||
@ -605,7 +641,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||||||
* *Nothing*
|
* *Nothing*
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* [#979](https://github.com/shlinkio/shlink/issues/979) Added missing `itemsPerPage` query param to swagger docs for short RULs list.
|
* [#979](https://github.com/shlinkio/shlink/issues/979) Added missing `itemsPerPage` query param to swagger docs for short URLs list.
|
||||||
* [#980](https://github.com/shlinkio/shlink/issues/980) Fixed value used for `Access-Control-Allow-Origin`, that could not work as expected when including an IP address.
|
* [#980](https://github.com/shlinkio/shlink/issues/980) Fixed value used for `Access-Control-Allow-Origin`, that could not work as expected when including an IP address.
|
||||||
* [#947](https://github.com/shlinkio/shlink/issues/947) Fixed incorrect value returned in `Access-Control-Allow-Methods` header, which always contained all methods.
|
* [#947](https://github.com/shlinkio/shlink/issues/947) Fixed incorrect value returned in `Access-Control-Allow-Methods` header, which always contained all methods.
|
||||||
|
|
||||||
@ -1253,7 +1289,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.
|
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-compaign` 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://doma.in/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:
|
When resolving a short URL to redirect end users, the following rules are applied:
|
||||||
|
|
||||||
@ -1503,7 +1539,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* [#309](https://github.com/shlinkio/shlink/issues/309) Added missing favicon to prevent 404 errors logged when an error page is loaded in a browser.
|
* [#309](https://github.com/shlinkio/shlink/issues/309) Added missing favicon to prevent 404 errors logged when an error page is loaded in a browser.
|
||||||
* [#310](https://github.com/shlinkio/shlink/issues/310) Fixed execution context not being properly detected, making `CloseDbConnectionMiddlware` to be always piped. Now the check is not even made, which simplifies everything.
|
* [#310](https://github.com/shlinkio/shlink/issues/310) Fixed execution context not being properly detected, making `CloseDbConnectionMiddleware` to be always piped. Now the check is not even made, which simplifies everything.
|
||||||
|
|
||||||
|
|
||||||
## [1.15.0] - 2018-12-02
|
## [1.15.0] - 2018-12-02
|
||||||
@ -1568,7 +1604,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* [#241](https://github.com/shlinkio/shlink/issues/241) Fixed columns in `visit_locations` table, to be snake_case instead of camelCase.
|
* [#241](https://github.com/shlinkio/shlink/issues/241) Fixed columns in `visit_locations` table, to be snake_case instead of camelCase.
|
||||||
* [#228](https://github.com/shlinkio/shlink/issues/228) Updated how exceptions are serialized into logs, by using monlog's `PsrLogMessageProcessor`.
|
* [#228](https://github.com/shlinkio/shlink/issues/228) Updated how exceptions are serialized into logs, by using monolog's `PsrLogMessageProcessor`.
|
||||||
* [#225](https://github.com/shlinkio/shlink/issues/225) Performance and maintainability slightly improved by enforcing via code sniffer that all global namespace classes, functions and constants are explicitly imported.
|
* [#225](https://github.com/shlinkio/shlink/issues/225) Performance and maintainability slightly improved by enforcing via code sniffer that all global namespace classes, functions and constants are explicitly imported.
|
||||||
* [#196](https://github.com/shlinkio/shlink/issues/196) Reduced anemic model in entities, defining more expressive public APIs instead.
|
* [#196](https://github.com/shlinkio/shlink/issues/196) Reduced anemic model in entities, defining more expressive public APIs instead.
|
||||||
* [#249](https://github.com/shlinkio/shlink/issues/249) Added [functional-php](https://github.com/lstrojny/functional-php) to ease collections handling.
|
* [#249](https://github.com/shlinkio/shlink/issues/249) Added [functional-php](https://github.com/lstrojny/functional-php) to ease collections handling.
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
FROM php:8.1.5-alpine3.15 as base
|
FROM php:8.1.9-alpine3.16 as base
|
||||||
|
|
||||||
ARG SHLINK_VERSION=latest
|
ARG SHLINK_VERSION=latest
|
||||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||||
ENV OPENSWOOLE_VERSION 4.11.1
|
ENV OPENSWOOLE_VERSION 4.11.1
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.0
|
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||||
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
||||||
ENV LC_ALL "C"
|
ENV LC_ALL "C"
|
||||||
|
|
||||||
|
@ -35,7 +35,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:
|
First, make sure the host where you are going to run shlink fulfills these requirements:
|
||||||
|
|
||||||
* PHP 8.0 or 8.1
|
* PHP 8.1
|
||||||
* The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath.
|
* The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath.
|
||||||
* apcu extension is recommended if you don't plan to use openswoole.
|
* 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.
|
* xml extension is required if you want to generate QR codes in svg format.
|
||||||
@ -66,7 +66,9 @@ In order to run Shlink, you will need a built version of the project. There are
|
|||||||
|
|
||||||
After that, you will have a dist file inside the `build` directory, that you need to decompress in the location of your choice.
|
After that, you will have a dist file inside the `build` directory, that you need to decompress in the location of your choice.
|
||||||
|
|
||||||
> This is the process used when releasing new shlink versions. After tagging the new version with git, the Github release is automatically created by a [GitHub workflow](https://github.com/shlinkio/shlink/actions?query=workflow%3A%22Publish+release%22), attaching the generated dist file to it.
|
> **Note**
|
||||||
|
>
|
||||||
|
> This is the process used when releasing new Shlink versions. After tagging the new version with git, the GitHub release is automatically created by a [GitHub workflow](https://github.com/shlinkio/shlink/actions?query=workflow%3A%22Publish+release%22), attaching the generated dist file to it.
|
||||||
|
|
||||||
### Configure
|
### Configure
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ These routes have been removed, but have a direct replacement:
|
|||||||
* `/qr/{shortCode}[/{size}]` -> `/{shortCode}/qr-code[/{size}]`
|
* `/qr/{shortCode}[/{size}]` -> `/{shortCode}/qr-code[/{size}]`
|
||||||
* `PUT /rest/v{version}/short-urls/{shortCode}` -> `PATCH /rest/v{version}/short-urls/{shortCode}`
|
* `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.
|
When using the old ones, a 404 status will be returned now.
|
||||||
|
|
||||||
### Removed command and route aliases
|
### Removed command and route aliases
|
||||||
|
|
||||||
|
@ -12,53 +12,48 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.0",
|
"php": "^8.1",
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"akrabat/ip-address-middleware": "^2.1",
|
"akrabat/ip-address-middleware": "^2.1",
|
||||||
"cakephp/chronos": "^2.3",
|
"cakephp/chronos": "^2.3",
|
||||||
"doctrine/migrations": "^3.3",
|
"doctrine/migrations": "^3.5",
|
||||||
"doctrine/orm": "^2.11",
|
"doctrine/orm": "^2.12",
|
||||||
"endroid/qr-code": "^4.4",
|
"endroid/qr-code": "^4.4",
|
||||||
"geoip2/geoip2": "^2.12",
|
"geoip2/geoip2": "^2.12",
|
||||||
"guzzlehttp/guzzle": "^7.4",
|
"guzzlehttp/guzzle": "^7.4",
|
||||||
"happyr/doctrine-specification": "^2.0",
|
"happyr/doctrine-specification": "^2.0",
|
||||||
"jaybizzle/crawler-detect": "^1.2.110",
|
"jaybizzle/crawler-detect": "^1.2.110",
|
||||||
"laminas/laminas-config": "^3.7",
|
"laminas/laminas-config": "^3.7",
|
||||||
"laminas/laminas-config-aggregator": "^1.7",
|
"laminas/laminas-config-aggregator": "^1.8",
|
||||||
"laminas/laminas-diactoros": "^2.8",
|
"laminas/laminas-diactoros": "^2.14",
|
||||||
"laminas/laminas-inputfilter": "^2.13",
|
"laminas/laminas-inputfilter": "^2.19",
|
||||||
"laminas/laminas-servicemanager": "^3.11.2",
|
"laminas/laminas-servicemanager": "^3.16",
|
||||||
"laminas/laminas-stdlib": "^3.6",
|
"laminas/laminas-stdlib": "^3.11",
|
||||||
"lcobucci/jwt": "^4.1",
|
"lcobucci/jwt": "^4.1",
|
||||||
"league/uri": "^6.4",
|
"league/uri": "^6.7",
|
||||||
"lstrojny/functional-php": "^1.17",
|
"lstrojny/functional-php": "^1.17",
|
||||||
"mezzio/mezzio": "^3.7",
|
"mezzio/mezzio": "^3.11",
|
||||||
"mezzio/mezzio-fastroute": "^3.3",
|
"mezzio/mezzio-fastroute": "^3.5",
|
||||||
"mezzio/mezzio-problem-details": "^1.5",
|
"mezzio/mezzio-problem-details": "^1.6",
|
||||||
"mezzio/mezzio-swoole": "^4.0",
|
"mezzio/mezzio-swoole": "^4.3",
|
||||||
"mlocati/ip-lib": "^1.17",
|
"mlocati/ip-lib": "^1.18",
|
||||||
"monolog/monolog": "^2.3",
|
"ocramius/proxy-manager": "^2.14",
|
||||||
"nikolaposa/monolog-factory": "^3.1",
|
"pagerfanta/core": "^3.6",
|
||||||
"ocramius/proxy-manager": "^2.11",
|
|
||||||
"pagerfanta/core": "^3.5",
|
|
||||||
"php-amqplib/php-amqplib": "^3.1",
|
|
||||||
"php-middleware/request-id": "^4.1",
|
"php-middleware/request-id": "^4.1",
|
||||||
"predis/predis": "^1.1",
|
|
||||||
"pugx/shortid-php": "^1.0",
|
"pugx/shortid-php": "^1.0",
|
||||||
"ramsey/uuid": "^4.2",
|
"ramsey/uuid": "^4.3",
|
||||||
"shlinkio/shlink-common": "^4.4",
|
"shlinkio/shlink-common": "^4.5",
|
||||||
"shlinkio/shlink-config": "^1.6",
|
"shlinkio/shlink-config": "^1.6",
|
||||||
"shlinkio/shlink-event-dispatcher": "^2.3",
|
"shlinkio/shlink-event-dispatcher": "^2.4",
|
||||||
"shlinkio/shlink-importer": "^3.0",
|
"shlinkio/shlink-importer": "^3.0",
|
||||||
"shlinkio/shlink-installer": "^7.1",
|
"shlinkio/shlink-installer": "^8.0",
|
||||||
"shlinkio/shlink-ip-geolocation": "^2.2",
|
"shlinkio/shlink-ip-geolocation": "^2.2",
|
||||||
"symfony/console": "^6.0",
|
"symfony/console": "^6.1",
|
||||||
"symfony/filesystem": "^6.0",
|
"symfony/filesystem": "^6.1",
|
||||||
"symfony/lock": "^6.0",
|
"symfony/lock": "^6.1",
|
||||||
"symfony/mercure": "^0.6",
|
"symfony/process": "^6.1",
|
||||||
"symfony/process": "^6.0",
|
"symfony/string": "^6.1"
|
||||||
"symfony/string": "^6.0"
|
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"cebe/php-openapi": "^1.7",
|
"cebe/php-openapi": "^1.7",
|
||||||
@ -67,15 +62,15 @@
|
|||||||
"infection/infection": "^0.26.5",
|
"infection/infection": "^0.26.5",
|
||||||
"openswoole/ide-helper": "~4.11.1",
|
"openswoole/ide-helper": "~4.11.1",
|
||||||
"phpspec/prophecy-phpunit": "^2.0",
|
"phpspec/prophecy-phpunit": "^2.0",
|
||||||
"phpstan/phpstan": "^1.2",
|
"phpstan/phpstan": "^1.8",
|
||||||
"phpstan/phpstan-doctrine": "^1.0",
|
"phpstan/phpstan-doctrine": "^1.3",
|
||||||
"phpstan/phpstan-symfony": "^1.0",
|
"phpstan/phpstan-symfony": "^1.2",
|
||||||
"phpunit/php-code-coverage": "^9.2",
|
"phpunit/php-code-coverage": "^9.2",
|
||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "^9.5",
|
||||||
"roave/security-advisories": "dev-master",
|
"roave/security-advisories": "dev-master",
|
||||||
"shlinkio/php-coding-standard": "~2.2.0",
|
"shlinkio/php-coding-standard": "~2.3.0",
|
||||||
"shlinkio/shlink-test-utils": "^3.0.1",
|
"shlinkio/shlink-test-utils": "^3.0.1",
|
||||||
"symfony/var-dumper": "^6.0",
|
"symfony/var-dumper": "^6.1",
|
||||||
"veewee/composer-run-parallel": "^1.1"
|
"veewee/composer-run-parallel": "^1.1"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@ -176,7 +171,7 @@
|
|||||||
"test:db:mysql": "<fg=blue;options=bold>Runs database test suites on a MySQL database</>",
|
"test:db:mysql": "<fg=blue;options=bold>Runs database test suites on a MySQL database</>",
|
||||||
"test:db:maria": "<fg=blue;options=bold>Runs database test suites on a MariaDB database</>",
|
"test:db:maria": "<fg=blue;options=bold>Runs database test suites on a MariaDB database</>",
|
||||||
"test:db:postgres": "<fg=blue;options=bold>Runs database test suites on a PostgreSQL database</>",
|
"test:db:postgres": "<fg=blue;options=bold>Runs database test suites on a PostgreSQL database</>",
|
||||||
"test:db:ms": "<fg=blue;options=bold>Runs database test suites on a Miscrosoft SQL Server database</>",
|
"test:db:ms": "<fg=blue;options=bold>Runs database test suites on a Microsoft SQL Server database</>",
|
||||||
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
|
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
|
||||||
"test:api:ci": "<fg=blue;options=bold>Runs API test suites, and generates code coverage reports</>",
|
"test:api:ci": "<fg=blue;options=bold>Runs API test suites, and generates code coverage reports</>",
|
||||||
"infect:ci": "<fg=blue;options=bold>Checks unit and db tests quality applying mutation testing with existing reports and logs</>",
|
"infect:ci": "<fg=blue;options=bold>Checks unit and db tests quality applying mutation testing with existing reports and logs</>",
|
||||||
|
@ -7,7 +7,7 @@ namespace Shlinkio\Shlink;
|
|||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
|
|
||||||
return (static function (): array {
|
return (static function (): array {
|
||||||
$threshold = EnvVars::DELETE_SHORT_URL_THRESHOLD()->loadFromEnv();
|
$threshold = EnvVars::DELETE_SHORT_URL_THRESHOLD->loadFromEnv();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
|||||||
use function Functional\contains;
|
use function Functional\contains;
|
||||||
|
|
||||||
return (static function (): array {
|
return (static function (): array {
|
||||||
$driver = EnvVars::DB_DRIVER()->loadFromEnv();
|
$driver = EnvVars::DB_DRIVER->loadFromEnv();
|
||||||
$isMysqlCompatible = contains(['maria', 'mysql'], $driver);
|
$isMysqlCompatible = contains(['maria', 'mysql'], $driver);
|
||||||
|
|
||||||
$resolveDriver = static fn () => match ($driver) {
|
$resolveDriver = static fn () => match ($driver) {
|
||||||
@ -35,12 +35,12 @@ return (static function (): array {
|
|||||||
],
|
],
|
||||||
default => [
|
default => [
|
||||||
'driver' => $resolveDriver(),
|
'driver' => $resolveDriver(),
|
||||||
'dbname' => EnvVars::DB_NAME()->loadFromEnv('shlink'),
|
'dbname' => EnvVars::DB_NAME->loadFromEnv('shlink'),
|
||||||
'user' => EnvVars::DB_USER()->loadFromEnv(),
|
'user' => EnvVars::DB_USER->loadFromEnv(),
|
||||||
'password' => EnvVars::DB_PASSWORD()->loadFromEnv(),
|
'password' => EnvVars::DB_PASSWORD->loadFromEnv(),
|
||||||
'host' => EnvVars::DB_HOST()->loadFromEnv(EnvVars::DB_UNIX_SOCKET()->loadFromEnv()),
|
'host' => EnvVars::DB_HOST->loadFromEnv(EnvVars::DB_UNIX_SOCKET->loadFromEnv()),
|
||||||
'port' => EnvVars::DB_PORT()->loadFromEnv($resolveDefaultPort()),
|
'port' => EnvVars::DB_PORT->loadFromEnv($resolveDefaultPort()),
|
||||||
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET()->loadFromEnv() : null,
|
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null,
|
||||||
'charset' => $resolveCharset(),
|
'charset' => $resolveCharset(),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
@ -9,7 +9,7 @@ return [
|
|||||||
'geolite2' => [
|
'geolite2' => [
|
||||||
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
|
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
|
||||||
'temp_dir' => __DIR__ . '/../../data',
|
'temp_dir' => __DIR__ . '/../../data',
|
||||||
'license_key' => EnvVars::GEOLITE_LICENSE_KEY()->loadFromEnv(),
|
'license_key' => EnvVars::GEOLITE_LICENSE_KEY->loadFromEnv(),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -32,6 +32,7 @@ return [
|
|||||||
Option\Worker\WebWorkerNumConfigOption::class,
|
Option\Worker\WebWorkerNumConfigOption::class,
|
||||||
Option\Redis\RedisServersConfigOption::class,
|
Option\Redis\RedisServersConfigOption::class,
|
||||||
Option\Redis\RedisSentinelServiceConfigOption::class,
|
Option\Redis\RedisSentinelServiceConfigOption::class,
|
||||||
|
Option\Redis\RedisPubSubConfigOption::class,
|
||||||
Option\UrlShortener\ShortCodeLengthOption::class,
|
Option\UrlShortener\ShortCodeLengthOption::class,
|
||||||
Option\Mercure\EnableMercureConfigOption::class,
|
Option\Mercure\EnableMercureConfigOption::class,
|
||||||
Option\Mercure\MercurePublicUrlConfigOption::class,
|
Option\Mercure\MercurePublicUrlConfigOption::class,
|
||||||
@ -42,6 +43,7 @@ return [
|
|||||||
Option\UrlShortener\RedirectCacheLifeTimeConfigOption::class,
|
Option\UrlShortener\RedirectCacheLifeTimeConfigOption::class,
|
||||||
Option\UrlShortener\AutoResolveTitlesConfigOption::class,
|
Option\UrlShortener\AutoResolveTitlesConfigOption::class,
|
||||||
Option\UrlShortener\AppendExtraPathConfigOption::class,
|
Option\UrlShortener\AppendExtraPathConfigOption::class,
|
||||||
|
Option\UrlShortener\EnableMultiSegmentSlugsConfigOption::class,
|
||||||
Option\Tracking\IpAnonymizationConfigOption::class,
|
Option\Tracking\IpAnonymizationConfigOption::class,
|
||||||
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
Option\Tracking\OrphanVisitsTrackingConfigOption::class,
|
||||||
Option\Tracking\DisableTrackParamConfigOption::class,
|
Option\Tracking\DisableTrackParamConfigOption::class,
|
||||||
@ -64,13 +66,13 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
'installation_commands' => [
|
'installation_commands' => [
|
||||||
InstallationCommand::DB_CREATE_SCHEMA => [
|
InstallationCommand::DB_CREATE_SCHEMA->value => [
|
||||||
'command' => 'bin/cli ' . Command\Db\CreateDatabaseCommand::NAME,
|
'command' => 'bin/cli ' . Command\Db\CreateDatabaseCommand::NAME,
|
||||||
],
|
],
|
||||||
InstallationCommand::DB_MIGRATE => [
|
InstallationCommand::DB_MIGRATE->value => [
|
||||||
'command' => 'bin/cli ' . Command\Db\MigrateDatabaseCommand::NAME,
|
'command' => 'bin/cli ' . Command\Db\MigrateDatabaseCommand::NAME,
|
||||||
],
|
],
|
||||||
InstallationCommand::GEOLITE_DOWNLOAD_DB => [
|
InstallationCommand::GEOLITE_DOWNLOAD_DB->value => [
|
||||||
'command' => 'bin/cli ' . Command\Visit\DownloadGeoLiteDbCommand::NAME,
|
'command' => 'bin/cli ' . Command\Visit\DownloadGeoLiteDbCommand::NAME,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
use Predis\ClientInterface as PredisClient;
|
use Shlinkio\Shlink\Common\Cache\RedisFactory;
|
||||||
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
|
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
|
||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
use Symfony\Component\Lock;
|
use Symfony\Component\Lock;
|
||||||
@ -24,7 +24,7 @@ return [
|
|||||||
LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class,
|
LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class,
|
||||||
],
|
],
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
'lock_store' => EnvVars::REDIS_SERVERS()->existsInEnv() ? 'redis_lock_store' : 'local_lock_store',
|
'lock_store' => EnvVars::REDIS_SERVERS->existsInEnv() ? 'redis_lock_store' : 'local_lock_store',
|
||||||
|
|
||||||
'redis_lock_store' => Lock\Store\RedisStore::class,
|
'redis_lock_store' => Lock\Store\RedisStore::class,
|
||||||
'local_lock_store' => Lock\Store\FlockStore::class,
|
'local_lock_store' => Lock\Store\FlockStore::class,
|
||||||
@ -38,7 +38,7 @@ return [
|
|||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
Lock\Store\FlockStore::class => ['config.locks.locks_dir'],
|
Lock\Store\FlockStore::class => ['config.locks.locks_dir'],
|
||||||
Lock\Store\RedisStore::class => [PredisClient::class],
|
Lock\Store\RedisStore::class => [RedisFactory::SERVICE_NAME],
|
||||||
Lock\LockFactory::class => ['lock_store'],
|
Lock\LockFactory::class => ['lock_store'],
|
||||||
LOCAL_LOCK_FACTORY => ['local_lock_store'],
|
LOCAL_LOCK_FACTORY => ['local_lock_store'],
|
||||||
],
|
],
|
||||||
|
@ -4,72 +4,36 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink;
|
namespace Shlinkio\Shlink;
|
||||||
|
|
||||||
use Monolog\Formatter;
|
use Monolog\Level;
|
||||||
use Monolog\Handler;
|
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use Monolog\Processor;
|
|
||||||
use MonologFactory\DiContainerLoggerFactory;
|
|
||||||
use PhpMiddleware\RequestId;
|
use PhpMiddleware\RequestId;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
||||||
|
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||||
|
|
||||||
use const PHP_EOL;
|
$common = [
|
||||||
|
'level' => Level::Info->value,
|
||||||
$processors = [
|
'processors' => [RequestId\MonologProcessor::class],
|
||||||
'exception_with_new_line' => [
|
'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%',
|
||||||
'name' => Common\Logger\Processor\ExceptionWithNewLineProcessor::class,
|
|
||||||
],
|
|
||||||
'psr3' => [
|
|
||||||
'name' => Processor\PsrLogMessageProcessor::class,
|
|
||||||
],
|
|
||||||
'request_id' => RequestId\MonologProcessor::class,
|
|
||||||
];
|
|
||||||
$formatter = [
|
|
||||||
'name' => Formatter\LineFormatter::class,
|
|
||||||
'params' => [
|
|
||||||
'format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%' . PHP_EOL,
|
|
||||||
'allow_inline_line_breaks' => true,
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'logger' => [
|
'logger' => [
|
||||||
'Shlink' => [
|
'Shlink' => [
|
||||||
'name' => 'Shlink',
|
'type' => LoggerType::FILE->value,
|
||||||
'handlers' => [
|
...$common,
|
||||||
'shlink_handler' => [
|
|
||||||
'name' => Handler\RotatingFileHandler::class,
|
|
||||||
'params' => [
|
|
||||||
'level' => Logger::INFO,
|
|
||||||
'filename' => 'data/log/shlink_log.log',
|
|
||||||
'max_files' => 30,
|
|
||||||
'file_permission' => 0666,
|
|
||||||
],
|
|
||||||
'formatter' => $formatter,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'processors' => $processors,
|
|
||||||
],
|
],
|
||||||
'Access' => [
|
'Access' => [
|
||||||
'name' => 'Access',
|
'type' => LoggerType::STREAM->value,
|
||||||
'handlers' => [
|
...$common,
|
||||||
'access_handler' => [
|
|
||||||
'name' => Handler\StreamHandler::class,
|
|
||||||
'params' => [
|
|
||||||
'level' => Logger::INFO,
|
|
||||||
'stream' => 'php://stdout',
|
|
||||||
],
|
|
||||||
'formatter' => $formatter,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'processors' => $processors,
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
'Logger_Shlink' => [DiContainerLoggerFactory::class, 'Shlink'],
|
'Logger_Shlink' => [LoggerFactory::class, 'Shlink'],
|
||||||
'Logger_Access' => [DiContainerLoggerFactory::class, 'Access'],
|
'Logger_Access' => [LoggerFactory::class, 'Access'],
|
||||||
],
|
],
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
'logger' => 'Logger_Shlink',
|
'logger' => 'Logger_Shlink',
|
||||||
|
@ -2,33 +2,18 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Monolog\Handler\StreamHandler;
|
use Monolog\Level;
|
||||||
use Monolog\Logger;
|
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||||
|
|
||||||
$isSwoole = extension_loaded('openswoole');
|
$isSwoole = extension_loaded('openswoole');
|
||||||
|
|
||||||
// For swoole, send logs to standard output
|
|
||||||
$handler = $isSwoole
|
|
||||||
? [
|
|
||||||
'name' => StreamHandler::class,
|
|
||||||
'params' => [
|
|
||||||
'level' => Logger::DEBUG,
|
|
||||||
'stream' => 'php://stdout',
|
|
||||||
],
|
|
||||||
]
|
|
||||||
: [
|
|
||||||
'params' => [
|
|
||||||
'level' => Logger::DEBUG,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'logger' => [
|
'logger' => [
|
||||||
'Shlink' => [
|
'Shlink' => [
|
||||||
'handlers' => [
|
// For swoole, send logs as stream
|
||||||
'shlink_handler' => $handler,
|
'type' => $isSwoole ? LoggerType::STREAM->value : LoggerType::FILE->value,
|
||||||
],
|
'level' => Level::Debug->value,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -9,14 +9,14 @@ use Symfony\Component\Mercure\Hub;
|
|||||||
use Symfony\Component\Mercure\HubInterface;
|
use Symfony\Component\Mercure\HubInterface;
|
||||||
|
|
||||||
return (static function (): array {
|
return (static function (): array {
|
||||||
$publicUrl = EnvVars::MERCURE_PUBLIC_HUB_URL()->loadFromEnv();
|
$publicUrl = EnvVars::MERCURE_PUBLIC_HUB_URL->loadFromEnv();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'mercure' => [
|
'mercure' => [
|
||||||
'public_hub_url' => $publicUrl,
|
'public_hub_url' => $publicUrl,
|
||||||
'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL()->loadFromEnv($publicUrl),
|
'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL->loadFromEnv($publicUrl),
|
||||||
'jwt_secret' => EnvVars::MERCURE_JWT_SECRET()->loadFromEnv(),
|
'jwt_secret' => EnvVars::MERCURE_JWT_SECRET->loadFromEnv(),
|
||||||
'jwt_issuer' => 'Shlink',
|
'jwt_issuer' => 'Shlink',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -13,13 +13,13 @@ use const Shlinkio\Shlink\DEFAULT_QR_CODE_SIZE;
|
|||||||
return [
|
return [
|
||||||
|
|
||||||
'qr_codes' => [
|
'qr_codes' => [
|
||||||
'size' => (int) EnvVars::DEFAULT_QR_CODE_SIZE()->loadFromEnv(DEFAULT_QR_CODE_SIZE),
|
'size' => (int) EnvVars::DEFAULT_QR_CODE_SIZE->loadFromEnv(DEFAULT_QR_CODE_SIZE),
|
||||||
'margin' => (int) EnvVars::DEFAULT_QR_CODE_MARGIN()->loadFromEnv(DEFAULT_QR_CODE_MARGIN),
|
'margin' => (int) EnvVars::DEFAULT_QR_CODE_MARGIN->loadFromEnv(DEFAULT_QR_CODE_MARGIN),
|
||||||
'format' => EnvVars::DEFAULT_QR_CODE_FORMAT()->loadFromEnv(DEFAULT_QR_CODE_FORMAT),
|
'format' => EnvVars::DEFAULT_QR_CODE_FORMAT->loadFromEnv(DEFAULT_QR_CODE_FORMAT),
|
||||||
'error_correction' => EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION()->loadFromEnv(
|
'error_correction' => EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION->loadFromEnv(
|
||||||
DEFAULT_QR_CODE_ERROR_CORRECTION,
|
DEFAULT_QR_CODE_ERROR_CORRECTION,
|
||||||
),
|
),
|
||||||
'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE()->loadFromEnv(
|
'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv(
|
||||||
DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
|
DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -2,46 +2,20 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
|
||||||
use Laminas\ServiceManager\Proxy\LazyServiceFactory;
|
|
||||||
use PhpAmqpLib\Connection\AMQPStreamConnection;
|
|
||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'rabbitmq' => [
|
'rabbitmq' => [
|
||||||
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED()->loadFromEnv(false),
|
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(false),
|
||||||
'host' => EnvVars::RABBITMQ_HOST()->loadFromEnv(),
|
'host' => EnvVars::RABBITMQ_HOST->loadFromEnv(),
|
||||||
'port' => (int) EnvVars::RABBITMQ_PORT()->loadFromEnv('5672'),
|
'port' => (int) EnvVars::RABBITMQ_PORT->loadFromEnv('5672'),
|
||||||
'user' => EnvVars::RABBITMQ_USER()->loadFromEnv(),
|
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
|
||||||
'password' => EnvVars::RABBITMQ_PASSWORD()->loadFromEnv(),
|
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
|
||||||
'vhost' => EnvVars::RABBITMQ_VHOST()->loadFromEnv('/'),
|
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv('/'),
|
||||||
],
|
|
||||||
|
|
||||||
'dependencies' => [
|
// Deprecated
|
||||||
'factories' => [
|
'legacy_visits_publishing' => (bool) EnvVars::RABBITMQ_LEGACY_VISITS_PUBLISHING->loadFromEnv(false),
|
||||||
AMQPStreamConnection::class => ConfigAbstractFactory::class,
|
|
||||||
],
|
|
||||||
'delegators' => [
|
|
||||||
AMQPStreamConnection::class => [
|
|
||||||
LazyServiceFactory::class,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
'lazy_services' => [
|
|
||||||
'class_map' => [
|
|
||||||
AMQPStreamConnection::class => AMQPStreamConnection::class,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
|
||||||
AMQPStreamConnection::class => [
|
|
||||||
'config.rabbitmq.host',
|
|
||||||
'config.rabbitmq.port',
|
|
||||||
'config.rabbitmq.user',
|
|
||||||
'config.rabbitmq.password',
|
|
||||||
'config.rabbitmq.vhost',
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -10,14 +10,14 @@ use const Shlinkio\Shlink\DEFAULT_REDIRECT_STATUS_CODE;
|
|||||||
return [
|
return [
|
||||||
|
|
||||||
'not_found_redirects' => [
|
'not_found_redirects' => [
|
||||||
'invalid_short_url' => EnvVars::DEFAULT_INVALID_SHORT_URL_REDIRECT()->loadFromEnv(),
|
'invalid_short_url' => EnvVars::DEFAULT_INVALID_SHORT_URL_REDIRECT->loadFromEnv(),
|
||||||
'regular_404' => EnvVars::DEFAULT_REGULAR_404_REDIRECT()->loadFromEnv(),
|
'regular_404' => EnvVars::DEFAULT_REGULAR_404_REDIRECT->loadFromEnv(),
|
||||||
'base_url' => EnvVars::DEFAULT_BASE_URL_REDIRECT()->loadFromEnv(),
|
'base_url' => EnvVars::DEFAULT_BASE_URL_REDIRECT->loadFromEnv(),
|
||||||
],
|
],
|
||||||
|
|
||||||
'redirects' => [
|
'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),
|
||||||
'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME()->loadFromEnv(
|
'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME->loadFromEnv(
|
||||||
DEFAULT_REDIRECT_CACHE_LIFETIME,
|
DEFAULT_REDIRECT_CACHE_LIFETIME,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -5,17 +5,23 @@ declare(strict_types=1);
|
|||||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
|
|
||||||
return (static function (): array {
|
return (static function (): array {
|
||||||
$redisServers = EnvVars::REDIS_SERVERS()->loadFromEnv();
|
$redisServers = EnvVars::REDIS_SERVERS->loadFromEnv();
|
||||||
|
$pubSub = [
|
||||||
|
'redis' => [
|
||||||
|
'pub_sub_enabled' => $redisServers !== null && EnvVars::REDIS_PUB_SUB_ENABLED->loadFromEnv(false),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
return match ($redisServers) {
|
return match ($redisServers) {
|
||||||
null => [],
|
null => $pubSub,
|
||||||
default => [
|
default => [
|
||||||
'cache' => [
|
'cache' => [
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'servers' => $redisServers,
|
'servers' => $redisServers,
|
||||||
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE()->loadFromEnv(),
|
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
...$pubSub,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
@ -7,12 +7,13 @@ return [
|
|||||||
'cache' => [
|
'cache' => [
|
||||||
'redis' => [
|
'redis' => [
|
||||||
'servers' => 'tcp://shlink_redis:6379',
|
'servers' => 'tcp://shlink_redis:6379',
|
||||||
// 'servers' => [
|
|
||||||
// 'tcp://shlink_redis:6379',
|
|
||||||
// ],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'redis' => [
|
||||||
|
'pub_sub_enabled' => true,
|
||||||
|
],
|
||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
// With this config, a user could alias 'lock_store' => 'redis_lock_store' to override the default
|
// With this config, a user could alias 'lock_store' => 'redis_lock_store' to override the default
|
||||||
|
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
use PhpMiddleware\RequestId;
|
use PhpMiddleware\RequestId;
|
||||||
|
use Shlinkio\Shlink\Common\Logger\Processor\BackwardsCompatibleMonologProcessorDelegator;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@ -20,6 +21,11 @@ return [
|
|||||||
RequestId\RequestIdMiddleware::class => ConfigAbstractFactory::class,
|
RequestId\RequestIdMiddleware::class => ConfigAbstractFactory::class,
|
||||||
RequestId\MonologProcessor::class => ConfigAbstractFactory::class,
|
RequestId\MonologProcessor::class => ConfigAbstractFactory::class,
|
||||||
],
|
],
|
||||||
|
'delegators' => [
|
||||||
|
RequestId\MonologProcessor::class => [
|
||||||
|
BackwardsCompatibleMonologProcessorDelegator::class,
|
||||||
|
],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
ConfigAbstractFactory::class => [
|
ConfigAbstractFactory::class => [
|
||||||
|
@ -8,7 +8,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
|||||||
return [
|
return [
|
||||||
|
|
||||||
'router' => [
|
'router' => [
|
||||||
'base_path' => EnvVars::BASE_PATH()->loadFromEnv(''),
|
'base_path' => EnvVars::BASE_PATH->loadFromEnv(''),
|
||||||
|
|
||||||
'fastroute' => [
|
'fastroute' => [
|
||||||
FastRouteRouter::CONFIG_CACHE_ENABLED => true,
|
FastRouteRouter::CONFIG_CACHE_ENABLED => true,
|
||||||
|
107
config/autoload/routes.config.php
Normal file
107
config/autoload/routes.config.php
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink;
|
||||||
|
|
||||||
|
use Fig\Http\Message\RequestMethodInterface;
|
||||||
|
use RKA\Middleware\IpAddress;
|
||||||
|
use Shlinkio\Shlink\Core\Action as CoreAction;
|
||||||
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
|
use Shlinkio\Shlink\Rest\Action;
|
||||||
|
use Shlinkio\Shlink\Rest\ConfigProvider;
|
||||||
|
use Shlinkio\Shlink\Rest\Middleware;
|
||||||
|
use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
// The order of the routes defined here matters. Changing it might cause path conflicts
|
||||||
|
return (static function (): array {
|
||||||
|
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
|
||||||
|
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||||
|
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||||
|
$multiSegment = (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false);
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'routes' => [
|
||||||
|
// Rest
|
||||||
|
...ConfigProvider::applyRoutesPrefix([
|
||||||
|
Action\HealthAction::getRouteDef(),
|
||||||
|
|
||||||
|
// Visits
|
||||||
|
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
|
Action\Visit\TagVisitsAction::getRouteDef(),
|
||||||
|
Action\Visit\DomainVisitsAction::getRouteDef(),
|
||||||
|
Action\Visit\GlobalVisitsAction::getRouteDef(),
|
||||||
|
Action\Visit\OrphanVisitsAction::getRouteDef(),
|
||||||
|
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
|
||||||
|
|
||||||
|
// Short URLs
|
||||||
|
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
|
||||||
|
$contentNegotiationMiddleware,
|
||||||
|
$dropDomainMiddleware,
|
||||||
|
$overrideDomainMiddleware,
|
||||||
|
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class,
|
||||||
|
]),
|
||||||
|
Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([
|
||||||
|
$contentNegotiationMiddleware,
|
||||||
|
$overrideDomainMiddleware,
|
||||||
|
]),
|
||||||
|
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
|
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
|
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||||
|
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
Action\Tag\ListTagsAction::getRouteDef(),
|
||||||
|
Action\Tag\TagsStatsAction::getRouteDef(),
|
||||||
|
Action\Tag\DeleteTagsAction::getRouteDef(),
|
||||||
|
Action\Tag\UpdateTagAction::getRouteDef(),
|
||||||
|
|
||||||
|
// Domains
|
||||||
|
Action\Domain\ListDomainsAction::getRouteDef(),
|
||||||
|
Action\Domain\DomainRedirectsAction::getRouteDef(),
|
||||||
|
|
||||||
|
Action\MercureInfoAction::getRouteDef([NotConfiguredMercureErrorHandler::class]),
|
||||||
|
], $multiSegment),
|
||||||
|
|
||||||
|
// Non-rest
|
||||||
|
[
|
||||||
|
'name' => CoreAction\RobotsAction::class,
|
||||||
|
'path' => '/robots.txt',
|
||||||
|
'middleware' => [
|
||||||
|
CoreAction\RobotsAction::class,
|
||||||
|
],
|
||||||
|
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => CoreAction\PixelAction::class,
|
||||||
|
'path' => sprintf('/{shortCode%s}/track', $multiSegment ? ':.+' : ''),
|
||||||
|
'middleware' => [
|
||||||
|
IpAddress::class,
|
||||||
|
CoreAction\PixelAction::class,
|
||||||
|
],
|
||||||
|
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => CoreAction\QrCodeAction::class,
|
||||||
|
'path' => sprintf('/{shortCode%s}/qr-code', $multiSegment ? ':.+' : ''),
|
||||||
|
'middleware' => [
|
||||||
|
CoreAction\QrCodeAction::class,
|
||||||
|
],
|
||||||
|
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => CoreAction\RedirectAction::class,
|
||||||
|
'path' => sprintf('/{shortCode%s}', $multiSegment ? ':.+' : ''),
|
||||||
|
'middleware' => [
|
||||||
|
IpAddress::class,
|
||||||
|
CoreAction\RedirectAction::class,
|
||||||
|
],
|
||||||
|
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
})();
|
@ -7,7 +7,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
|||||||
use const Shlinkio\Shlink\MIN_TASK_WORKERS;
|
use const Shlinkio\Shlink\MIN_TASK_WORKERS;
|
||||||
|
|
||||||
return (static function (): array {
|
return (static function (): array {
|
||||||
$taskWorkers = (int) EnvVars::TASK_WORKER_NUM()->loadFromEnv(16);
|
$taskWorkers = (int) EnvVars::TASK_WORKER_NUM->loadFromEnv(16);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@ -17,11 +17,11 @@ return (static function (): array {
|
|||||||
|
|
||||||
'swoole-http-server' => [
|
'swoole-http-server' => [
|
||||||
'host' => '0.0.0.0',
|
'host' => '0.0.0.0',
|
||||||
'port' => (int) EnvVars::PORT()->loadFromEnv(8080),
|
'port' => (int) EnvVars::PORT->loadFromEnv(8080),
|
||||||
'process-name' => 'shlink',
|
'process-name' => 'shlink',
|
||||||
|
|
||||||
'options' => [
|
'options' => [
|
||||||
'worker_num' => (int) EnvVars::WEB_WORKER_NUM()->loadFromEnv(16),
|
'worker_num' => (int) EnvVars::WEB_WORKER_NUM->loadFromEnv(16),
|
||||||
'task_worker_num' => max($taskWorkers, MIN_TASK_WORKERS),
|
'task_worker_num' => max($taskWorkers, MIN_TASK_WORKERS),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
@ -9,28 +9,28 @@ return [
|
|||||||
'tracking' => [
|
'tracking' => [
|
||||||
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
||||||
// This applies only if IP address tracking is enabled
|
// This applies only if IP address tracking is enabled
|
||||||
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR()->loadFromEnv(true),
|
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(true),
|
||||||
|
|
||||||
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
// Tells if visits to not-found URLs should be tracked. The disable_tracking option takes precedence
|
||||||
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS()->loadFromEnv(true),
|
'track_orphan_visits' => (bool) EnvVars::TRACK_ORPHAN_VISITS->loadFromEnv(true),
|
||||||
|
|
||||||
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM()->loadFromEnv(),
|
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
||||||
|
|
||||||
// If true, visits will not be tracked at all
|
// If true, visits will not be tracked at all
|
||||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING()->loadFromEnv(false),
|
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(false),
|
||||||
|
|
||||||
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
// If true, visits will be tracked, but neither the IP address, nor the location will be resolved
|
||||||
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING()->loadFromEnv(false),
|
'disable_ip_tracking' => (bool) EnvVars::DISABLE_IP_TRACKING->loadFromEnv(false),
|
||||||
|
|
||||||
// If true, the referrer will not be tracked
|
// If true, the referrer will not be tracked
|
||||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING()->loadFromEnv(false),
|
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(false),
|
||||||
|
|
||||||
// If true, the user agent will not be tracked
|
// If true, the user agent will not be tracked
|
||||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING()->loadFromEnv(false),
|
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(false),
|
||||||
|
|
||||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||||
'disable_tracking_from' => EnvVars::DISABLE_TRACKING_FROM()->loadFromEnv(),
|
'disable_tracking_from' => EnvVars::DISABLE_TRACKING_FROM->loadFromEnv(),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -9,7 +9,7 @@ use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
|
|||||||
|
|
||||||
return (static function (): array {
|
return (static function (): array {
|
||||||
$shortCodesLength = max(
|
$shortCodesLength = max(
|
||||||
(int) EnvVars::DEFAULT_SHORT_CODES_LENGTH()->loadFromEnv(DEFAULT_SHORT_CODES_LENGTH),
|
(int) EnvVars::DEFAULT_SHORT_CODES_LENGTH->loadFromEnv(DEFAULT_SHORT_CODES_LENGTH),
|
||||||
MIN_SHORT_CODES_LENGTH,
|
MIN_SHORT_CODES_LENGTH,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -17,12 +17,13 @@ return (static function (): array {
|
|||||||
|
|
||||||
'url_shortener' => [
|
'url_shortener' => [
|
||||||
'domain' => [ // TODO Refactor this structure to url_shortener.schema and url_shortener.default_domain
|
'domain' => [ // TODO Refactor this structure to url_shortener.schema and url_shortener.default_domain
|
||||||
'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED()->loadFromEnv(true)) ? 'https' : 'http',
|
'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED->loadFromEnv(true)) ? 'https' : 'http',
|
||||||
'hostname' => EnvVars::DEFAULT_DOMAIN()->loadFromEnv(''),
|
'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(''),
|
||||||
],
|
],
|
||||||
'default_short_codes_length' => $shortCodesLength,
|
'default_short_codes_length' => $shortCodesLength,
|
||||||
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES()->loadFromEnv(false),
|
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
|
||||||
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH()->loadFromEnv(false),
|
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false),
|
||||||
|
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -6,14 +6,14 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
|||||||
|
|
||||||
// Deprecated. Webhooks are no longer supported. To be removed in Shlink 4.0.0
|
// Deprecated. Webhooks are no longer supported. To be removed in Shlink 4.0.0
|
||||||
return (static function (): array {
|
return (static function (): array {
|
||||||
$webhooks = EnvVars::VISITS_WEBHOOKS()->loadFromEnv();
|
$webhooks = EnvVars::VISITS_WEBHOOKS->loadFromEnv();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'visits_webhooks' => [
|
'visits_webhooks' => [
|
||||||
'webhooks' => $webhooks === null ? [] : explode(',', $webhooks),
|
'webhooks' => $webhooks === null ? [] : explode(',', $webhooks),
|
||||||
'notify_orphan_visits_to_webhooks' =>
|
'notify_orphan_visits_to_webhooks' =>
|
||||||
(bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS()->loadFromEnv(false),
|
(bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS->loadFromEnv(false),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -43,6 +43,8 @@ return (new ConfigAggregator\ConfigAggregator([
|
|||||||
$isTestEnv
|
$isTestEnv
|
||||||
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
|
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
|
||||||
: new ConfigAggregator\ArrayProvider([]),
|
: new ConfigAggregator\ArrayProvider([]),
|
||||||
|
// Routes have to be loaded last
|
||||||
|
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
|
||||||
], 'data/cache/app_config.php', [
|
], 'data/cache/app_config.php', [
|
||||||
Core\Config\BasePathPrefixer::class,
|
Core\Config\BasePathPrefixer::class,
|
||||||
]))->getMergedConfig();
|
]))->getMergedConfig();
|
||||||
|
@ -13,7 +13,7 @@ chdir(dirname(__DIR__));
|
|||||||
require 'vendor/autoload.php';
|
require 'vendor/autoload.php';
|
||||||
|
|
||||||
// This is one of the first files loaded. Configure the timezone here
|
// This is one of the first files loaded. Configure the timezone here
|
||||||
date_default_timezone_set(EnvVars::TIMEZONE()->loadFromEnv(date_default_timezone_get()));
|
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get()));
|
||||||
|
|
||||||
// This class alias tricks the ConfigAbstractFactory to return Lock\Factory instances even with a different service name
|
// This class alias tricks the ConfigAbstractFactory to return Lock\Factory instances even with a different service name
|
||||||
// It needs to be placed here as individual config files will not be loaded once config is cached
|
// It needs to be placed here as individual config files will not be loaded once config is cached
|
||||||
|
@ -8,8 +8,7 @@ use GuzzleHttp\Client;
|
|||||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||||
use Laminas\Diactoros\Response\EmptyResponse;
|
use Laminas\Diactoros\Response\EmptyResponse;
|
||||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||||
use Monolog\Handler\StreamHandler;
|
use Monolog\Level;
|
||||||
use Monolog\Logger;
|
|
||||||
use PHPUnit\Runner\Version;
|
use PHPUnit\Runner\Version;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
@ -20,6 +19,7 @@ use SebastianBergmann\CodeCoverage\Filter;
|
|||||||
use SebastianBergmann\CodeCoverage\Report\Html\Facade as Html;
|
use SebastianBergmann\CodeCoverage\Report\Html\Facade as Html;
|
||||||
use SebastianBergmann\CodeCoverage\Report\PHP;
|
use SebastianBergmann\CodeCoverage\Report\PHP;
|
||||||
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
|
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
|
||||||
|
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||||
|
|
||||||
use function Laminas\Stratigility\middleware;
|
use function Laminas\Stratigility\middleware;
|
||||||
use function Shlinkio\Shlink\Config\env;
|
use function Shlinkio\Shlink\Config\env;
|
||||||
@ -76,16 +76,10 @@ $buildDbConnection = static function (): array {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
$buildTestLoggerConfig = fn (string $handlerName, string $filename) => [
|
$buildTestLoggerConfig = static fn (string $filename) => [
|
||||||
'handlers' => [
|
'level' => Level::Debug->value,
|
||||||
$handlerName => [
|
'type' => LoggerType::STREAM->value,
|
||||||
'name' => StreamHandler::class,
|
'destination' => sprintf('data/log/api-tests/%s', $filename),
|
||||||
'params' => [
|
|
||||||
'level' => Logger::DEBUG,
|
|
||||||
'stream' => sprintf('data/log/api-tests/%s', $filename),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -183,8 +177,8 @@ return [
|
|||||||
],
|
],
|
||||||
|
|
||||||
'logger' => [
|
'logger' => [
|
||||||
'Shlink' => $buildTestLoggerConfig('shlink_handler', 'shlink.log'),
|
'Shlink' => $buildTestLoggerConfig('shlink.log'),
|
||||||
'Access' => $buildTestLoggerConfig('access_handler', 'access.log'),
|
'Access' => $buildTestLoggerConfig('access.log'),
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
@ -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/php8.0-fpm.sock;
|
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||||
fastcgi_index index.php;
|
fastcgi_index index.php;
|
||||||
include fastcgi.conf;
|
include fastcgi.conf;
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
FROM php:8.1.5-fpm-alpine3.15
|
FROM php:8.1.9-fpm-alpine3.16
|
||||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.21
|
ENV APCU_VERSION 5.1.21
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.0
|
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||||
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
FROM php:8.1.5-alpine3.15
|
FROM php:8.1.9-alpine3.16
|
||||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.21
|
ENV APCU_VERSION 5.1.21
|
||||||
ENV INOTIFY_VERSION 3.0.0
|
ENV INOTIFY_VERSION 3.0.0
|
||||||
ENV OPENSWOOLE_VERSION 4.11.1
|
ENV OPENSWOOLE_VERSION 4.11.1
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.0
|
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||||
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
|
@ -8,8 +8,8 @@ use Doctrine\DBAL\Platforms\MySQLPlatform;
|
|||||||
use Doctrine\DBAL\Schema\Schema;
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||||
|
|
||||||
final class Version20210207100807 extends AbstractMigration
|
final class Version20210207100807 extends AbstractMigration
|
||||||
{
|
{
|
||||||
@ -27,7 +27,7 @@ final class Version20210207100807 extends AbstractMigration
|
|||||||
]);
|
]);
|
||||||
$visits->addColumn('type', Types::STRING, [
|
$visits->addColumn('type', Types::STRING, [
|
||||||
'length' => 255,
|
'length' => 255,
|
||||||
'default' => Visit::TYPE_VALID_SHORT_URL,
|
'default' => VisitType::VALID_SHORT_URL->value,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,22 +4,13 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink;
|
namespace Shlinkio\Shlink;
|
||||||
|
|
||||||
use Monolog\Handler\StreamHandler;
|
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||||
use Monolog\Logger;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'logger' => [
|
'logger' => [
|
||||||
'Shlink' => [
|
'Shlink' => [
|
||||||
'handlers' => [
|
'type' => LoggerType::STREAM->value,
|
||||||
'shlink_handler' => [
|
|
||||||
'name' => StreamHandler::class,
|
|
||||||
'params' => [
|
|
||||||
'level' => Logger::INFO,
|
|
||||||
'stream' => 'php://stdout',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ The intention is to implement a system that allows adding to API keys as many of
|
|||||||
|
|
||||||
Supporting more restrictions in the future is also desirable.
|
Supporting more restrictions in the future is also desirable.
|
||||||
|
|
||||||
## Considered option
|
## Considered options
|
||||||
|
|
||||||
* Using an ACL/RBAC library, and checking roles in a middleware.
|
* Using an ACL/RBAC library, and checking roles in a middleware.
|
||||||
* Using a service that, provided an API key, tells if certain resource is reachable while it also allows building queries dynamically.
|
* Using a service that, provided an API key, tells if certain resource is reachable while it also allows building queries dynamically.
|
||||||
|
@ -11,7 +11,7 @@ However, it does not track visits to any of those, just to valid short URLs.
|
|||||||
|
|
||||||
The intention is to change that, and allow users to track the cases mentioned above.
|
The intention is to change that, and allow users to track the cases mentioned above.
|
||||||
|
|
||||||
## Considered option
|
## Considered options
|
||||||
|
|
||||||
* Create a new table to track visits o this kind.
|
* Create a new table to track visits o this kind.
|
||||||
* Reuse the existing `visits` table, by making `short_url_id` nullable and adding a couple of other fields.
|
* Reuse the existing `visits` table, by making `short_url_id` nullable and adding a couple of other fields.
|
||||||
|
@ -13,7 +13,7 @@ However, after the creation of the caching PSRs ([PSR-6 - Cache](https://www.php
|
|||||||
|
|
||||||
Also, Shlink needs support for Redis clusters and Redis sentinels, which is not supported by `doctrine/cache` Redis adapters.
|
Also, Shlink needs support for Redis clusters and Redis sentinels, which is not supported by `doctrine/cache` Redis adapters.
|
||||||
|
|
||||||
## Considered option
|
## Considered options
|
||||||
|
|
||||||
After some research, the only packages that seem to support the capabilities required by Shlink and also seem healthy, are these:
|
After some research, the only packages that seem to support the capabilities required by Shlink and also seem healthy, are these:
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ It is potentially possible to combine both, but if you do so, you will find out
|
|||||||
|
|
||||||
A [Twitter survey](https://twitter.com/shlinkio/status/1480614855006732289) has also showed up all participants also found the behavior should be the opposite.
|
A [Twitter survey](https://twitter.com/shlinkio/status/1480614855006732289) has also showed up all participants also found the behavior should be the opposite.
|
||||||
|
|
||||||
## Considered option
|
## Considered options
|
||||||
|
|
||||||
* Move the logic to read env vars to another config file which always overrides installer options.
|
* Move the logic to read env vars to another config file which always overrides installer options.
|
||||||
* Move the logic to read env vars to a config post-processor which overrides config dynamically, only if the appropriate env var had been defined.
|
* Move the logic to read env vars to a config post-processor which overrides config dynamically, only if the appropriate env var had been defined.
|
||||||
|
42
docs/adr/2022-08-05-support-multi-segment-custom-slugs.md
Normal file
42
docs/adr/2022-08-05-support-multi-segment-custom-slugs.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Support multi-segment custom slugs
|
||||||
|
|
||||||
|
* Status: Accepted
|
||||||
|
* Date: 2022-08-05
|
||||||
|
|
||||||
|
## Context and problem statement
|
||||||
|
|
||||||
|
There's a new requirement to support multi-segment custom slugs (as in `https://exam.ple/foo/bar/baz`).
|
||||||
|
|
||||||
|
The internal router does not support this at the moment, as it only matches the shortCode in one of the segments.
|
||||||
|
|
||||||
|
## Considered options
|
||||||
|
|
||||||
|
* Tweak the internal router, so that it is capable of matching multiple segments for the slug, in every route that requires it.
|
||||||
|
* Define a new set of routes with a short prefix that allows configuring multi-segment in those, without touching the existing routes.
|
||||||
|
* Let the router fail, and use a middleware to fall back to the proper route (similar to what was done for the extra path forwarding feature).
|
||||||
|
|
||||||
|
## Decision outcome
|
||||||
|
|
||||||
|
Even though I was initially inclined to use a fallback middleware, that has turned out to be harder than anticipated, because there are several possible routes where the slug is used, and we would still need some kind of router to determine which one matches.
|
||||||
|
|
||||||
|
Because of that, the selected approach has been to tweak the existing router, so that it can match multiple segments, and moving the configuration of routes to a common place so that they can be defined in the proper order that prevents conflicts.
|
||||||
|
|
||||||
|
## Pros and Cons of the Options
|
||||||
|
|
||||||
|
### Tweaking the router
|
||||||
|
|
||||||
|
* Bad: It requires routes to be defined in a specific order, and remember it in the future if more routes are added.
|
||||||
|
* Good: It initially requires fewer changes.
|
||||||
|
* Good: Once routes are defined in the proper order, all the internal logic works out of the box.
|
||||||
|
|
||||||
|
### Defining new routes
|
||||||
|
|
||||||
|
* Bad: The end-user experience gets affected.
|
||||||
|
* Bad: Probably a lot of side effects would happen when it comes to assembling short URLs.
|
||||||
|
* Bad: Routing needs to be configured twice, resolving the same logic.
|
||||||
|
* Bad: It turns out to still conflict with some routes, even with the prefix, which defeats what looked like its main benefit.
|
||||||
|
|
||||||
|
### Let routing fail and fall back in middleware
|
||||||
|
|
||||||
|
* Good: Does not require changing routes configuration, which means less side effects.
|
||||||
|
* Bad: Since many routes can potentially end up in the middleware, there's still the need to have some kind of routing logic.
|
@ -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.
|
Here listed you will find the different architectural decisions taken in the project, including all the reasoning behind it, options considered, and final outcome.
|
||||||
|
|
||||||
|
* [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)
|
* [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)
|
* [2021-08-05 Migrate to a new caching library](2021-08-05-migrate-to-a-new-caching-library.md)
|
||||||
* [2021-02-07 Track visits to 'base_url', 'invalid_short_url' and 'regular_404'](2021-02-07-track-visits-to-base-url-invalid-short-url-and-regular-404.md)
|
* [2021-02-07 Track visits to 'base_url', 'invalid_short_url' and 'regular_404'](2021-02-07-track-visits-to-base-url-invalid-short-url-and-regular-404.md)
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"asyncapi": "2.0.0",
|
"asyncapi": "2.4.0",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "Shlink",
|
"title": "Shlink",
|
||||||
"version": "2.0.0",
|
"version": "3.0.0",
|
||||||
"description": "Shlink, the self-hosted URL shortener",
|
"description": "Shlink, the self-hosted URL shortener",
|
||||||
"license": {
|
"license": {
|
||||||
"name": "MIT",
|
"name": "MIT",
|
||||||
@ -75,6 +75,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"https://shlink.io/new-short-url": {
|
||||||
|
"subscribe": {
|
||||||
|
"summary": "Receive information about any new short URL.",
|
||||||
|
"operationId": "newshortUrl",
|
||||||
|
"message": {
|
||||||
|
"payload": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"shortUrl": {
|
||||||
|
"$ref": "#/components/schemas/ShortUrl"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
@ -101,7 +118,7 @@
|
|||||||
},
|
},
|
||||||
"visitsCount": {
|
"visitsCount": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "The number of visits that this short URL has recieved."
|
"description": "The number of visits that this short URL has received."
|
||||||
},
|
},
|
||||||
"tags": {
|
"tags": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
},
|
},
|
||||||
"visitsCount": {
|
"visitsCount": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "The number of visits that this short URL has recieved."
|
"description": "The number of visits that this short URL has received."
|
||||||
},
|
},
|
||||||
"tags": {
|
"tags": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
|
@ -312,7 +312,7 @@
|
|||||||
},
|
},
|
||||||
"threshold": {
|
"threshold": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"description": "The amount of visits currently configured as threshold to allow deleting short UYRLs or not"
|
"description": "The amount of visits currently configured as threshold to allow deleting short URLs or not"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@
|
|||||||
{
|
{
|
||||||
"name": "errorCorrection",
|
"name": "errorCorrection",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"description": "The error correction level to apply to the the QR code: **[L]**ow, **[M]**edium, **[Q]**uartile or **[H]**igh. See [docs](https://www.qrcode.com/en/about/error_correction.html).",
|
"description": "The error correction level to apply to the QR code: **[L]**ow, **[M]**edium, **[Q]**uartile or **[H]**igh. See [docs](https://www.qrcode.com/en/about/error_correction.html).",
|
||||||
"required": false,
|
"required": false,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Shlink",
|
"title": "Shlink",
|
||||||
"description": "Shlink, the self-hosted URL shortener",
|
"description": "Shlink, the self-hosted URL shortener",
|
||||||
"version": "1.0"
|
"version": "2.0"
|
||||||
},
|
},
|
||||||
|
|
||||||
"externalDocs": {
|
"externalDocs": {
|
||||||
|
@ -11,11 +11,13 @@ return [
|
|||||||
Command\ShortUrl\CreateShortUrlCommand::NAME => Command\ShortUrl\CreateShortUrlCommand::class,
|
Command\ShortUrl\CreateShortUrlCommand::NAME => Command\ShortUrl\CreateShortUrlCommand::class,
|
||||||
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\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::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\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class,
|
Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class,
|
||||||
|
Command\Visit\GetOrphanVisitsCommand::NAME => Command\Visit\GetOrphanVisitsCommand::class,
|
||||||
|
Command\Visit\GetNonOrphanVisitsCommand::NAME => Command\Visit\GetNonOrphanVisitsCommand::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,
|
||||||
@ -24,9 +26,11 @@ return [
|
|||||||
Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class,
|
Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class,
|
||||||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||||
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class,
|
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class,
|
||||||
|
Command\Tag\GetTagVisitsCommand::NAME => Command\Tag\GetTagVisitsCommand::class,
|
||||||
|
|
||||||
Command\Domain\ListDomainsCommand::NAME => Command\Domain\ListDomainsCommand::class,
|
Command\Domain\ListDomainsCommand::NAME => Command\Domain\ListDomainsCommand::class,
|
||||||
Command\Domain\DomainRedirectsCommand::NAME => Command\Domain\DomainRedirectsCommand::class,
|
Command\Domain\DomainRedirectsCommand::NAME => Command\Domain\DomainRedirectsCommand::class,
|
||||||
|
Command\Domain\GetDomainVisitsCommand::NAME => Command\Domain\GetDomainVisitsCommand::class,
|
||||||
|
|
||||||
Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class,
|
Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class,
|
||||||
Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class,
|
Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class,
|
||||||
|
@ -11,6 +11,7 @@ use Laminas\ServiceManager\Factory\InvokableFactory;
|
|||||||
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||||
use Shlinkio\Shlink\Core\Domain\DomainService;
|
use Shlinkio\Shlink\Core\Domain\DomainService;
|
||||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||||
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
use Shlinkio\Shlink\Core\Service;
|
use Shlinkio\Shlink\Core\Service;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||||
@ -42,11 +43,13 @@ return [
|
|||||||
Command\ShortUrl\CreateShortUrlCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\CreateShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||||
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\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class,
|
Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
|
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
Command\Visit\GetOrphanVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
Command\Visit\GetNonOrphanVisitsCommand::class => ConfigAbstractFactory::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,
|
||||||
@ -55,12 +58,14 @@ return [
|
|||||||
Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class,
|
Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
|
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class,
|
Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
Command\Tag\GetTagVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class,
|
Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Db\MigrateDatabaseCommand::class => ConfigAbstractFactory::class,
|
Command\Db\MigrateDatabaseCommand::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Command\Domain\ListDomainsCommand::class => ConfigAbstractFactory::class,
|
Command\Domain\ListDomainsCommand::class => ConfigAbstractFactory::class,
|
||||||
Command\Domain\DomainRedirectsCommand::class => ConfigAbstractFactory::class,
|
Command\Domain\DomainRedirectsCommand::class => ConfigAbstractFactory::class,
|
||||||
|
Command\Domain\GetDomainVisitsCommand::class => ConfigAbstractFactory::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -77,15 +82,14 @@ return [
|
|||||||
Command\ShortUrl\CreateShortUrlCommand::class => [
|
Command\ShortUrl\CreateShortUrlCommand::class => [
|
||||||
Service\UrlShortener::class,
|
Service\UrlShortener::class,
|
||||||
ShortUrlStringifier::class,
|
ShortUrlStringifier::class,
|
||||||
'config.url_shortener.default_short_codes_length',
|
UrlShortenerOptions::class,
|
||||||
'config.url_shortener.domain.hostname',
|
|
||||||
],
|
],
|
||||||
Command\ShortUrl\ResolveUrlCommand::class => [Service\ShortUrl\ShortUrlResolver::class],
|
Command\ShortUrl\ResolveUrlCommand::class => [Service\ShortUrl\ShortUrlResolver::class],
|
||||||
Command\ShortUrl\ListShortUrlsCommand::class => [
|
Command\ShortUrl\ListShortUrlsCommand::class => [
|
||||||
Service\ShortUrlService::class,
|
Service\ShortUrlService::class,
|
||||||
ShortUrlDataTransformer::class,
|
ShortUrlDataTransformer::class,
|
||||||
],
|
],
|
||||||
Command\ShortUrl\GetVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
Command\ShortUrl\DeleteShortUrlCommand::class => [Service\ShortUrl\DeleteShortUrlService::class],
|
||||||
|
|
||||||
Command\Visit\DownloadGeoLiteDbCommand::class => [Util\GeolocationDbUpdater::class],
|
Command\Visit\DownloadGeoLiteDbCommand::class => [Util\GeolocationDbUpdater::class],
|
||||||
@ -94,6 +98,8 @@ return [
|
|||||||
IpLocationResolverInterface::class,
|
IpLocationResolverInterface::class,
|
||||||
LockFactory::class,
|
LockFactory::class,
|
||||||
],
|
],
|
||||||
|
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
|
||||||
|
Command\Visit\GetNonOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
|
||||||
|
|
||||||
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
|
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
|
||||||
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
|
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
|
||||||
@ -102,9 +108,11 @@ return [
|
|||||||
Command\Tag\ListTagsCommand::class => [TagService::class],
|
Command\Tag\ListTagsCommand::class => [TagService::class],
|
||||||
Command\Tag\RenameTagCommand::class => [TagService::class],
|
Command\Tag\RenameTagCommand::class => [TagService::class],
|
||||||
Command\Tag\DeleteTagsCommand::class => [TagService::class],
|
Command\Tag\DeleteTagsCommand::class => [TagService::class],
|
||||||
|
Command\Tag\GetTagVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
|
||||||
|
|
||||||
Command\Domain\ListDomainsCommand::class => [DomainService::class],
|
Command\Domain\ListDomainsCommand::class => [DomainService::class],
|
||||||
Command\Domain\DomainRedirectsCommand::class => [DomainService::class],
|
Command\Domain\DomainRedirectsCommand::class => [DomainService::class],
|
||||||
|
Command\Domain\GetDomainVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
|
||||||
|
|
||||||
Command\Db\CreateDatabaseCommand::class => [
|
Command\Db\CreateDatabaseCommand::class => [
|
||||||
LockFactory::class,
|
LockFactory::class,
|
||||||
|
@ -73,13 +73,16 @@ class GenerateKeyCommand extends Command
|
|||||||
$authorOnly,
|
$authorOnly,
|
||||||
'a',
|
'a',
|
||||||
InputOption::VALUE_NONE,
|
InputOption::VALUE_NONE,
|
||||||
sprintf('Adds the "%s" role to the new API key.', Role::AUTHORED_SHORT_URLS),
|
sprintf('Adds the "%s" role to the new API key.', Role::AUTHORED_SHORT_URLS->value),
|
||||||
)
|
)
|
||||||
->addOption(
|
->addOption(
|
||||||
$domainOnly,
|
$domainOnly,
|
||||||
'd',
|
'd',
|
||||||
InputOption::VALUE_REQUIRED,
|
InputOption::VALUE_REQUIRED,
|
||||||
sprintf('Adds the "%s" role to the new API key, with the domain provided.', Role::DOMAIN_SPECIFIC),
|
sprintf(
|
||||||
|
'Adds the "%s" role to the new API key, with the domain provided.',
|
||||||
|
Role::DOMAIN_SPECIFIC->value,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
->setHelp($help);
|
->setHelp($help);
|
||||||
}
|
}
|
||||||
@ -99,7 +102,7 @@ class GenerateKeyCommand extends Command
|
|||||||
if (! $apiKey->isAdmin()) {
|
if (! $apiKey->isAdmin()) {
|
||||||
ShlinkTable::default($io)->render(
|
ShlinkTable::default($io)->render(
|
||||||
['Role name', 'Role metadata'],
|
['Role name', 'Role metadata'],
|
||||||
$apiKey->mapRoles(fn (string $name, array $meta) => [$name, arrayToString($meta, 0)]),
|
$apiKey->mapRoles(fn (Role $role, array $meta) => [$role->value, arrayToString($meta, 0)]),
|
||||||
null,
|
null,
|
||||||
'Roles',
|
'Roles',
|
||||||
);
|
);
|
||||||
|
@ -60,10 +60,10 @@ class ListKeysCommand extends Command
|
|||||||
}
|
}
|
||||||
$rowData[] = $expiration?->toAtomString() ?? '-';
|
$rowData[] = $expiration?->toAtomString() ?? '-';
|
||||||
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
||||||
fn (string $roleName, array $meta) =>
|
fn (Role $role, array $meta) =>
|
||||||
empty($meta)
|
empty($meta)
|
||||||
? Role::toFriendlyName($roleName)
|
? Role::toFriendlyName($role)
|
||||||
: sprintf('%s: %s', Role::toFriendlyName($roleName), Role::domainAuthorityFromMeta($meta)),
|
: sprintf('%s: %s', Role::toFriendlyName($role), Role::domainAuthorityFromMeta($meta)),
|
||||||
));
|
));
|
||||||
|
|
||||||
return $rowData;
|
return $rowData;
|
||||||
|
@ -53,7 +53,7 @@ class DomainRedirectsCommand extends Command
|
|||||||
|
|
||||||
/** @var string[] $availableDomains */
|
/** @var string[] $availableDomains */
|
||||||
$availableDomains = invoke(
|
$availableDomains = invoke(
|
||||||
filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault()),
|
filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
|
||||||
'toString',
|
'toString',
|
||||||
);
|
);
|
||||||
if (empty($availableDomains)) {
|
if (empty($availableDomains)) {
|
||||||
|
50
module/CLI/src/Command/Domain/GetDomainVisitsCommand.php
Normal file
50
module/CLI/src/Command/Domain/GetDomainVisitsCommand.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Domain;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
|
||||||
|
class GetDomainVisitsCommand extends AbstractVisitsListCommand
|
||||||
|
{
|
||||||
|
public const NAME = 'domain:visits';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
VisitsStatsHelperInterface $visitsHelper,
|
||||||
|
private readonly ShortUrlStringifierInterface $shortUrlStringifier,
|
||||||
|
) {
|
||||||
|
parent::__construct($visitsHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doConfigure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName(self::NAME)
|
||||||
|
->setDescription('Returns the list of visits for provided domain.')
|
||||||
|
->addArgument('domain', InputArgument::REQUIRED, 'The domain which visits we want to get.');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||||
|
{
|
||||||
|
$domain = $input->getArgument('domain');
|
||||||
|
return $this->visitsHelper->visitsForDomain($domain, new VisitsParams($dateRange));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function mapExtraFields(Visit $visit): array
|
||||||
|
{
|
||||||
|
$shortUrl = $visit->getShortUrl();
|
||||||
|
return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)];
|
||||||
|
}
|
||||||
|
}
|
@ -48,12 +48,12 @@ class ListDomainsCommand extends Command
|
|||||||
$table->render(
|
$table->render(
|
||||||
$showRedirects ? [...$commonFields, '"Not found" redirects'] : $commonFields,
|
$showRedirects ? [...$commonFields, '"Not found" redirects'] : $commonFields,
|
||||||
map($domains, function (DomainItem $domain) use ($showRedirects) {
|
map($domains, function (DomainItem $domain) use ($showRedirects) {
|
||||||
$commonValues = [$domain->toString(), $domain->isDefault() ? 'Yes' : 'No'];
|
$commonValues = [$domain->toString(), $domain->isDefault ? 'Yes' : 'No'];
|
||||||
|
|
||||||
return $showRedirects
|
return $showRedirects
|
||||||
? [
|
? [
|
||||||
...$commonValues,
|
...$commonValues,
|
||||||
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig()),
|
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig),
|
||||||
]
|
]
|
||||||
: $commonValues;
|
: $commonValues;
|
||||||
}),
|
}),
|
||||||
|
@ -5,9 +5,11 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
|
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||||
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
|
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
|
||||||
@ -19,6 +21,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
use function array_map;
|
use function array_map;
|
||||||
|
use function explode;
|
||||||
use function Functional\curry;
|
use function Functional\curry;
|
||||||
use function Functional\flatten;
|
use function Functional\flatten;
|
||||||
use function Functional\unique;
|
use function Functional\unique;
|
||||||
@ -29,14 +32,15 @@ class CreateShortUrlCommand extends Command
|
|||||||
public const NAME = 'short-url:create';
|
public const NAME = 'short-url:create';
|
||||||
|
|
||||||
private ?SymfonyStyle $io;
|
private ?SymfonyStyle $io;
|
||||||
|
private string $defaultDomain;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private UrlShortenerInterface $urlShortener,
|
private readonly UrlShortenerInterface $urlShortener,
|
||||||
private ShortUrlStringifierInterface $stringifier,
|
private readonly ShortUrlStringifierInterface $stringifier,
|
||||||
private int $defaultShortCodeLength,
|
private readonly UrlShortenerOptions $options,
|
||||||
private string $defaultDomain,
|
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
|
$this->defaultDomain = $this->options->domain()['hostname'] ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configure(): void
|
protected function configure(): void
|
||||||
@ -150,11 +154,11 @@ class CreateShortUrlCommand extends Command
|
|||||||
return ExitCodes::EXIT_FAILURE;
|
return ExitCodes::EXIT_FAILURE;
|
||||||
}
|
}
|
||||||
|
|
||||||
$explodeWithComma = curry('explode')(',');
|
$explodeWithComma = curry(explode(...))(',');
|
||||||
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
|
||||||
$customSlug = $input->getOption('custom-slug');
|
$customSlug = $input->getOption('custom-slug');
|
||||||
$maxVisits = $input->getOption('max-visits');
|
$maxVisits = $input->getOption('max-visits');
|
||||||
$shortCodeLength = $input->getOption('short-code-length') ?? $this->defaultShortCodeLength;
|
$shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength();
|
||||||
$doValidateUrl = $input->getOption('validate-url');
|
$doValidateUrl = $input->getOption('validate-url');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -171,6 +175,7 @@ class CreateShortUrlCommand extends Command
|
|||||||
ShortUrlInputFilter::TAGS => $tags,
|
ShortUrlInputFilter::TAGS => $tags,
|
||||||
ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'),
|
ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'),
|
||||||
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
|
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
|
||||||
|
EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->value => $this->options->multiSegmentSlugsEnabled(),
|
||||||
]));
|
]));
|
||||||
|
|
||||||
$io->writeln([
|
$io->writeln([
|
||||||
|
@ -81,6 +81,6 @@ class DeleteShortUrlCommand extends Command
|
|||||||
private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void
|
private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void
|
||||||
{
|
{
|
||||||
$this->deleteShortUrlService->deleteByShortCode($identifier, $ignoreThreshold);
|
$this->deleteShortUrlService->deleteByShortCode($identifier, $ignoreThreshold);
|
||||||
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $identifier->shortCode()));
|
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $identifier->shortCode));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
59
module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php
Normal file
59
module/CLI/src/Command/ShortUrl/GetShortUrlVisitsCommand.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||||
|
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
|
|
||||||
|
class GetShortUrlVisitsCommand extends AbstractVisitsListCommand
|
||||||
|
{
|
||||||
|
public const NAME = 'short-url:visits';
|
||||||
|
|
||||||
|
protected function doConfigure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName(self::NAME)
|
||||||
|
->setDescription('Returns the detailed visits information for provided short code')
|
||||||
|
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get.')
|
||||||
|
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain for the short code.');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||||
|
{
|
||||||
|
$shortCode = $input->getArgument('shortCode');
|
||||||
|
if (! empty($shortCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$io = new SymfonyStyle($input, $output);
|
||||||
|
$shortCode = $io->ask('A short code was not provided. Which short code do you want to use?');
|
||||||
|
if (! empty($shortCode)) {
|
||||||
|
$input->setArgument('shortCode', $shortCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||||
|
{
|
||||||
|
$identifier = ShortUrlIdentifier::fromCli($input);
|
||||||
|
return $this->visitsHelper->visitsForShortUrl($identifier, new VisitsParams($dateRange));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function mapExtraFields(Visit $visit): array
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
@ -1,88 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
|
||||||
|
|
||||||
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
|
||||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
|
||||||
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation;
|
|
||||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
|
||||||
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 Functional\map;
|
|
||||||
use function Functional\select_keys;
|
|
||||||
use function Shlinkio\Shlink\Common\buildDateRange;
|
|
||||||
use function sprintf;
|
|
||||||
|
|
||||||
class GetVisitsCommand extends AbstractWithDateRangeCommand
|
|
||||||
{
|
|
||||||
public const NAME = 'short-url:visits';
|
|
||||||
|
|
||||||
public function __construct(private VisitsStatsHelperInterface $visitsHelper)
|
|
||||||
{
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function doConfigure(): void
|
|
||||||
{
|
|
||||||
$this
|
|
||||||
->setName(self::NAME)
|
|
||||||
->setDescription('Returns the detailed visits information for provided short code')
|
|
||||||
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get.')
|
|
||||||
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain for the short code.');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getStartDateDesc(string $optionName): string
|
|
||||||
{
|
|
||||||
return sprintf('Allows to filter visits, returning only those older than "%s".', $optionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function getEndDateDesc(string $optionName): string
|
|
||||||
{
|
|
||||||
return sprintf('Allows to filter visits, returning only those newer than "%s".', $optionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
|
||||||
{
|
|
||||||
$shortCode = $input->getArgument('shortCode');
|
|
||||||
if (! empty($shortCode)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$io = new SymfonyStyle($input, $output);
|
|
||||||
$shortCode = $io->ask('A short code was not provided. Which short code do you want to use?');
|
|
||||||
if (! empty($shortCode)) {
|
|
||||||
$input->setArgument('shortCode', $shortCode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function execute(InputInterface $input, OutputInterface $output): ?int
|
|
||||||
{
|
|
||||||
$identifier = ShortUrlIdentifier::fromCli($input);
|
|
||||||
$startDate = $this->getStartDateOption($input, $output);
|
|
||||||
$endDate = $this->getEndDateOption($input, $output);
|
|
||||||
|
|
||||||
$paginator = $this->visitsHelper->visitsForShortUrl(
|
|
||||||
$identifier,
|
|
||||||
new VisitsParams(buildDateRange($startDate, $endDate)),
|
|
||||||
);
|
|
||||||
|
|
||||||
$rows = map($paginator->getCurrentPageResults(), function (Visit $visit) {
|
|
||||||
$rowData = $visit->jsonSerialize();
|
|
||||||
$rowData['country'] = ($visit->getVisitLocation() ?? new UnknownVisitLocation())->getCountryName();
|
|
||||||
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
|
|
||||||
});
|
|
||||||
ShlinkTable::default($output)->render(['Referer', 'Date', 'User agent', 'Country'], $rows);
|
|
||||||
|
|
||||||
return ExitCodes::EXIT_SUCCESS;
|
|
||||||
}
|
|
||||||
}
|
|
@ -13,6 +13,7 @@ use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
|
|||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||||
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
|
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
@ -120,9 +121,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
|
|||||||
$page = (int) $input->getOption('page');
|
$page = (int) $input->getOption('page');
|
||||||
$searchTerm = $input->getOption('search-term');
|
$searchTerm = $input->getOption('search-term');
|
||||||
$tags = $input->getOption('tags');
|
$tags = $input->getOption('tags');
|
||||||
$tagsMode = $input->getOption('including-all-tags') === true
|
$tagsMode = $input->getOption('including-all-tags') === true ? TagsMode::ALL->value : TagsMode::ANY->value;
|
||||||
? ShortUrlsParams::TAGS_MODE_ALL
|
|
||||||
: ShortUrlsParams::TAGS_MODE_ANY;
|
|
||||||
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
||||||
$all = $input->getOption('all');
|
$all = $input->getOption('all');
|
||||||
$startDate = $this->getStartDateOption($input, $output);
|
$startDate = $this->getStartDateOption($input, $output);
|
||||||
|
50
module/CLI/src/Command/Tag/GetTagVisitsCommand.php
Normal file
50
module/CLI/src/Command/Tag/GetTagVisitsCommand.php
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
|
||||||
|
class GetTagVisitsCommand extends AbstractVisitsListCommand
|
||||||
|
{
|
||||||
|
public const NAME = 'tag:visits';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
VisitsStatsHelperInterface $visitsHelper,
|
||||||
|
private readonly ShortUrlStringifierInterface $shortUrlStringifier,
|
||||||
|
) {
|
||||||
|
parent::__construct($visitsHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doConfigure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName(self::NAME)
|
||||||
|
->setDescription('Returns the list of visits for provided tag.')
|
||||||
|
->addArgument('tag', InputArgument::REQUIRED, 'The tag which visits we want to get.');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||||
|
{
|
||||||
|
$tag = $input->getArgument('tag');
|
||||||
|
return $this->visitsHelper->visitsForTag($tag, new VisitsParams($dateRange));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function mapExtraFields(Visit $visit): array
|
||||||
|
{
|
||||||
|
$shortUrl = $visit->getShortUrl();
|
||||||
|
return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)];
|
||||||
|
}
|
||||||
|
}
|
@ -46,7 +46,7 @@ class ListTagsCommand extends Command
|
|||||||
|
|
||||||
return map(
|
return map(
|
||||||
$tags,
|
$tags,
|
||||||
static fn (TagInfo $tagInfo) => [$tagInfo->tag(), $tagInfo->shortUrlsCount(), $tagInfo->visitsCount()],
|
static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsCount],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ class RenameTagCommand extends Command
|
|||||||
{
|
{
|
||||||
public const NAME = 'tag:rename';
|
public const NAME = 'tag:rename';
|
||||||
|
|
||||||
public function __construct(private TagServiceInterface $tagService)
|
public function __construct(private readonly TagServiceInterface $tagService)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ use function sprintf;
|
|||||||
|
|
||||||
abstract class AbstractLockedCommand extends Command
|
abstract class AbstractLockedCommand extends Command
|
||||||
{
|
{
|
||||||
public function __construct(private LockFactory $locker)
|
public function __construct(private readonly LockFactory $locker)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
@ -22,11 +22,11 @@ abstract class AbstractLockedCommand extends Command
|
|||||||
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
{
|
{
|
||||||
$lockConfig = $this->getLockConfig();
|
$lockConfig = $this->getLockConfig();
|
||||||
$lock = $this->locker->createLock($lockConfig->lockName(), $lockConfig->ttl(), $lockConfig->isBlocking());
|
$lock = $this->locker->createLock($lockConfig->lockName, $lockConfig->ttl, $lockConfig->isBlocking);
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,9 @@ final class LockedCommandConfig
|
|||||||
public const DEFAULT_TTL = 600.0; // 10 minutes
|
public const DEFAULT_TTL = 600.0; // 10 minutes
|
||||||
|
|
||||||
private function __construct(
|
private function __construct(
|
||||||
private string $lockName,
|
public readonly string $lockName,
|
||||||
private bool $isBlocking,
|
public readonly bool $isBlocking,
|
||||||
private float $ttl = self::DEFAULT_TTL,
|
public readonly float $ttl = self::DEFAULT_TTL,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,19 +24,4 @@ final class LockedCommandConfig
|
|||||||
{
|
{
|
||||||
return new self($lockName, false);
|
return new self($lockName, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function lockName(): string
|
|
||||||
{
|
|
||||||
return $this->lockName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function isBlocking(): bool
|
|
||||||
{
|
|
||||||
return $this->isBlocking;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function ttl(): float
|
|
||||||
{
|
|
||||||
return $this->ttl;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
83
module/CLI/src/Command/Visit/AbstractVisitsListCommand.php
Normal file
83
module/CLI/src/Command/Visit/AbstractVisitsListCommand.php
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\ExitCodes;
|
||||||
|
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
use function array_keys;
|
||||||
|
use function Functional\map;
|
||||||
|
use function Functional\select_keys;
|
||||||
|
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||||
|
use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly;
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
abstract class AbstractVisitsListCommand extends AbstractWithDateRangeCommand
|
||||||
|
{
|
||||||
|
public function __construct(protected readonly VisitsStatsHelperInterface $visitsHelper)
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
final protected function getStartDateDesc(string $optionName): string
|
||||||
|
{
|
||||||
|
return sprintf('Allows to filter visits, returning only those older than "%s".', $optionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
final protected function getEndDateDesc(string $optionName): string
|
||||||
|
{
|
||||||
|
return sprintf('Allows to filter visits, returning only those newer than "%s".', $optionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
final protected function execute(InputInterface $input, OutputInterface $output): ?int
|
||||||
|
{
|
||||||
|
$startDate = $this->getStartDateOption($input, $output);
|
||||||
|
$endDate = $this->getEndDateOption($input, $output);
|
||||||
|
$paginator = $this->getVisitsPaginator($input, buildDateRange($startDate, $endDate));
|
||||||
|
[$rows, $headers] = $this->resolveRowsAndHeaders($paginator);
|
||||||
|
|
||||||
|
ShlinkTable::default($output)->render($headers, $rows);
|
||||||
|
|
||||||
|
return ExitCodes::EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveRowsAndHeaders(Paginator $paginator): array
|
||||||
|
{
|
||||||
|
$extraKeys = [];
|
||||||
|
$rows = map($paginator->getCurrentPageResults(), function (Visit $visit) use (&$extraKeys) {
|
||||||
|
$extraFields = $this->mapExtraFields($visit);
|
||||||
|
$extraKeys = array_keys($extraFields);
|
||||||
|
|
||||||
|
$rowData = [
|
||||||
|
...$visit->jsonSerialize(),
|
||||||
|
'country' => $visit->getVisitLocation()?->getCountryName() ?? 'Unknown',
|
||||||
|
'city' => $visit->getVisitLocation()?->getCityName() ?? 'Unknown',
|
||||||
|
...$extraFields,
|
||||||
|
];
|
||||||
|
|
||||||
|
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]);
|
||||||
|
});
|
||||||
|
$extra = map($extraKeys, camelCaseToHumanFriendly(...));
|
||||||
|
|
||||||
|
return [
|
||||||
|
$rows,
|
||||||
|
['Referer', 'Date', 'User agent', 'Country', 'City', ...$extra],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
abstract protected function mapExtraFields(Visit $visit): array;
|
||||||
|
}
|
46
module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php
Normal file
46
module/CLI/src/Command/Visit/GetNonOrphanVisitsCommand.php
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
|
||||||
|
class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand
|
||||||
|
{
|
||||||
|
public const NAME = 'visit:non-orphan';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
VisitsStatsHelperInterface $visitsHelper,
|
||||||
|
private readonly ShortUrlStringifierInterface $shortUrlStringifier,
|
||||||
|
) {
|
||||||
|
parent::__construct($visitsHelper);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function doConfigure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName(self::NAME)
|
||||||
|
->setDescription('Returns the list of non-orphan visits.');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||||
|
{
|
||||||
|
return $this->visitsHelper->nonOrphanVisits(new VisitsParams($dateRange));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function mapExtraFields(Visit $visit): array
|
||||||
|
{
|
||||||
|
$shortUrl = $visit->getShortUrl();
|
||||||
|
return $shortUrl === null ? [] : ['shortUrl' => $this->shortUrlStringifier->stringify($shortUrl)];
|
||||||
|
}
|
||||||
|
}
|
36
module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php
Normal file
36
module/CLI/src/Command/Visit/GetOrphanVisitsCommand.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Model\VisitsParams;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
|
||||||
|
class GetOrphanVisitsCommand extends AbstractVisitsListCommand
|
||||||
|
{
|
||||||
|
public const NAME = 'visit:orphan';
|
||||||
|
|
||||||
|
protected function doConfigure(): void
|
||||||
|
{
|
||||||
|
$this
|
||||||
|
->setName(self::NAME)
|
||||||
|
->setDescription('Returns the list of orphan visits.');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
|
||||||
|
{
|
||||||
|
return $this->visitsHelper->orphanVisits(new VisitsParams($dateRange));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function mapExtraFields(Visit $visit): array
|
||||||
|
{
|
||||||
|
return ['type' => $visit->type()->value];
|
||||||
|
}
|
||||||
|
}
|
@ -17,6 +17,7 @@ use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
|||||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||||
use Symfony\Component\Console\Exception\RuntimeException;
|
use Symfony\Component\Console\Exception\RuntimeException;
|
||||||
|
use Symfony\Component\Console\Input\ArrayInput;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
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;
|
||||||
@ -80,12 +81,12 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($all && $retry && ! $this->warnAndVerifyContinue($input)) {
|
if ($all && $retry && ! $this->warnAndVerifyContinue()) {
|
||||||
throw new RuntimeException('Execution aborted');
|
throw new RuntimeException('Execution aborted');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function warnAndVerifyContinue(InputInterface $input): bool
|
private function warnAndVerifyContinue(): bool
|
||||||
{
|
{
|
||||||
$this->io->warning([
|
$this->io->warning([
|
||||||
'You are about to process the location of all existing visits your short URLs received.',
|
'You are about to process the location of all existing visits your short URLs received.',
|
||||||
@ -103,7 +104,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
$all = $retry && $input->getOption('all');
|
$all = $retry && $input->getOption('all');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->checkDbUpdate($input);
|
$this->checkDbUpdate();
|
||||||
|
|
||||||
if ($all) {
|
if ($all) {
|
||||||
$this->visitLocator->locateAllVisits($this);
|
$this->visitLocator->locateAllVisits($this);
|
||||||
@ -166,7 +167,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
$this->io->writeln($message);
|
$this->io->writeln($message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function checkDbUpdate(InputInterface $input): void
|
private function checkDbUpdate(): void
|
||||||
{
|
{
|
||||||
$cliApp = $this->getApplication();
|
$cliApp = $this->getApplication();
|
||||||
if ($cliApp === null) {
|
if ($cliApp === null) {
|
||||||
@ -174,7 +175,7 @@ class LocateVisitsCommand extends AbstractLockedCommand implements VisitGeolocat
|
|||||||
}
|
}
|
||||||
|
|
||||||
$downloadDbCommand = $cliApp->find(DownloadGeoLiteDbCommand::NAME);
|
$downloadDbCommand = $cliApp->find(DownloadGeoLiteDbCommand::NAME);
|
||||||
$exitCode = $downloadDbCommand->run($input, $this->io);
|
$exitCode = $downloadDbCommand->run(new ArrayInput([]), $this->io);
|
||||||
|
|
||||||
if ($exitCode === ExitCodes::EXIT_FAILURE) {
|
if ($exitCode === ExitCodes::EXIT_FAILURE) {
|
||||||
throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.');
|
throw new RuntimeException('It is not possible to locate visits without a GeoLite2 db file.');
|
||||||
|
@ -16,7 +16,7 @@ class InvalidRoleConfigException extends InvalidArgumentException implements Exc
|
|||||||
return new self(sprintf(
|
return new self(sprintf(
|
||||||
'You cannot create an API key with the "%s" role attached to the default domain. '
|
'You cannot create an API key with the "%s" role attached to the default domain. '
|
||||||
. 'The role is currently limited to non-default domains.',
|
. 'The role is currently limited to non-default domains.',
|
||||||
Role::DOMAIN_SPECIFIC,
|
Role::DOMAIN_SPECIFIC->value,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,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 </>';
|
||||||
|
|
||||||
private function __construct(private Table $baseTable, private bool $withRowSeparators)
|
private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
|
||||||
|
|
||||||
|
use Pagerfanta\Adapter\ArrayAdapter;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Domain\GetDomainVisitsCommand;
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
|
class GetDomainVisitsCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use CliTestUtilsTrait;
|
||||||
|
|
||||||
|
private CommandTester $commandTester;
|
||||||
|
private ObjectProphecy $visitsHelper;
|
||||||
|
private ObjectProphecy $stringifier;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
|
||||||
|
$this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class);
|
||||||
|
|
||||||
|
$this->commandTester = $this->testerForCommand(
|
||||||
|
new GetDomainVisitsCommand($this->visitsHelper->reveal(), $this->stringifier->reveal()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function outputIsProperlyGenerated(): void
|
||||||
|
{
|
||||||
|
$shortUrl = ShortUrl::createEmpty();
|
||||||
|
$visit = Visit::forValidShortUrl($shortUrl, new Visitor('bar', 'foo', '', ''))->locate(
|
||||||
|
VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
|
||||||
|
);
|
||||||
|
$domain = 'doma.in';
|
||||||
|
$getVisits = $this->visitsHelper->visitsForDomain($domain, Argument::any())->willReturn(
|
||||||
|
new Paginator(new ArrayAdapter([$visit])),
|
||||||
|
);
|
||||||
|
$stringify = $this->stringifier->stringify($shortUrl)->willReturn('the_short_url');
|
||||||
|
|
||||||
|
$this->commandTester->execute(['domain' => $domain]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
self::assertEquals(
|
||||||
|
<<<OUTPUT
|
||||||
|
+---------+---------------------------+------------+---------+--------+---------------+
|
||||||
|
| Referer | Date | User agent | Country | City | Short Url |
|
||||||
|
+---------+---------------------------+------------+---------+--------+---------------+
|
||||||
|
| foo | {$visit->getDate()->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
||||||
|
+---------+---------------------------+------------+---------+--------+---------------+
|
||||||
|
|
||||||
|
OUTPUT,
|
||||||
|
$output,
|
||||||
|
);
|
||||||
|
$getVisits->shouldHaveBeenCalledOnce();
|
||||||
|
$stringify->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
|||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||||
|
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||||
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
@ -38,8 +39,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||||||
$command = new CreateShortUrlCommand(
|
$command = new CreateShortUrlCommand(
|
||||||
$this->urlShortener->reveal(),
|
$this->urlShortener->reveal(),
|
||||||
$this->stringifier->reveal(),
|
$this->stringifier->reveal(),
|
||||||
5,
|
new UrlShortenerOptions(['defaultShortCodesLength' => 5, 'domain' => ['hostname' => self::DEFAULT_DOMAIN]]),
|
||||||
self::DEFAULT_DOMAIN,
|
|
||||||
);
|
);
|
||||||
$this->commandTester = $this->testerForCommand($command);
|
$this->commandTester = $this->testerForCommand($command);
|
||||||
}
|
}
|
||||||
|
@ -36,10 +36,11 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
|
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$deleteByShortCode = $this->service->deleteByShortCode(new ShortUrlIdentifier($shortCode), false)->will(
|
$deleteByShortCode = $this->service->deleteByShortCode(
|
||||||
function (): void {
|
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
|
||||||
},
|
false,
|
||||||
);
|
)->will(function (): void {
|
||||||
|
});
|
||||||
|
|
||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
@ -55,7 +56,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
public function invalidShortCodePrintsMessage(): void
|
public function invalidShortCodePrintsMessage(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$identifier = new ShortUrlIdentifier($shortCode);
|
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
|
||||||
$deleteByShortCode = $this->service->deleteByShortCode($identifier, false)->willThrow(
|
$deleteByShortCode = $this->service->deleteByShortCode($identifier, false)->willThrow(
|
||||||
Exception\ShortUrlNotFoundException::fromNotFound($identifier),
|
Exception\ShortUrlNotFoundException::fromNotFound($identifier),
|
||||||
);
|
);
|
||||||
@ -77,7 +78,7 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
string $expectedMessage,
|
string $expectedMessage,
|
||||||
): void {
|
): void {
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$identifier = new ShortUrlIdentifier($shortCode);
|
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
|
||||||
$deleteByShortCode = $this->service->deleteByShortCode($identifier, Argument::type('bool'))->will(
|
$deleteByShortCode = $this->service->deleteByShortCode($identifier, Argument::type('bool'))->will(
|
||||||
function (array $args) use ($shortCode): void {
|
function (array $args) use ($shortCode): void {
|
||||||
$ignoreThreshold = array_pop($args);
|
$ignoreThreshold = array_pop($args);
|
||||||
@ -114,12 +115,13 @@ class DeleteShortUrlCommandTest extends TestCase
|
|||||||
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void
|
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void
|
||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$deleteByShortCode = $this->service->deleteByShortCode(new ShortUrlIdentifier($shortCode), false)->willThrow(
|
$deleteByShortCode = $this->service->deleteByShortCode(
|
||||||
Exception\DeleteShortUrlException::fromVisitsThreshold(
|
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
|
||||||
10,
|
false,
|
||||||
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
|
)->willThrow(Exception\DeleteShortUrlException::fromVisitsThreshold(
|
||||||
),
|
10,
|
||||||
);
|
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
|
||||||
|
));
|
||||||
$this->commandTester->setInputs(['no']);
|
$this->commandTester->setInputs(['no']);
|
||||||
|
|
||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
|
@ -9,7 +9,7 @@ use Pagerfanta\Adapter\ArrayAdapter;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
|
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetShortUrlVisitsCommand;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
@ -23,9 +23,10 @@ use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
|||||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
use Symfony\Component\Console\Tester\CommandTester;
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
|
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
|
|
||||||
class GetVisitsCommandTest extends TestCase
|
class GetShortUrlVisitsCommandTest extends TestCase
|
||||||
{
|
{
|
||||||
use CliTestUtilsTrait;
|
use CliTestUtilsTrait;
|
||||||
|
|
||||||
@ -35,7 +36,7 @@ class GetVisitsCommandTest extends TestCase
|
|||||||
public function setUp(): void
|
public function setUp(): void
|
||||||
{
|
{
|
||||||
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
|
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
|
||||||
$command = new GetVisitsCommand($this->visitsHelper->reveal());
|
$command = new GetShortUrlVisitsCommand($this->visitsHelper->reveal());
|
||||||
$this->commandTester = $this->testerForCommand($command);
|
$this->commandTester = $this->testerForCommand($command);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,8 +45,8 @@ class GetVisitsCommandTest extends TestCase
|
|||||||
{
|
{
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$this->visitsHelper->visitsForShortUrl(
|
$this->visitsHelper->visitsForShortUrl(
|
||||||
new ShortUrlIdentifier($shortCode),
|
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
|
||||||
new VisitsParams(DateRange::emptyInstance()),
|
new VisitsParams(DateRange::allTime()),
|
||||||
)
|
)
|
||||||
->willReturn(new Paginator(new ArrayAdapter([])))
|
->willReturn(new Paginator(new ArrayAdapter([])))
|
||||||
->shouldBeCalledOnce();
|
->shouldBeCalledOnce();
|
||||||
@ -60,8 +61,8 @@ class GetVisitsCommandTest extends TestCase
|
|||||||
$startDate = '2016-01-01';
|
$startDate = '2016-01-01';
|
||||||
$endDate = '2016-02-01';
|
$endDate = '2016-02-01';
|
||||||
$this->visitsHelper->visitsForShortUrl(
|
$this->visitsHelper->visitsForShortUrl(
|
||||||
new ShortUrlIdentifier($shortCode),
|
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
|
||||||
new VisitsParams(DateRange::withStartAndEndDate(Chronos::parse($startDate), Chronos::parse($endDate))),
|
new VisitsParams(buildDateRange(Chronos::parse($startDate), Chronos::parse($endDate))),
|
||||||
)
|
)
|
||||||
->willReturn(new Paginator(new ArrayAdapter([])))
|
->willReturn(new Paginator(new ArrayAdapter([])))
|
||||||
->shouldBeCalledOnce();
|
->shouldBeCalledOnce();
|
||||||
@ -79,8 +80,8 @@ class GetVisitsCommandTest extends TestCase
|
|||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$startDate = 'foo';
|
$startDate = 'foo';
|
||||||
$info = $this->visitsHelper->visitsForShortUrl(
|
$info = $this->visitsHelper->visitsForShortUrl(
|
||||||
new ShortUrlIdentifier($shortCode),
|
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
|
||||||
new VisitsParams(DateRange::emptyInstance()),
|
new VisitsParams(DateRange::allTime()),
|
||||||
)->willReturn(new Paginator(new ArrayAdapter([])));
|
)->willReturn(new Paginator(new ArrayAdapter([])));
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
@ -99,19 +100,30 @@ class GetVisitsCommandTest extends TestCase
|
|||||||
/** @test */
|
/** @test */
|
||||||
public function outputIsProperlyGenerated(): void
|
public function outputIsProperlyGenerated(): void
|
||||||
{
|
{
|
||||||
|
$visit = Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('bar', 'foo', '', ''))->locate(
|
||||||
|
VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
|
||||||
|
);
|
||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$this->visitsHelper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), Argument::any())->willReturn(
|
$this->visitsHelper->visitsForShortUrl(
|
||||||
new Paginator(new ArrayAdapter([
|
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
|
||||||
Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('bar', 'foo', '', ''))->locate(
|
Argument::any(),
|
||||||
VisitLocation::fromGeolocation(new Location('', 'Spain', '', '', 0, 0, '')),
|
)->willReturn(
|
||||||
),
|
new Paginator(new ArrayAdapter([$visit])),
|
||||||
])),
|
|
||||||
)->shouldBeCalledOnce();
|
)->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
self::assertStringContainsString('foo', $output);
|
|
||||||
self::assertStringContainsString('Spain', $output);
|
self::assertEquals(
|
||||||
self::assertStringContainsString('bar', $output);
|
<<<OUTPUT
|
||||||
|
+---------+---------------------------+------------+---------+--------+
|
||||||
|
| Referer | Date | User agent | Country | City |
|
||||||
|
+---------+---------------------------+------------+---------+--------+
|
||||||
|
| foo | {$visit->getDate()->toAtomString()} | bar | Spain | Madrid |
|
||||||
|
+---------+---------------------------+------------+---------+--------+
|
||||||
|
|
||||||
|
OUTPUT,
|
||||||
|
$output,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -16,6 +16,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
|||||||
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
@ -205,23 +206,23 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
|
|
||||||
public function provideArgs(): iterable
|
public function provideArgs(): iterable
|
||||||
{
|
{
|
||||||
yield [[], 1, null, [], ShortUrlsParams::TAGS_MODE_ANY];
|
yield [[], 1, null, [], TagsMode::ANY->value];
|
||||||
yield [['--page' => $page = 3], $page, null, [], ShortUrlsParams::TAGS_MODE_ANY];
|
yield [['--page' => $page = 3], $page, null, [], TagsMode::ANY->value];
|
||||||
yield [['--including-all-tags' => true], 1, null, [], ShortUrlsParams::TAGS_MODE_ALL];
|
yield [['--including-all-tags' => true], 1, null, [], TagsMode::ALL->value];
|
||||||
yield [['--search-term' => $searchTerm = 'search this'], 1, $searchTerm, [], ShortUrlsParams::TAGS_MODE_ANY];
|
yield [['--search-term' => $searchTerm = 'search this'], 1, $searchTerm, [], TagsMode::ANY->value];
|
||||||
yield [
|
yield [
|
||||||
['--page' => $page = 3, '--search-term' => $searchTerm = 'search this', '--tags' => $tags = 'foo,bar'],
|
['--page' => $page = 3, '--search-term' => $searchTerm = 'search this', '--tags' => $tags = 'foo,bar'],
|
||||||
$page,
|
$page,
|
||||||
$searchTerm,
|
$searchTerm,
|
||||||
explode(',', $tags),
|
explode(',', $tags),
|
||||||
ShortUrlsParams::TAGS_MODE_ANY,
|
TagsMode::ANY->value,
|
||||||
];
|
];
|
||||||
yield [
|
yield [
|
||||||
['--start-date' => $startDate = '2019-01-01'],
|
['--start-date' => $startDate = '2019-01-01'],
|
||||||
1,
|
1,
|
||||||
null,
|
null,
|
||||||
[],
|
[],
|
||||||
ShortUrlsParams::TAGS_MODE_ANY,
|
TagsMode::ANY->value,
|
||||||
$startDate,
|
$startDate,
|
||||||
];
|
];
|
||||||
yield [
|
yield [
|
||||||
@ -229,7 +230,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
1,
|
1,
|
||||||
null,
|
null,
|
||||||
[],
|
[],
|
||||||
ShortUrlsParams::TAGS_MODE_ANY,
|
TagsMode::ANY->value,
|
||||||
null,
|
null,
|
||||||
$endDate,
|
$endDate,
|
||||||
];
|
];
|
||||||
@ -238,7 +239,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
1,
|
1,
|
||||||
null,
|
null,
|
||||||
[],
|
[],
|
||||||
ShortUrlsParams::TAGS_MODE_ANY,
|
TagsMode::ANY->value,
|
||||||
$startDate,
|
$startDate,
|
||||||
$endDate,
|
$endDate,
|
||||||
];
|
];
|
||||||
@ -276,7 +277,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||||||
'page' => 1,
|
'page' => 1,
|
||||||
'searchTerm' => null,
|
'searchTerm' => null,
|
||||||
'tags' => [],
|
'tags' => [],
|
||||||
'tagsMode' => ShortUrlsParams::TAGS_MODE_ANY,
|
'tagsMode' => TagsMode::ANY->value,
|
||||||
'startDate' => null,
|
'startDate' => null,
|
||||||
'endDate' => null,
|
'endDate' => null,
|
||||||
'orderBy' => null,
|
'orderBy' => null,
|
||||||
|
@ -37,8 +37,9 @@ class ResolveUrlCommandTest extends TestCase
|
|||||||
$shortCode = 'abc123';
|
$shortCode = 'abc123';
|
||||||
$expectedUrl = 'http://domain.com/foo/bar';
|
$expectedUrl = 'http://domain.com/foo/bar';
|
||||||
$shortUrl = ShortUrl::withLongUrl($expectedUrl);
|
$shortUrl = ShortUrl::withLongUrl($expectedUrl);
|
||||||
$this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode))->willReturn($shortUrl)
|
$this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode))->willReturn(
|
||||||
->shouldBeCalledOnce();
|
$shortUrl,
|
||||||
|
)->shouldBeCalledOnce();
|
||||||
|
|
||||||
$this->commandTester->execute(['shortCode' => $shortCode]);
|
$this->commandTester->execute(['shortCode' => $shortCode]);
|
||||||
$output = $this->commandTester->getDisplay();
|
$output = $this->commandTester->getDisplay();
|
||||||
@ -48,8 +49,8 @@ class ResolveUrlCommandTest extends TestCase
|
|||||||
/** @test */
|
/** @test */
|
||||||
public function incorrectShortCodeOutputsErrorMessage(): void
|
public function incorrectShortCodeOutputsErrorMessage(): void
|
||||||
{
|
{
|
||||||
$identifier = new ShortUrlIdentifier('abc123');
|
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain('abc123');
|
||||||
$shortCode = $identifier->shortCode();
|
$shortCode = $identifier->shortCode;
|
||||||
|
|
||||||
$this->urlResolver->resolveShortUrl($identifier)
|
$this->urlResolver->resolveShortUrl($identifier)
|
||||||
->willThrow(ShortUrlNotFoundException::fromNotFound($identifier))
|
->willThrow(ShortUrlNotFoundException::fromNotFound($identifier))
|
||||||
|
71
module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php
Normal file
71
module/CLI/test/Command/Tag/GetTagVisitsCommandTest.php
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
|
||||||
|
|
||||||
|
use Pagerfanta\Adapter\ArrayAdapter;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Tag\GetTagVisitsCommand;
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
|
class GetTagVisitsCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use CliTestUtilsTrait;
|
||||||
|
|
||||||
|
private CommandTester $commandTester;
|
||||||
|
private ObjectProphecy $visitsHelper;
|
||||||
|
private ObjectProphecy $stringifier;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
|
||||||
|
$this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class);
|
||||||
|
|
||||||
|
$this->commandTester = $this->testerForCommand(
|
||||||
|
new GetTagVisitsCommand($this->visitsHelper->reveal(), $this->stringifier->reveal()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function outputIsProperlyGenerated(): void
|
||||||
|
{
|
||||||
|
$shortUrl = ShortUrl::createEmpty();
|
||||||
|
$visit = Visit::forValidShortUrl($shortUrl, new Visitor('bar', 'foo', '', ''))->locate(
|
||||||
|
VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
|
||||||
|
);
|
||||||
|
$tag = 'abc123';
|
||||||
|
$getVisits = $this->visitsHelper->visitsForTag($tag, Argument::any())->willReturn(
|
||||||
|
new Paginator(new ArrayAdapter([$visit])),
|
||||||
|
);
|
||||||
|
$stringify = $this->stringifier->stringify($shortUrl)->willReturn('the_short_url');
|
||||||
|
|
||||||
|
$this->commandTester->execute(['tag' => $tag]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
self::assertEquals(
|
||||||
|
<<<OUTPUT
|
||||||
|
+---------+---------------------------+------------+---------+--------+---------------+
|
||||||
|
| Referer | Date | User agent | Country | City | Short Url |
|
||||||
|
+---------+---------------------------+------------+---------+--------+---------------+
|
||||||
|
| foo | {$visit->getDate()->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
||||||
|
+---------+---------------------------+------------+---------+--------+---------------+
|
||||||
|
|
||||||
|
OUTPUT,
|
||||||
|
$output,
|
||||||
|
);
|
||||||
|
$getVisits->shouldHaveBeenCalledOnce();
|
||||||
|
$stringify->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use Pagerfanta\Adapter\ArrayAdapter;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Visit\GetNonOrphanVisitsCommand;
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
|
class GetNonOrphanVisitsCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use CliTestUtilsTrait;
|
||||||
|
|
||||||
|
private CommandTester $commandTester;
|
||||||
|
private ObjectProphecy $visitsHelper;
|
||||||
|
private ObjectProphecy $stringifier;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
|
||||||
|
$this->stringifier = $this->prophesize(ShortUrlStringifierInterface::class);
|
||||||
|
|
||||||
|
$this->commandTester = $this->testerForCommand(
|
||||||
|
new GetNonOrphanVisitsCommand($this->visitsHelper->reveal(), $this->stringifier->reveal()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function outputIsProperlyGenerated(): void
|
||||||
|
{
|
||||||
|
$shortUrl = ShortUrl::createEmpty();
|
||||||
|
$visit = Visit::forValidShortUrl($shortUrl, new Visitor('bar', 'foo', '', ''))->locate(
|
||||||
|
VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
|
||||||
|
);
|
||||||
|
$getVisits = $this->visitsHelper->nonOrphanVisits(Argument::any())->willReturn(
|
||||||
|
new Paginator(new ArrayAdapter([$visit])),
|
||||||
|
);
|
||||||
|
$stringify = $this->stringifier->stringify($shortUrl)->willReturn('the_short_url');
|
||||||
|
|
||||||
|
$this->commandTester->execute([]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
self::assertEquals(
|
||||||
|
<<<OUTPUT
|
||||||
|
+---------+---------------------------+------------+---------+--------+---------------+
|
||||||
|
| Referer | Date | User agent | Country | City | Short Url |
|
||||||
|
+---------+---------------------------+------------+---------+--------+---------------+
|
||||||
|
| foo | {$visit->getDate()->toAtomString()} | bar | Spain | Madrid | the_short_url |
|
||||||
|
+---------+---------------------------+------------+---------+--------+---------------+
|
||||||
|
|
||||||
|
OUTPUT,
|
||||||
|
$output,
|
||||||
|
);
|
||||||
|
$getVisits->shouldHaveBeenCalledOnce();
|
||||||
|
$stringify->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
}
|
60
module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php
Normal file
60
module/CLI/test/Command/Visit/GetOrphanVisitsCommandTest.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
|
use Pagerfanta\Adapter\ArrayAdapter;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Visit\GetOrphanVisitsCommand;
|
||||||
|
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Visit;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\VisitLocation;
|
||||||
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||||
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
|
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
|
||||||
|
class GetOrphanVisitsCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
use CliTestUtilsTrait;
|
||||||
|
|
||||||
|
private CommandTester $commandTester;
|
||||||
|
private ObjectProphecy $visitsHelper;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->visitsHelper = $this->prophesize(VisitsStatsHelperInterface::class);
|
||||||
|
$this->commandTester = $this->testerForCommand(new GetOrphanVisitsCommand($this->visitsHelper->reveal()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function outputIsProperlyGenerated(): void
|
||||||
|
{
|
||||||
|
$visit = Visit::forBasePath(new Visitor('bar', 'foo', '', ''))->locate(
|
||||||
|
VisitLocation::fromGeolocation(new Location('', 'Spain', '', 'Madrid', 0, 0, '')),
|
||||||
|
);
|
||||||
|
$getVisits = $this->visitsHelper->orphanVisits(Argument::any())->willReturn(
|
||||||
|
new Paginator(new ArrayAdapter([$visit])),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->commandTester->execute([]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
|
||||||
|
self::assertEquals(
|
||||||
|
<<<OUTPUT
|
||||||
|
+---------+---------------------------+------------+---------+--------+----------+
|
||||||
|
| Referer | Date | User agent | Country | City | Type |
|
||||||
|
+---------+---------------------------+------------+---------+--------+----------+
|
||||||
|
| foo | {$visit->getDate()->toAtomString()} | bar | Spain | Madrid | base_url |
|
||||||
|
+---------+---------------------------+------------+---------+--------+----------+
|
||||||
|
|
||||||
|
OUTPUT,
|
||||||
|
$output,
|
||||||
|
);
|
||||||
|
$getVisits->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ class InvalidRoleConfigExceptionTest extends TestCase
|
|||||||
self::assertEquals(sprintf(
|
self::assertEquals(sprintf(
|
||||||
'You cannot create an API key with the "%s" role attached to the default domain. '
|
'You cannot create an API key with the "%s" role attached to the default domain. '
|
||||||
. 'The role is currently limited to non-default domains.',
|
. 'The role is currently limited to non-default domains.',
|
||||||
Role::DOMAIN_SPECIFIC,
|
Role::DOMAIN_SPECIFIC->value,
|
||||||
), $e->getMessage());
|
), $e->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ return [
|
|||||||
Options\UrlShortenerOptions::class => ConfigAbstractFactory::class,
|
Options\UrlShortenerOptions::class => ConfigAbstractFactory::class,
|
||||||
Options\TrackingOptions::class => ConfigAbstractFactory::class,
|
Options\TrackingOptions::class => ConfigAbstractFactory::class,
|
||||||
Options\QrCodeOptions::class => ConfigAbstractFactory::class,
|
Options\QrCodeOptions::class => ConfigAbstractFactory::class,
|
||||||
|
Options\RabbitMqOptions::class => ConfigAbstractFactory::class,
|
||||||
Options\WebhookOptions::class => ConfigAbstractFactory::class,
|
Options\WebhookOptions::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Service\UrlShortener::class => ConfigAbstractFactory::class,
|
Service\UrlShortener::class => ConfigAbstractFactory::class,
|
||||||
@ -63,7 +64,7 @@ return [
|
|||||||
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
|
ShortUrl\Transformer\ShortUrlDataTransformer::class => ConfigAbstractFactory::class,
|
||||||
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
|
ShortUrl\Middleware\ExtraPathRedirectMiddleware::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Mercure\MercureUpdatesGenerator::class => ConfigAbstractFactory::class,
|
EventDispatcher\PublishingUpdatesGenerator::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
Importer\ImportedLinksProcessor::class => ConfigAbstractFactory::class,
|
Importer\ImportedLinksProcessor::class => ConfigAbstractFactory::class,
|
||||||
|
|
||||||
@ -91,6 +92,7 @@ return [
|
|||||||
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
Options\UrlShortenerOptions::class => ['config.url_shortener'],
|
||||||
Options\TrackingOptions::class => ['config.tracking'],
|
Options\TrackingOptions::class => ['config.tracking'],
|
||||||
Options\QrCodeOptions::class => ['config.qr_codes'],
|
Options\QrCodeOptions::class => ['config.qr_codes'],
|
||||||
|
Options\RabbitMqOptions::class => ['config.rabbitmq'],
|
||||||
Options\WebhookOptions::class => ['config.visits_webhooks'],
|
Options\WebhookOptions::class => ['config.visits_webhooks'],
|
||||||
|
|
||||||
Service\UrlShortener::class => [
|
Service\UrlShortener::class => [
|
||||||
@ -98,6 +100,7 @@ return [
|
|||||||
'em',
|
'em',
|
||||||
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
|
||||||
Service\ShortUrl\ShortCodeUniquenessHelper::class,
|
Service\ShortUrl\ShortCodeUniquenessHelper::class,
|
||||||
|
EventDispatcherInterface::class,
|
||||||
],
|
],
|
||||||
Visit\VisitsTracker::class => [
|
Visit\VisitsTracker::class => [
|
||||||
'em',
|
'em',
|
||||||
@ -157,7 +160,7 @@ return [
|
|||||||
Options\UrlShortenerOptions::class,
|
Options\UrlShortenerOptions::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
Mercure\MercureUpdatesGenerator::class => [
|
EventDispatcher\PublishingUpdatesGenerator::class => [
|
||||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||||
],
|
],
|
||||||
|
@ -6,9 +6,11 @@ namespace Shlinkio\Shlink\Core;
|
|||||||
|
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||||
|
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
|
||||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||||
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
|
use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType;
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||||
|
|
||||||
return static function (ClassMetadata $metadata, array $emConfig): void {
|
return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||||
$builder = new ClassMetadataBuilder($metadata);
|
$builder = new ClassMetadataBuilder($metadata);
|
||||||
@ -61,10 +63,13 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
|||||||
->nullable()
|
->nullable()
|
||||||
->build();
|
->build();
|
||||||
|
|
||||||
$builder->createField('type', Types::STRING)
|
(new FieldBuilder($builder, [
|
||||||
->columnName('type')
|
'fieldName' => 'type',
|
||||||
->length(255)
|
'type' => Types::STRING,
|
||||||
->build();
|
'enumType' => VisitType::class,
|
||||||
|
]))->columnName('type')
|
||||||
|
->length(255)
|
||||||
|
->build();
|
||||||
|
|
||||||
$builder->createField('potentialBot', Types::BOOLEAN)
|
$builder->createField('potentialBot', Types::BOOLEAN)
|
||||||
->columnName('potential_bot')
|
->columnName('potential_bot')
|
||||||
|
@ -5,12 +5,13 @@ declare(strict_types=1);
|
|||||||
namespace Shlinkio\Shlink\Core;
|
namespace Shlinkio\Shlink\Core;
|
||||||
|
|
||||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||||
use PhpAmqpLib\Connection\AMQPStreamConnection;
|
|
||||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||||
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
|
||||||
|
use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper;
|
||||||
|
use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper;
|
||||||
|
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper;
|
||||||
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 Symfony\Component\Mercure\Hub;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
@ -22,11 +23,17 @@ return [
|
|||||||
],
|
],
|
||||||
'async' => [
|
'async' => [
|
||||||
EventDispatcher\Event\VisitLocated::class => [
|
EventDispatcher\Event\VisitLocated::class => [
|
||||||
EventDispatcher\NotifyVisitToMercure::class,
|
EventDispatcher\Mercure\NotifyVisitToMercure::class,
|
||||||
EventDispatcher\NotifyVisitToRabbitMq::class,
|
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class,
|
||||||
|
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class,
|
||||||
EventDispatcher\NotifyVisitToWebHooks::class,
|
EventDispatcher\NotifyVisitToWebHooks::class,
|
||||||
EventDispatcher\UpdateGeoLiteDb::class,
|
EventDispatcher\UpdateGeoLiteDb::class,
|
||||||
],
|
],
|
||||||
|
EventDispatcher\Event\ShortUrlCreated::class => [
|
||||||
|
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class,
|
||||||
|
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class,
|
||||||
|
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class,
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
@ -34,16 +41,32 @@ return [
|
|||||||
'factories' => [
|
'factories' => [
|
||||||
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
||||||
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
||||||
EventDispatcher\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||||
EventDispatcher\NotifyVisitToRabbitMq::class => ConfigAbstractFactory::class,
|
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
|
||||||
|
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => ConfigAbstractFactory::class,
|
||||||
|
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => ConfigAbstractFactory::class,
|
||||||
|
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => ConfigAbstractFactory::class,
|
||||||
|
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => ConfigAbstractFactory::class,
|
||||||
EventDispatcher\UpdateGeoLiteDb::class => ConfigAbstractFactory::class,
|
EventDispatcher\UpdateGeoLiteDb::class => ConfigAbstractFactory::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
'delegators' => [
|
'delegators' => [
|
||||||
EventDispatcher\NotifyVisitToMercure::class => [
|
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||||
],
|
],
|
||||||
EventDispatcher\NotifyVisitToRabbitMq::class => [
|
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
|
||||||
|
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||||
|
],
|
||||||
|
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
|
||||||
|
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||||
|
],
|
||||||
|
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||||
|
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||||
|
],
|
||||||
|
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => [
|
||||||
|
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||||
|
],
|
||||||
|
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
||||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||||
],
|
],
|
||||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||||
@ -68,18 +91,46 @@ return [
|
|||||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||||
Options\AppOptions::class,
|
Options\AppOptions::class,
|
||||||
],
|
],
|
||||||
EventDispatcher\NotifyVisitToMercure::class => [
|
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||||
Hub::class,
|
MercureHubPublishingHelper::class,
|
||||||
Mercure\MercureUpdatesGenerator::class,
|
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||||
'em',
|
'em',
|
||||||
'Logger_Shlink',
|
'Logger_Shlink',
|
||||||
],
|
],
|
||||||
EventDispatcher\NotifyVisitToRabbitMq::class => [
|
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
|
||||||
AMQPStreamConnection::class,
|
MercureHubPublishingHelper::class,
|
||||||
|
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||||
|
'em',
|
||||||
|
'Logger_Shlink',
|
||||||
|
],
|
||||||
|
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
|
||||||
|
RabbitMqPublishingHelper::class,
|
||||||
|
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||||
'em',
|
'em',
|
||||||
'Logger_Shlink',
|
'Logger_Shlink',
|
||||||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||||
'config.rabbitmq.enabled',
|
Options\RabbitMqOptions::class,
|
||||||
|
],
|
||||||
|
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||||
|
RabbitMqPublishingHelper::class,
|
||||||
|
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||||
|
'em',
|
||||||
|
'Logger_Shlink',
|
||||||
|
Options\RabbitMqOptions::class,
|
||||||
|
],
|
||||||
|
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => [
|
||||||
|
RedisPublishingHelper::class,
|
||||||
|
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||||
|
'em',
|
||||||
|
'Logger_Shlink',
|
||||||
|
'config.redis.pub_sub_enabled',
|
||||||
|
],
|
||||||
|
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
||||||
|
RedisPublishingHelper::class,
|
||||||
|
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||||
|
'em',
|
||||||
|
'Logger_Shlink',
|
||||||
|
'config.redis.pub_sub_enabled',
|
||||||
],
|
],
|
||||||
EventDispatcher\UpdateGeoLiteDb::class => [GeolocationDbUpdater::class, 'Logger_Shlink'],
|
EventDispatcher\UpdateGeoLiteDb::class => [GeolocationDbUpdater::class, 'Logger_Shlink'],
|
||||||
],
|
],
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
use Fig\Http\Message\RequestMethodInterface as RequestMethod;
|
|
||||||
use RKA\Middleware\IpAddress;
|
|
||||||
use Shlinkio\Shlink\Core\Action;
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
'routes' => [
|
|
||||||
[
|
|
||||||
'name' => Action\RobotsAction::class,
|
|
||||||
'path' => '/robots.txt',
|
|
||||||
'middleware' => [
|
|
||||||
Action\RobotsAction::class,
|
|
||||||
],
|
|
||||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => Action\RedirectAction::class,
|
|
||||||
'path' => '/{shortCode}',
|
|
||||||
'middleware' => [
|
|
||||||
IpAddress::class,
|
|
||||||
Action\RedirectAction::class,
|
|
||||||
],
|
|
||||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => Action\PixelAction::class,
|
|
||||||
'path' => '/{shortCode}/track',
|
|
||||||
'middleware' => [
|
|
||||||
IpAddress::class,
|
|
||||||
Action\PixelAction::class,
|
|
||||||
],
|
|
||||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'name' => Action\QrCodeAction::class,
|
|
||||||
'path' => '/{shortCode}/qr-code',
|
|
||||||
'middleware' => [
|
|
||||||
Action\QrCodeAction::class,
|
|
||||||
],
|
|
||||||
'allowed_methods' => [RequestMethod::METHOD_GET],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
@ -8,6 +8,7 @@ use Cake\Chronos\Chronos;
|
|||||||
use DateTimeInterface;
|
use DateTimeInterface;
|
||||||
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
|
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
|
||||||
use Jaybizzle\CrawlerDetect\CrawlerDetect;
|
use Jaybizzle\CrawlerDetect\CrawlerDetect;
|
||||||
|
use Laminas\Filter\Word\CamelCaseToSeparator;
|
||||||
use Laminas\InputFilter\InputFilter;
|
use Laminas\InputFilter\InputFilter;
|
||||||
use PUGX\Shortid\Factory as ShortIdFactory;
|
use PUGX\Shortid\Factory as ShortIdFactory;
|
||||||
use Shlinkio\Shlink\Common\Util\DateRange;
|
use Shlinkio\Shlink\Common\Util\DateRange;
|
||||||
@ -19,6 +20,7 @@ use function print_r;
|
|||||||
use function Shlinkio\Shlink\Common\buildDateRange;
|
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||||
use function sprintf;
|
use function sprintf;
|
||||||
use function str_repeat;
|
use function str_repeat;
|
||||||
|
use function ucfirst;
|
||||||
|
|
||||||
function generateRandomShortCode(int $length): string
|
function generateRandomShortCode(int $length): string
|
||||||
{
|
{
|
||||||
@ -115,3 +117,13 @@ function fieldWithUtf8Charset(FieldBuilder $field, array $emConfig, string $coll
|
|||||||
default => $field,
|
default => $field,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function camelCaseToHumanFriendly(string $value): string
|
||||||
|
{
|
||||||
|
static $filter;
|
||||||
|
if ($filter === null) {
|
||||||
|
$filter = new CamelCaseToSeparator(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ucfirst($filter->filter($value));
|
||||||
|
}
|
||||||
|
@ -29,11 +29,11 @@ final class QrCodeParams
|
|||||||
private const SUPPORTED_FORMATS = ['png', 'svg'];
|
private const SUPPORTED_FORMATS = ['png', 'svg'];
|
||||||
|
|
||||||
private function __construct(
|
private function __construct(
|
||||||
private int $size,
|
public readonly int $size,
|
||||||
private int $margin,
|
public readonly int $margin,
|
||||||
private WriterInterface $writer,
|
public readonly WriterInterface $writer,
|
||||||
private ErrorCorrectionLevelInterface $errorCorrectionLevel,
|
public readonly ErrorCorrectionLevelInterface $errorCorrectionLevel,
|
||||||
private RoundBlockSizeModeInterface $roundBlockSizeMode,
|
public readonly RoundBlockSizeModeInterface $roundBlockSizeMode,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,29 +105,4 @@ final class QrCodeParams
|
|||||||
{
|
{
|
||||||
return strtolower(trim($param));
|
return strtolower(trim($param));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function size(): int
|
|
||||||
{
|
|
||||||
return $this->size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function margin(): int
|
|
||||||
{
|
|
||||||
return $this->margin;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function writer(): WriterInterface
|
|
||||||
{
|
|
||||||
return $this->writer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function errorCorrectionLevel(): ErrorCorrectionLevelInterface
|
|
||||||
{
|
|
||||||
return $this->errorCorrectionLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function roundBlockSizeMode(): RoundBlockSizeModeInterface
|
|
||||||
{
|
|
||||||
return $this->roundBlockSizeMode;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -42,11 +42,11 @@ class QrCodeAction implements MiddlewareInterface
|
|||||||
$params = QrCodeParams::fromRequest($request, $this->defaultOptions);
|
$params = QrCodeParams::fromRequest($request, $this->defaultOptions);
|
||||||
$qrCodeBuilder = Builder::create()
|
$qrCodeBuilder = Builder::create()
|
||||||
->data($this->stringifier->stringify($shortUrl))
|
->data($this->stringifier->stringify($shortUrl))
|
||||||
->size($params->size())
|
->size($params->size)
|
||||||
->margin($params->margin())
|
->margin($params->margin)
|
||||||
->writer($params->writer())
|
->writer($params->writer)
|
||||||
->errorCorrectionLevel($params->errorCorrectionLevel())
|
->errorCorrectionLevel($params->errorCorrectionLevel)
|
||||||
->roundBlockSizeMode($params->roundBlockSizeMode());
|
->roundBlockSizeMode($params->roundBlockSizeMode);
|
||||||
|
|
||||||
return new QrCodeResponse($qrCodeBuilder->build());
|
return new QrCodeResponse($qrCodeBuilder->build());
|
||||||
}
|
}
|
||||||
|
@ -4,153 +4,70 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Config;
|
namespace Shlinkio\Shlink\Core\Config;
|
||||||
|
|
||||||
use ReflectionClass;
|
|
||||||
use ReflectionClassConstant;
|
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
|
||||||
|
|
||||||
use function array_values;
|
|
||||||
use function Functional\contains;
|
|
||||||
use function Shlinkio\Shlink\Config\env;
|
use function Shlinkio\Shlink\Config\env;
|
||||||
|
|
||||||
// TODO Convert to enum after dropping PHP 8.0 support
|
enum EnvVars: string
|
||||||
|
|
||||||
/**
|
|
||||||
* @method static EnvVars DELETE_SHORT_URL_THRESHOLD()
|
|
||||||
* @method static EnvVars DB_DRIVER()
|
|
||||||
* @method static EnvVars DB_NAME()
|
|
||||||
* @method static EnvVars DB_USER()
|
|
||||||
* @method static EnvVars DB_PASSWORD()
|
|
||||||
* @method static EnvVars DB_HOST()
|
|
||||||
* @method static EnvVars DB_UNIX_SOCKET()
|
|
||||||
* @method static EnvVars DB_PORT()
|
|
||||||
* @method static EnvVars GEOLITE_LICENSE_KEY()
|
|
||||||
* @method static EnvVars REDIS_SERVERS()
|
|
||||||
* @method static EnvVars REDIS_SENTINEL_SERVICE()
|
|
||||||
* @method static EnvVars MERCURE_PUBLIC_HUB_URL()
|
|
||||||
* @method static EnvVars MERCURE_INTERNAL_HUB_URL()
|
|
||||||
* @method static EnvVars MERCURE_JWT_SECRET()
|
|
||||||
* @method static EnvVars DEFAULT_QR_CODE_SIZE()
|
|
||||||
* @method static EnvVars DEFAULT_QR_CODE_MARGIN()
|
|
||||||
* @method static EnvVars DEFAULT_QR_CODE_FORMAT()
|
|
||||||
* @method static EnvVars DEFAULT_QR_CODE_ERROR_CORRECTION()
|
|
||||||
* @method static EnvVars DEFAULT_QR_CODE_ROUND_BLOCK_SIZE()
|
|
||||||
* @method static EnvVars RABBITMQ_ENABLED()
|
|
||||||
* @method static EnvVars RABBITMQ_HOST()
|
|
||||||
* @method static EnvVars RABBITMQ_PORT()
|
|
||||||
* @method static EnvVars RABBITMQ_USER()
|
|
||||||
* @method static EnvVars RABBITMQ_PASSWORD()
|
|
||||||
* @method static EnvVars RABBITMQ_VHOST()
|
|
||||||
* @method static EnvVars DEFAULT_INVALID_SHORT_URL_REDIRECT()
|
|
||||||
* @method static EnvVars DEFAULT_REGULAR_404_REDIRECT()
|
|
||||||
* @method static EnvVars DEFAULT_BASE_URL_REDIRECT()
|
|
||||||
* @method static EnvVars REDIRECT_STATUS_CODE()
|
|
||||||
* @method static EnvVars REDIRECT_CACHE_LIFETIME()
|
|
||||||
* @method static EnvVars BASE_PATH()
|
|
||||||
* @method static EnvVars PORT()
|
|
||||||
* @method static EnvVars TASK_WORKER_NUM()
|
|
||||||
* @method static EnvVars WEB_WORKER_NUM()
|
|
||||||
* @method static EnvVars ANONYMIZE_REMOTE_ADDR()
|
|
||||||
* @method static EnvVars TRACK_ORPHAN_VISITS()
|
|
||||||
* @method static EnvVars DISABLE_TRACK_PARAM()
|
|
||||||
* @method static EnvVars DISABLE_TRACKING()
|
|
||||||
* @method static EnvVars DISABLE_IP_TRACKING()
|
|
||||||
* @method static EnvVars DISABLE_REFERRER_TRACKING()
|
|
||||||
* @method static EnvVars DISABLE_UA_TRACKING()
|
|
||||||
* @method static EnvVars DISABLE_TRACKING_FROM()
|
|
||||||
* @method static EnvVars DEFAULT_SHORT_CODES_LENGTH()
|
|
||||||
* @method static EnvVars IS_HTTPS_ENABLED()
|
|
||||||
* @method static EnvVars DEFAULT_DOMAIN()
|
|
||||||
* @method static EnvVars AUTO_RESOLVE_TITLES()
|
|
||||||
* @method static EnvVars REDIRECT_APPEND_EXTRA_PATH()
|
|
||||||
* @method static EnvVars TIMEZONE()
|
|
||||||
* @method static EnvVars VISITS_WEBHOOKS()
|
|
||||||
* @method static EnvVars NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS()
|
|
||||||
*/
|
|
||||||
final class EnvVars
|
|
||||||
{
|
{
|
||||||
public const DELETE_SHORT_URL_THRESHOLD = 'DELETE_SHORT_URL_THRESHOLD';
|
case DELETE_SHORT_URL_THRESHOLD = 'DELETE_SHORT_URL_THRESHOLD';
|
||||||
public const DB_DRIVER = 'DB_DRIVER';
|
case DB_DRIVER = 'DB_DRIVER';
|
||||||
public const DB_NAME = 'DB_NAME';
|
case DB_NAME = 'DB_NAME';
|
||||||
public const DB_USER = 'DB_USER';
|
case DB_USER = 'DB_USER';
|
||||||
public const DB_PASSWORD = 'DB_PASSWORD';
|
case DB_PASSWORD = 'DB_PASSWORD';
|
||||||
public const DB_HOST = 'DB_HOST';
|
case DB_HOST = 'DB_HOST';
|
||||||
public const DB_UNIX_SOCKET = 'DB_UNIX_SOCKET';
|
case DB_UNIX_SOCKET = 'DB_UNIX_SOCKET';
|
||||||
public const DB_PORT = 'DB_PORT';
|
case DB_PORT = 'DB_PORT';
|
||||||
public const GEOLITE_LICENSE_KEY = 'GEOLITE_LICENSE_KEY';
|
case GEOLITE_LICENSE_KEY = 'GEOLITE_LICENSE_KEY';
|
||||||
public const REDIS_SERVERS = 'REDIS_SERVERS';
|
case REDIS_SERVERS = 'REDIS_SERVERS';
|
||||||
public const REDIS_SENTINEL_SERVICE = 'REDIS_SENTINEL_SERVICE';
|
case REDIS_SENTINEL_SERVICE = 'REDIS_SENTINEL_SERVICE';
|
||||||
public const MERCURE_PUBLIC_HUB_URL = 'MERCURE_PUBLIC_HUB_URL';
|
case REDIS_PUB_SUB_ENABLED = 'REDIS_PUB_SUB_ENABLED';
|
||||||
public const MERCURE_INTERNAL_HUB_URL = 'MERCURE_INTERNAL_HUB_URL';
|
case MERCURE_PUBLIC_HUB_URL = 'MERCURE_PUBLIC_HUB_URL';
|
||||||
public const MERCURE_JWT_SECRET = 'MERCURE_JWT_SECRET';
|
case MERCURE_INTERNAL_HUB_URL = 'MERCURE_INTERNAL_HUB_URL';
|
||||||
public const DEFAULT_QR_CODE_SIZE = 'DEFAULT_QR_CODE_SIZE';
|
case MERCURE_JWT_SECRET = 'MERCURE_JWT_SECRET';
|
||||||
public const DEFAULT_QR_CODE_MARGIN = 'DEFAULT_QR_CODE_MARGIN';
|
case DEFAULT_QR_CODE_SIZE = 'DEFAULT_QR_CODE_SIZE';
|
||||||
public const DEFAULT_QR_CODE_FORMAT = 'DEFAULT_QR_CODE_FORMAT';
|
case DEFAULT_QR_CODE_MARGIN = 'DEFAULT_QR_CODE_MARGIN';
|
||||||
public const DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION';
|
case DEFAULT_QR_CODE_FORMAT = 'DEFAULT_QR_CODE_FORMAT';
|
||||||
public const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE';
|
case DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION';
|
||||||
public const RABBITMQ_ENABLED = 'RABBITMQ_ENABLED';
|
case DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE';
|
||||||
public const RABBITMQ_HOST = 'RABBITMQ_HOST';
|
case RABBITMQ_ENABLED = 'RABBITMQ_ENABLED';
|
||||||
public const RABBITMQ_PORT = 'RABBITMQ_PORT';
|
case RABBITMQ_HOST = 'RABBITMQ_HOST';
|
||||||
public const RABBITMQ_USER = 'RABBITMQ_USER';
|
case RABBITMQ_PORT = 'RABBITMQ_PORT';
|
||||||
public const RABBITMQ_PASSWORD = 'RABBITMQ_PASSWORD';
|
case RABBITMQ_USER = 'RABBITMQ_USER';
|
||||||
public const RABBITMQ_VHOST = 'RABBITMQ_VHOST';
|
case RABBITMQ_PASSWORD = 'RABBITMQ_PASSWORD';
|
||||||
public const DEFAULT_INVALID_SHORT_URL_REDIRECT = 'DEFAULT_INVALID_SHORT_URL_REDIRECT';
|
case RABBITMQ_VHOST = 'RABBITMQ_VHOST';
|
||||||
public const DEFAULT_REGULAR_404_REDIRECT = 'DEFAULT_REGULAR_404_REDIRECT';
|
|
||||||
public const DEFAULT_BASE_URL_REDIRECT = 'DEFAULT_BASE_URL_REDIRECT';
|
|
||||||
public const REDIRECT_STATUS_CODE = 'REDIRECT_STATUS_CODE';
|
|
||||||
public const REDIRECT_CACHE_LIFETIME = 'REDIRECT_CACHE_LIFETIME';
|
|
||||||
public const BASE_PATH = 'BASE_PATH';
|
|
||||||
public const PORT = 'PORT';
|
|
||||||
public const TASK_WORKER_NUM = 'TASK_WORKER_NUM';
|
|
||||||
public const WEB_WORKER_NUM = 'WEB_WORKER_NUM';
|
|
||||||
public const ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR';
|
|
||||||
public const TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS';
|
|
||||||
public const DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM';
|
|
||||||
public const DISABLE_TRACKING = 'DISABLE_TRACKING';
|
|
||||||
public const DISABLE_IP_TRACKING = 'DISABLE_IP_TRACKING';
|
|
||||||
public const DISABLE_REFERRER_TRACKING = 'DISABLE_REFERRER_TRACKING';
|
|
||||||
public const DISABLE_UA_TRACKING = 'DISABLE_UA_TRACKING';
|
|
||||||
public const DISABLE_TRACKING_FROM = 'DISABLE_TRACKING_FROM';
|
|
||||||
public const DEFAULT_SHORT_CODES_LENGTH = 'DEFAULT_SHORT_CODES_LENGTH';
|
|
||||||
public const IS_HTTPS_ENABLED = 'IS_HTTPS_ENABLED';
|
|
||||||
public const DEFAULT_DOMAIN = 'DEFAULT_DOMAIN';
|
|
||||||
public const AUTO_RESOLVE_TITLES = 'AUTO_RESOLVE_TITLES';
|
|
||||||
public const REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH';
|
|
||||||
public const TIMEZONE = 'TIMEZONE';
|
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
public const VISITS_WEBHOOKS = 'VISITS_WEBHOOKS';
|
case RABBITMQ_LEGACY_VISITS_PUBLISHING = 'RABBITMQ_LEGACY_VISITS_PUBLISHING';
|
||||||
|
case DEFAULT_INVALID_SHORT_URL_REDIRECT = 'DEFAULT_INVALID_SHORT_URL_REDIRECT';
|
||||||
|
case DEFAULT_REGULAR_404_REDIRECT = 'DEFAULT_REGULAR_404_REDIRECT';
|
||||||
|
case DEFAULT_BASE_URL_REDIRECT = 'DEFAULT_BASE_URL_REDIRECT';
|
||||||
|
case REDIRECT_STATUS_CODE = 'REDIRECT_STATUS_CODE';
|
||||||
|
case REDIRECT_CACHE_LIFETIME = 'REDIRECT_CACHE_LIFETIME';
|
||||||
|
case BASE_PATH = 'BASE_PATH';
|
||||||
|
case PORT = 'PORT';
|
||||||
|
case TASK_WORKER_NUM = 'TASK_WORKER_NUM';
|
||||||
|
case WEB_WORKER_NUM = 'WEB_WORKER_NUM';
|
||||||
|
case ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR';
|
||||||
|
case TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS';
|
||||||
|
case DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM';
|
||||||
|
case DISABLE_TRACKING = 'DISABLE_TRACKING';
|
||||||
|
case DISABLE_IP_TRACKING = 'DISABLE_IP_TRACKING';
|
||||||
|
case DISABLE_REFERRER_TRACKING = 'DISABLE_REFERRER_TRACKING';
|
||||||
|
case DISABLE_UA_TRACKING = 'DISABLE_UA_TRACKING';
|
||||||
|
case DISABLE_TRACKING_FROM = 'DISABLE_TRACKING_FROM';
|
||||||
|
case DEFAULT_SHORT_CODES_LENGTH = 'DEFAULT_SHORT_CODES_LENGTH';
|
||||||
|
case IS_HTTPS_ENABLED = 'IS_HTTPS_ENABLED';
|
||||||
|
case DEFAULT_DOMAIN = 'DEFAULT_DOMAIN';
|
||||||
|
case AUTO_RESOLVE_TITLES = 'AUTO_RESOLVE_TITLES';
|
||||||
|
case REDIRECT_APPEND_EXTRA_PATH = 'REDIRECT_APPEND_EXTRA_PATH';
|
||||||
|
case TIMEZONE = 'TIMEZONE';
|
||||||
|
case MULTI_SEGMENT_SLUGS_ENABLED = 'MULTI_SEGMENT_SLUGS_ENABLED';
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
public const NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS = 'NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS';
|
case VISITS_WEBHOOKS = 'VISITS_WEBHOOKS';
|
||||||
|
/** @deprecated */
|
||||||
/**
|
case NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS = 'NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS';
|
||||||
* @return string[]
|
|
||||||
*/
|
|
||||||
public static function cases(): array
|
|
||||||
{
|
|
||||||
static $constants;
|
|
||||||
if ($constants !== null) {
|
|
||||||
return $constants;
|
|
||||||
}
|
|
||||||
|
|
||||||
$ref = new ReflectionClass(self::class);
|
|
||||||
return $constants = array_values($ref->getConstants(ReflectionClassConstant::IS_PUBLIC));
|
|
||||||
}
|
|
||||||
|
|
||||||
private function __construct(private string $envVar)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function __callStatic(string $name, array $arguments): self
|
|
||||||
{
|
|
||||||
if (! contains(self::cases(), $name)) {
|
|
||||||
throw new InvalidArgumentException('Invalid env var: "' . $name . '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
return new self($name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function loadFromEnv(mixed $default = null): mixed
|
public function loadFromEnv(mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
return env($this->envVar, $default);
|
return env($this->value, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function existsInEnv(): bool
|
public function existsInEnv(): bool
|
||||||
|
@ -13,7 +13,9 @@ use Shlinkio\Shlink\Core\ErrorHandler\Model\NotFoundType;
|
|||||||
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||||
|
|
||||||
use function Functional\compose;
|
use function Functional\compose;
|
||||||
|
use function Functional\id;
|
||||||
use function str_replace;
|
use function str_replace;
|
||||||
|
use function urlencode;
|
||||||
|
|
||||||
class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
|
class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
|
||||||
{
|
{
|
||||||
@ -71,10 +73,10 @@ class NotFoundRedirectResolver implements NotFoundRedirectResolverInterface
|
|||||||
$replacePlaceholderForPattern(self::ORIGINAL_PATH_PLACEHOLDER, $path, $modifier),
|
$replacePlaceholderForPattern(self::ORIGINAL_PATH_PLACEHOLDER, $path, $modifier),
|
||||||
);
|
);
|
||||||
$replacePlaceholdersInPath = compose(
|
$replacePlaceholdersInPath = compose(
|
||||||
$replacePlaceholders('\Functional\id'),
|
$replacePlaceholders(id(...)),
|
||||||
static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path), // Fix duplicated bars
|
static fn (?string $path) => $path === null ? null : str_replace('//', '/', $path),
|
||||||
);
|
);
|
||||||
$replacePlaceholdersInQuery = $replacePlaceholders('\urlencode');
|
$replacePlaceholdersInQuery = $replacePlaceholders(urlencode(...));
|
||||||
|
|
||||||
return $redirectUri
|
return $redirectUri
|
||||||
->withPath($replacePlaceholdersInPath($redirectUri->getPath()))
|
->withPath($replacePlaceholdersInPath($redirectUri->getPath()))
|
||||||
|
@ -9,9 +9,9 @@ use JsonSerializable;
|
|||||||
final class NotFoundRedirects implements JsonSerializable
|
final class NotFoundRedirects implements JsonSerializable
|
||||||
{
|
{
|
||||||
private function __construct(
|
private function __construct(
|
||||||
private ?string $baseUrlRedirect,
|
public readonly ?string $baseUrlRedirect,
|
||||||
private ?string $regular404Redirect,
|
public readonly ?string $regular404Redirect,
|
||||||
private ?string $invalidShortUrlRedirect,
|
public readonly ?string $invalidShortUrlRedirect,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,21 +33,6 @@ final class NotFoundRedirects implements JsonSerializable
|
|||||||
return new self($config->baseUrlRedirect(), $config->regular404Redirect(), $config->invalidShortUrlRedirect());
|
return new self($config->baseUrlRedirect(), $config->regular404Redirect(), $config->invalidShortUrlRedirect());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function baseUrlRedirect(): ?string
|
|
||||||
{
|
|
||||||
return $this->baseUrlRedirect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function regular404Redirect(): ?string
|
|
||||||
{
|
|
||||||
return $this->regular404Redirect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function invalidShortUrlRedirect(): ?string
|
|
||||||
{
|
|
||||||
return $this->invalidShortUrlRedirect;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function jsonSerialize(): array
|
public function jsonSerialize(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
@ -12,9 +12,9 @@ use Shlinkio\Shlink\Core\Entity\Domain;
|
|||||||
final class DomainItem implements JsonSerializable
|
final class DomainItem implements JsonSerializable
|
||||||
{
|
{
|
||||||
private function __construct(
|
private function __construct(
|
||||||
private string $authority,
|
private readonly string $authority,
|
||||||
private NotFoundRedirectConfigInterface $notFoundRedirectConfig,
|
public readonly NotFoundRedirectConfigInterface $notFoundRedirectConfig,
|
||||||
private bool $isDefault,
|
public readonly bool $isDefault,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -23,9 +23,9 @@ final class DomainItem implements JsonSerializable
|
|||||||
return new self($domain->getAuthority(), $domain, false);
|
return new self($domain->getAuthority(), $domain, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function forDefaultDomain(string $authority, NotFoundRedirectConfigInterface $config): self
|
public static function forDefaultDomain(string $defaultDomain, NotFoundRedirectConfigInterface $config): self
|
||||||
{
|
{
|
||||||
return new self($authority, $config, true);
|
return new self($defaultDomain, $config, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function jsonSerialize(): array
|
public function jsonSerialize(): array
|
||||||
@ -41,14 +41,4 @@ final class DomainItem implements JsonSerializable
|
|||||||
{
|
{
|
||||||
return $this->authority;
|
return $this->authority;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isDefault(): bool
|
|
||||||
{
|
|
||||||
return $this->isDefault;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function notFoundRedirectConfig(): NotFoundRedirectConfigInterface
|
|
||||||
{
|
|
||||||
return $this->notFoundRedirectConfig;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,6 @@ namespace Shlinkio\Shlink\Core\Domain\Repository;
|
|||||||
use Doctrine\ORM\Query\Expr\Join;
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
|
||||||
use Happyr\DoctrineSpecification\Spec;
|
|
||||||
use Shlinkio\Shlink\Core\Domain\Spec\IsDomain;
|
use Shlinkio\Shlink\Core\Domain\Spec\IsDomain;
|
||||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
@ -77,10 +76,9 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe
|
|||||||
// FIXME The $apiKey->spec() method cannot be used here, as it returns a single spec which assumes the
|
// FIXME The $apiKey->spec() method cannot be used here, as it returns a single spec which assumes the
|
||||||
// ShortUrl is the root entity. Here, the Domain is the root entity.
|
// ShortUrl is the root entity. Here, the Domain is the root entity.
|
||||||
// Think on a way to centralize the conditional behavior and make $apiKey->spec() more flexible.
|
// Think on a way to centralize the conditional behavior and make $apiKey->spec() more flexible.
|
||||||
yield from $apiKey?->mapRoles(fn (string $roleName, array $meta) => match ($roleName) {
|
yield from $apiKey?->mapRoles(fn (Role $role, array $meta) => match ($role) {
|
||||||
Role::DOMAIN_SPECIFIC => ['d', new IsDomain(Role::domainIdFromMeta($meta))],
|
Role::DOMAIN_SPECIFIC => ['d', new IsDomain(Role::domainIdFromMeta($meta))],
|
||||||
Role::AUTHORED_SHORT_URLS => ['s', new BelongsToApiKey($apiKey)],
|
Role::AUTHORED_SHORT_URLS => ['s', new BelongsToApiKey($apiKey)],
|
||||||
default => [null, Spec::andX()],
|
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,8 +66,8 @@ class Domain extends AbstractEntity implements JsonSerializable, NotFoundRedirec
|
|||||||
|
|
||||||
public function configureNotFoundRedirects(NotFoundRedirects $redirects): void
|
public function configureNotFoundRedirects(NotFoundRedirects $redirects): void
|
||||||
{
|
{
|
||||||
$this->baseUrlRedirect = $redirects->baseUrlRedirect();
|
$this->baseUrlRedirect = $redirects->baseUrlRedirect;
|
||||||
$this->regular404Redirect = $redirects->regular404Redirect();
|
$this->regular404Redirect = $redirects->regular404Redirect;
|
||||||
$this->invalidShortUrlRedirect = $redirects->invalidShortUrlRedirect();
|
$this->invalidShortUrlRedirect = $redirects->invalidShortUrlRedirect;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
|||||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
|
use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
|
||||||
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
|
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
|
||||||
|
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||||
|
|
||||||
@ -174,7 +175,7 @@ class ShortUrl extends AbstractEntity
|
|||||||
{
|
{
|
||||||
/** @var Selectable $visits */
|
/** @var Selectable $visits */
|
||||||
$visits = $this->visits;
|
$visits = $this->visits;
|
||||||
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', Visit::TYPE_IMPORTED))
|
$criteria = Criteria::create()->where(Criteria::expr()->eq('type', VisitType::IMPORTED))
|
||||||
->orderBy(['id' => 'DESC'])
|
->orderBy(['id' => 'DESC'])
|
||||||
->setMaxResults(1);
|
->setMaxResults(1);
|
||||||
|
|
||||||
|
@ -10,30 +10,24 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
|||||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||||
use Shlinkio\Shlink\Common\Util\IpAddress;
|
use Shlinkio\Shlink\Common\Util\IpAddress;
|
||||||
use Shlinkio\Shlink\Core\Model\Visitor;
|
use Shlinkio\Shlink\Core\Model\Visitor;
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface;
|
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisit;
|
use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisit;
|
||||||
|
|
||||||
use function Shlinkio\Shlink\Core\isCrawler;
|
use function Shlinkio\Shlink\Core\isCrawler;
|
||||||
|
|
||||||
class Visit extends AbstractEntity implements JsonSerializable
|
class Visit extends AbstractEntity implements JsonSerializable
|
||||||
{
|
{
|
||||||
public const TYPE_VALID_SHORT_URL = 'valid_short_url';
|
|
||||||
public const TYPE_IMPORTED = 'imported';
|
|
||||||
public const TYPE_INVALID_SHORT_URL = 'invalid_short_url';
|
|
||||||
public const TYPE_BASE_URL = 'base_url';
|
|
||||||
public const TYPE_REGULAR_404 = 'regular_404';
|
|
||||||
|
|
||||||
private string $referer;
|
private string $referer;
|
||||||
private Chronos $date;
|
private Chronos $date;
|
||||||
private ?string $remoteAddr = null;
|
private ?string $remoteAddr = null;
|
||||||
private ?string $visitedUrl = null;
|
private ?string $visitedUrl = null;
|
||||||
private string $userAgent;
|
private string $userAgent;
|
||||||
private string $type;
|
private VisitType $type;
|
||||||
private ?ShortUrl $shortUrl;
|
private ?ShortUrl $shortUrl;
|
||||||
private ?VisitLocation $visitLocation = null;
|
private ?VisitLocation $visitLocation = null;
|
||||||
private bool $potentialBot;
|
private bool $potentialBot;
|
||||||
|
|
||||||
private function __construct(?ShortUrl $shortUrl, string $type)
|
private function __construct(?ShortUrl $shortUrl, VisitType $type)
|
||||||
{
|
{
|
||||||
$this->shortUrl = $shortUrl;
|
$this->shortUrl = $shortUrl;
|
||||||
$this->date = Chronos::now();
|
$this->date = Chronos::now();
|
||||||
@ -42,7 +36,7 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
|
|
||||||
public static function forValidShortUrl(ShortUrl $shortUrl, Visitor $visitor, bool $anonymize = true): self
|
public static function forValidShortUrl(ShortUrl $shortUrl, Visitor $visitor, bool $anonymize = true): self
|
||||||
{
|
{
|
||||||
$instance = new self($shortUrl, self::TYPE_VALID_SHORT_URL);
|
$instance = new self($shortUrl, VisitType::VALID_SHORT_URL);
|
||||||
$instance->hydrateFromVisitor($visitor, $anonymize);
|
$instance->hydrateFromVisitor($visitor, $anonymize);
|
||||||
|
|
||||||
return $instance;
|
return $instance;
|
||||||
@ -50,7 +44,7 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
|
|
||||||
public static function fromImport(ShortUrl $shortUrl, ImportedShlinkVisit $importedVisit): self
|
public static function fromImport(ShortUrl $shortUrl, ImportedShlinkVisit $importedVisit): self
|
||||||
{
|
{
|
||||||
$instance = new self($shortUrl, self::TYPE_IMPORTED);
|
$instance = new self($shortUrl, VisitType::IMPORTED);
|
||||||
$instance->userAgent = $importedVisit->userAgent();
|
$instance->userAgent = $importedVisit->userAgent();
|
||||||
$instance->potentialBot = isCrawler($instance->userAgent);
|
$instance->potentialBot = isCrawler($instance->userAgent);
|
||||||
$instance->referer = $importedVisit->referer();
|
$instance->referer = $importedVisit->referer();
|
||||||
@ -64,7 +58,7 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
|
|
||||||
public static function forBasePath(Visitor $visitor, bool $anonymize = true): self
|
public static function forBasePath(Visitor $visitor, bool $anonymize = true): self
|
||||||
{
|
{
|
||||||
$instance = new self(null, self::TYPE_BASE_URL);
|
$instance = new self(null, VisitType::BASE_URL);
|
||||||
$instance->hydrateFromVisitor($visitor, $anonymize);
|
$instance->hydrateFromVisitor($visitor, $anonymize);
|
||||||
|
|
||||||
return $instance;
|
return $instance;
|
||||||
@ -72,7 +66,7 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
|
|
||||||
public static function forInvalidShortUrl(Visitor $visitor, bool $anonymize = true): self
|
public static function forInvalidShortUrl(Visitor $visitor, bool $anonymize = true): self
|
||||||
{
|
{
|
||||||
$instance = new self(null, self::TYPE_INVALID_SHORT_URL);
|
$instance = new self(null, VisitType::INVALID_SHORT_URL);
|
||||||
$instance->hydrateFromVisitor($visitor, $anonymize);
|
$instance->hydrateFromVisitor($visitor, $anonymize);
|
||||||
|
|
||||||
return $instance;
|
return $instance;
|
||||||
@ -80,7 +74,7 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
|
|
||||||
public static function forRegularNotFound(Visitor $visitor, bool $anonymize = true): self
|
public static function forRegularNotFound(Visitor $visitor, bool $anonymize = true): self
|
||||||
{
|
{
|
||||||
$instance = new self(null, self::TYPE_REGULAR_404);
|
$instance = new self(null, VisitType::REGULAR_404);
|
||||||
$instance->hydrateFromVisitor($visitor, $anonymize);
|
$instance->hydrateFromVisitor($visitor, $anonymize);
|
||||||
|
|
||||||
return $instance;
|
return $instance;
|
||||||
@ -88,10 +82,10 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
|
|
||||||
private function hydrateFromVisitor(Visitor $visitor, bool $anonymize = true): void
|
private function hydrateFromVisitor(Visitor $visitor, bool $anonymize = true): void
|
||||||
{
|
{
|
||||||
$this->userAgent = $visitor->getUserAgent();
|
$this->userAgent = $visitor->userAgent;
|
||||||
$this->referer = $visitor->getReferer();
|
$this->referer = $visitor->referer;
|
||||||
$this->remoteAddr = $this->processAddress($anonymize, $visitor->getRemoteAddress());
|
$this->remoteAddr = $this->processAddress($anonymize, $visitor->remoteAddress);
|
||||||
$this->visitedUrl = $visitor->getVisitedUrl();
|
$this->visitedUrl = $visitor->visitedUrl;
|
||||||
$this->potentialBot = $visitor->isPotentialBot();
|
$this->potentialBot = $visitor->isPotentialBot();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,7 +118,7 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
return $this->shortUrl;
|
return $this->shortUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getVisitLocation(): ?VisitLocationInterface
|
public function getVisitLocation(): ?VisitLocation
|
||||||
{
|
{
|
||||||
return $this->visitLocation;
|
return $this->visitLocation;
|
||||||
}
|
}
|
||||||
@ -150,7 +144,7 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
return $this->visitedUrl;
|
return $this->visitedUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function type(): string
|
public function type(): VisitType
|
||||||
{
|
{
|
||||||
return $this->type;
|
return $this->type;
|
||||||
}
|
}
|
||||||
@ -159,11 +153,19 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
* Needed only for ArrayCollections to be able to apply criteria filtering
|
* Needed only for ArrayCollections to be able to apply criteria filtering
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
public function getType(): string
|
public function getType(): VisitType
|
||||||
{
|
{
|
||||||
return $this->type();
|
return $this->type();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public function getDate(): Chronos
|
||||||
|
{
|
||||||
|
return $this->date;
|
||||||
|
}
|
||||||
|
|
||||||
public function jsonSerialize(): array
|
public function jsonSerialize(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@ -174,12 +176,4 @@ class Visit extends AbstractEntity implements JsonSerializable
|
|||||||
'potentialBot' => $this->potentialBot,
|
'potentialBot' => $this->potentialBot,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
public function getDate(): Chronos
|
|
||||||
{
|
|
||||||
return $this->date;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,12 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\Core\Entity;
|
namespace Shlinkio\Shlink\Core\Entity;
|
||||||
|
|
||||||
|
use JsonSerializable;
|
||||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||||
use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface;
|
|
||||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisitLocation;
|
use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisitLocation;
|
||||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||||
|
|
||||||
class VisitLocation extends AbstractEntity implements VisitLocationInterface
|
class VisitLocation extends AbstractEntity implements JsonSerializable
|
||||||
{
|
{
|
||||||
private string $countryCode;
|
private string $countryCode;
|
||||||
private string $countryName;
|
private string $countryName;
|
||||||
|
@ -7,27 +7,27 @@ namespace Shlinkio\Shlink\Core\ErrorHandler\Model;
|
|||||||
use Mezzio\Router\RouteResult;
|
use Mezzio\Router\RouteResult;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||||
use Shlinkio\Shlink\Core\Entity\Visit;
|
use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||||
|
|
||||||
use function rtrim;
|
use function rtrim;
|
||||||
|
|
||||||
class NotFoundType
|
class NotFoundType
|
||||||
{
|
{
|
||||||
private function __construct(private string $type)
|
private function __construct(private readonly ?VisitType $type)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function fromRequest(ServerRequestInterface $request, string $basePath): self
|
public static function fromRequest(ServerRequestInterface $request, string $basePath): self
|
||||||
{
|
{
|
||||||
/** @var RouteResult $routeResult */
|
/** @var RouteResult $routeResult */
|
||||||
$routeResult = $request->getAttribute(RouteResult::class, RouteResult::fromRouteFailure(null));
|
$routeResult = $request->getAttribute(RouteResult::class) ?? RouteResult::fromRouteFailure(null);
|
||||||
$isBaseUrl = rtrim($request->getUri()->getPath(), '/') === $basePath;
|
$isBaseUrl = rtrim($request->getUri()->getPath(), '/') === $basePath;
|
||||||
|
|
||||||
$type = match (true) {
|
$type = match (true) {
|
||||||
$isBaseUrl => Visit::TYPE_BASE_URL,
|
$isBaseUrl => VisitType::BASE_URL,
|
||||||
$routeResult->isFailure() => Visit::TYPE_REGULAR_404,
|
$routeResult->isFailure() => VisitType::REGULAR_404,
|
||||||
$routeResult->getMatchedRouteName() === RedirectAction::class => Visit::TYPE_INVALID_SHORT_URL,
|
$routeResult->getMatchedRouteName() === RedirectAction::class => VisitType::INVALID_SHORT_URL,
|
||||||
default => self::class,
|
default => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
return new self($type);
|
return new self($type);
|
||||||
@ -35,16 +35,16 @@ class NotFoundType
|
|||||||
|
|
||||||
public function isBaseUrl(): bool
|
public function isBaseUrl(): bool
|
||||||
{
|
{
|
||||||
return $this->type === Visit::TYPE_BASE_URL;
|
return $this->type === VisitType::BASE_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isRegularNotFound(): bool
|
public function isRegularNotFound(): bool
|
||||||
{
|
{
|
||||||
return $this->type === Visit::TYPE_REGULAR_404;
|
return $this->type === VisitType::REGULAR_404;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isInvalidShortUrl(): bool
|
public function isInvalidShortUrl(): bool
|
||||||
{
|
{
|
||||||
return $this->type === Visit::TYPE_INVALID_SHORT_URL;
|
return $this->type === VisitType::INVALID_SHORT_URL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\EventDispatcher\Async;
|
||||||
|
|
||||||
|
abstract class AbstractAsyncListener
|
||||||
|
{
|
||||||
|
abstract protected function isEnabled(): bool;
|
||||||
|
|
||||||
|
abstract protected function getRemoteSystem(): RemoteSystem;
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\Core\EventDispatcher\Async;
|
||||||
|
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Shlinkio\Shlink\Common\UpdatePublishing\PublishingHelperInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\EventDispatcher\Event\ShortUrlCreated;
|
||||||
|
use Shlinkio\Shlink\Core\EventDispatcher\PublishingUpdatesGeneratorInterface;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
abstract class AbstractNotifyNewShortUrlListener extends AbstractAsyncListener
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly PublishingHelperInterface $publishingHelper,
|
||||||
|
private readonly PublishingUpdatesGeneratorInterface $updatesGenerator,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __invoke(ShortUrlCreated $shortUrlCreated): void
|
||||||
|
{
|
||||||
|
if (! $this->isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$shortUrlId = $shortUrlCreated->shortUrlId;
|
||||||
|
$shortUrl = $this->em->find(ShortUrl::class, $shortUrlId);
|
||||||
|
$name = $this->getRemoteSystem()->value;
|
||||||
|
|
||||||
|
if ($shortUrl === null) {
|
||||||
|
$this->logger->warning(
|
||||||
|
'Tried to notify {name} for new short URL with id "{shortUrlId}", but it does not exist.',
|
||||||
|
['shortUrlId' => $shortUrlId, 'name' => $name],
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->publishingHelper->publishUpdate($this->updatesGenerator->newShortUrlUpdate($shortUrl));
|
||||||
|
} catch (Throwable $e) {
|
||||||
|
$this->logger->debug(
|
||||||
|
'Error while trying to notify {name} with new short URL. {e}',
|
||||||
|
['e' => $e, 'name' => $name],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user