mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 20:23:12 +08:00
Compare commits
145 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d4f2bbd12 | ||
|
|
557c74286b | ||
|
|
67abe21716 | ||
|
|
33cea36b15 | ||
|
|
4e8f3f737a | ||
|
|
35b835ec7b | ||
|
|
eff4f1fca3 | ||
|
|
6f6388b2fc | ||
|
|
6428903e7d | ||
|
|
19f56e7ab0 | ||
|
|
6a96b72b94 | ||
|
|
7634f55587 | ||
|
|
571a4643ab | ||
|
|
d5544554ef | ||
|
|
85065c9330 | ||
|
|
86cc2b717c | ||
|
|
89f70114e4 | ||
|
|
8274525f75 | ||
|
|
fef512a7a3 | ||
|
|
deb9d4bdc7 | ||
|
|
259aadfdb2 | ||
|
|
fe660654ed | ||
|
|
b2fc19af44 | ||
|
|
7434616a8d | ||
|
|
fbf1aabcf5 | ||
|
|
8ee905882f | ||
|
|
2946b630c5 | ||
|
|
b2bfe9799a | ||
|
|
d7e300e2d5 | ||
|
|
0c75202936 | ||
|
|
81bed53f90 | ||
|
|
a56ff1293e | ||
|
|
c323bfcd63 | ||
|
|
f57f159002 | ||
|
|
fa08014226 | ||
|
|
052c9e76a1 | ||
|
|
8298ef36f8 | ||
|
|
b11d5c6864 | ||
|
|
08394431f8 | ||
|
|
a9ae4a24d0 | ||
|
|
9b7b91402c | ||
|
|
178a99b993 | ||
|
|
a8f046dfff | ||
|
|
42ff0d5b69 | ||
|
|
6aaea2ac26 | ||
|
|
b5ff568651 | ||
|
|
4a0b7e3fc9 | ||
|
|
1fee745786 | ||
|
|
a6e0916272 | ||
|
|
dbef32ffcb | ||
|
|
7ddb3e7a70 | ||
|
|
fd34332e69 | ||
|
|
51d838870d | ||
|
|
4619ebd014 | ||
|
|
f2371b6124 | ||
|
|
b5b5f92eda | ||
|
|
781c083c9f | ||
|
|
a444ed0246 | ||
|
|
9a69d06531 | ||
|
|
15cb3bb73c | ||
|
|
7ca605e216 | ||
|
|
59a4704658 | ||
|
|
48ecef3436 | ||
|
|
a5a98bd578 | ||
|
|
12a08cb373 | ||
|
|
3c6f12aec6 | ||
|
|
d228b88e51 | ||
|
|
95685d958d | ||
|
|
1a278eaf07 | ||
|
|
72f1e243b5 | ||
|
|
d6b103de83 | ||
|
|
fca3891819 | ||
|
|
3ec24e3c67 | ||
|
|
532102e662 | ||
|
|
fcd82522ab | ||
|
|
102169b6c7 | ||
|
|
dba9302f78 | ||
|
|
92ad6d2732 | ||
|
|
7e573bdb9b | ||
|
|
6f837b3b91 | ||
|
|
b08c498b13 | ||
|
|
a661d05100 | ||
|
|
9e6f129de6 | ||
|
|
4c1ff72438 | ||
|
|
6f95acc202 | ||
|
|
bd73362c94 | ||
|
|
f6d70c599e | ||
|
|
1b9c8377ae | ||
|
|
9f6975119e | ||
|
|
a094be2b9e | ||
|
|
819a535bfe | ||
|
|
e4fe7adf00 | ||
|
|
79c5418ac2 | ||
|
|
b5010e4d8c | ||
|
|
3085fa76cf | ||
|
|
1fd7d58084 | ||
|
|
eae001a34a | ||
|
|
d7ecef94f2 | ||
|
|
98364a1aae | ||
|
|
9ccb866e5e | ||
|
|
3f1d61e01e | ||
|
|
93a277a94d | ||
|
|
a10ca655a2 | ||
|
|
bb270396b6 | ||
|
|
525a306ec6 | ||
|
|
1dd71d2ee7 | ||
|
|
ac2e249746 | ||
|
|
af569ad7a5 | ||
|
|
bf121c58ba | ||
|
|
d2403367b5 | ||
|
|
84a187a26f | ||
|
|
3149adebdb | ||
|
|
228bf093d3 | ||
|
|
26589e6126 | ||
|
|
02e48ae665 | ||
|
|
0d627ce808 | ||
|
|
99639b9844 | ||
|
|
0c3c7ff3b2 | ||
|
|
d7423585ff | ||
|
|
7de07a9cd4 | ||
|
|
2a734b5d89 | ||
|
|
4520afb271 | ||
|
|
e7a9ad1db0 | ||
|
|
84860539ce | ||
|
|
2901fe8b7b | ||
|
|
f9694333c5 | ||
|
|
fc1f35ad59 | ||
|
|
9a58748581 | ||
|
|
45e108d21e | ||
|
|
f4da9c1fcc | ||
|
|
a3ea8f56dd | ||
|
|
f3244b35e3 | ||
|
|
442eea0ea7 | ||
|
|
46601443f5 | ||
|
|
c0200317dd | ||
|
|
c8e5196aab | ||
|
|
b991b1699e | ||
|
|
582033ceb3 | ||
|
|
549a8d8837 | ||
|
|
5fb6c8708c | ||
|
|
7ee757243a | ||
|
|
044efe6ee4 | ||
|
|
9b16749737 | ||
|
|
6d51ff831f | ||
|
|
0635615149 |
@@ -1,5 +1,6 @@
|
||||
bin/rr
|
||||
config/autoload/*local*
|
||||
config/params/shlink_dev_env.*
|
||||
data/infra
|
||||
data/cache/*
|
||||
data/log/*
|
||||
|
||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -13,7 +13,6 @@
|
||||
.travis.yml export-ignore
|
||||
build.sh export-ignore
|
||||
CHANGELOG.md export-ignore
|
||||
docker-compose.override.yml.dist export-ignore
|
||||
docker-compose.yml export-ignore
|
||||
indocker export-ignore
|
||||
phpcs.xml export-ignore
|
||||
|
||||
5
.github/actions/ci-setup/action.yml
vendored
5
.github/actions/ci-setup/action.yml
vendored
@@ -40,9 +40,8 @@ runs:
|
||||
php-version: ${{ inputs.php-version }}
|
||||
tools: composer
|
||||
extensions: ${{ inputs.php-extensions }}
|
||||
coverage: pcov
|
||||
ini-values: pcov.directory=module
|
||||
coverage: xdebug
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.install-deps == 'yes' }}
|
||||
run: composer install --no-interaction --prefer-dist ${{ inputs.php-version == '8.4' && '--ignore-platform-req=php' || '' }}
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
shell: bash
|
||||
|
||||
1
.github/workflows/ci-db-tests.yml
vendored
1
.github/workflows/ci-db-tests.yml
vendored
@@ -14,7 +14,6 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3', '8.4']
|
||||
continue-on-error: ${{ matrix.php-version == '8.4' }}
|
||||
env:
|
||||
LC_ALL: C
|
||||
steps:
|
||||
|
||||
8
.github/workflows/ci-docker-image-build.yml
vendored
8
.github/workflows/ci-docker-image-build.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: Build docker image
|
||||
name: Test docker image build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -7,8 +7,4 @@ on:
|
||||
|
||||
jobs:
|
||||
build-docker-image:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- run: docker build -t shlink-docker-image:temp .
|
||||
uses: shlinkio/github-actions/.github/workflows/docker-image-build-ci.yml@main
|
||||
|
||||
1
.github/workflows/ci-tests.yml
vendored
1
.github/workflows/ci-tests.yml
vendored
@@ -14,7 +14,6 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3', '8.4']
|
||||
continue-on-error: ${{ matrix.php-version == '8.4' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically
|
||||
steps:
|
||||
|
||||
2
.github/workflows/publish-release.yml
vendored
2
.github/workflows/publish-release.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.2', '8.3'] # TODO 8.4
|
||||
php-version: ['8.2', '8.3', '8.4']
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -12,7 +12,5 @@ data/GeoLite2-City.*
|
||||
data/infra/matomo
|
||||
docs/swagger-ui*
|
||||
docs/mercure.html
|
||||
docker-compose.override.yml
|
||||
.phpunit.result.cache
|
||||
docs/swagger/swagger-inlined.json
|
||||
phpcov*
|
||||
|
||||
98
CHANGELOG.md
98
CHANGELOG.md
@@ -4,6 +4,104 @@ 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).
|
||||
|
||||
# [4.3.1] - 2024-11-25
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#2285](https://github.com/shlinkio/shlink/issues/2285) Fix performance degradation when using Microsoft SQL due to incorrect order of columns in `unique_short_code_plus_domain` index.
|
||||
|
||||
|
||||
## [4.3.0] - 2024-11-24
|
||||
### Added
|
||||
* [#2159](https://github.com/shlinkio/shlink/issues/2159) Add support for PHP 8.4.
|
||||
* [#2207](https://github.com/shlinkio/shlink/issues/2207) Add `hasRedirectRules` flag to short URL API model. This flag tells if a specific short URL has any redirect rules attached to it.
|
||||
* [#1520](https://github.com/shlinkio/shlink/issues/1520) Allow short URLs list to be filtered by `domain`.
|
||||
|
||||
This change applies both to the `GET /short-urls` endpoint, via the `domain` query parameter, and the `short-url:list` console command, via the `--domain`|`-d` flag.
|
||||
|
||||
* [#1774](https://github.com/shlinkio/shlink/issues/1774) Add new geolocation redirect rules for the dynamic redirects system.
|
||||
|
||||
* `geolocation-country-code`: Allows to perform redirections based on the ISO 3166-1 alpha-2 two-letter country code resolved while geolocating the visitor.
|
||||
* `geolocation-city-name`: Allows to perform redirections based on the city name resolved while geolocating the visitor.
|
||||
|
||||
* [#2032](https://github.com/shlinkio/shlink/issues/2032) Save the URL to which a visitor is redirected when a visit is tracked.
|
||||
|
||||
The value is exposed in the API as a new `redirectUrl` field for visit objects.
|
||||
|
||||
This is useful to know where a visitor was redirected for a short URL with dynamic redirect rules, for special redirects, or simply in case the long URL was changed over time, and you still want to know where visitors were redirected originally.
|
||||
|
||||
Some visits may not have a redirect URL if a redirect didn't happen, like for orphan visits when no special redirects are configured, or when a visit is tracked as part of the pixel action.
|
||||
|
||||
### Changed
|
||||
* [#2193](https://github.com/shlinkio/shlink/issues/2193) API keys are now hashed using SHA256, instead of being saved in plain text.
|
||||
|
||||
As a side effect, API key names have now become more important, and are considered unique.
|
||||
|
||||
When people update to this Shlink version, existing API keys will be hashed for everything to continue working.
|
||||
|
||||
In order to avoid data to be lost, plain-text keys will be written in the `name` field, either together with any existing name, or as the name itself. Then users are responsible for renaming them using the new `api-key:rename` command.
|
||||
|
||||
For newly created API keys, it is recommended to provide a name, but if not provided, a name will be generated from a redacted version of the new API key.
|
||||
|
||||
* Update to Shlink PHP coding standard 2.4
|
||||
* Update to `hidehalo/nanoid-php` 2.0
|
||||
* Update to PHPStan 2.0
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#2264](https://github.com/shlinkio/shlink/issues/2264) Fix visits counts not being deleted when deleting short URL or orphan visits.
|
||||
|
||||
|
||||
## [4.2.5] - 2024-11-03
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* Update to Shlink PHP coding standard 2.4
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#2244](https://github.com/shlinkio/shlink/issues/2244) Fix integration with Redis 7.4 and Valkey.
|
||||
|
||||
|
||||
## [4.2.4] - 2024-10-27
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* [#2231](https://github.com/shlinkio/shlink/issues/2231) Update to `endroid/qr-code` 6.0.
|
||||
* [#2221](https://github.com/shlinkio/shlink/issues/2221) Switch to env vars to handle dev/local options.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#2232](https://github.com/shlinkio/shlink/issues/2232) Run RoadRunner in docker with `exec` to ensure signals are properly handled.
|
||||
|
||||
|
||||
## [4.2.3] - 2024-10-17
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
@@ -16,11 +16,14 @@ The first thing you need to do is fork the repository, and clone it in your loca
|
||||
|
||||
Then you will have to follow these steps:
|
||||
|
||||
* Copy all files with `.local.php.dist` extension from `config/autoload` by removing the dist extension.
|
||||
* Copy the `config/params/shlink_dev_env.php.dist` in the same directory, but removing the `.dist` extension:
|
||||
|
||||
For example the `common.local.php.dist` file should be copied as `common.local.php`.
|
||||
```
|
||||
cp config/params/shlink_dev_env.php.dist config/params/shlink_dev_env.php
|
||||
```
|
||||
|
||||
The `shlink_dev_env.php` file is gitignored, so you can customize it as you want. For example, by adding your own GeoLite license key.
|
||||
|
||||
* Copy the file `docker-compose.override.yml.dist` by also removing the `dist` extension.
|
||||
* Start-up the project by running `docker compose up`.
|
||||
|
||||
The first time this command is run, it will create several containers that are used during development, so it may take some time.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM php:8.3-alpine3.19 as base
|
||||
FROM php:8.3-alpine3.20 AS base
|
||||
|
||||
ARG SHLINK_VERSION=latest
|
||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||
@@ -7,8 +7,8 @@ ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
||||
|
||||
ENV USER_ID '1001'
|
||||
ENV PDO_SQLSRV_VERSION 5.12.0
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
ENV MS_ODBC_DOWNLOAD '7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.4.1.1
|
||||
ENV LC_ALL 'C'
|
||||
|
||||
WORKDIR /etc/shlink
|
||||
@@ -43,7 +43,7 @@ RUN apk add --no-cache git && \
|
||||
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction && \
|
||||
php composer.phar clear-cache && \
|
||||
rm -r docker composer.* && \
|
||||
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
|
||||
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" module/Core/src/Config/Options/AppOptions.php
|
||||
|
||||
|
||||
# Prepare final image
|
||||
@@ -61,7 +61,6 @@ EXPOSE 8080
|
||||
|
||||
# Copy config specific for the image
|
||||
COPY docker/docker-entrypoint.sh docker-entrypoint.sh
|
||||
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
|
||||
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
|
||||
|
||||
USER ${USER_ID}
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
[](https://github.com/shlinkio/shlink/blob/main/LICENSE)
|
||||
|
||||
[](https://fosstodon.org/@shlinkio)
|
||||
[](https://bsky.app/profile/shlinkio.bsky.social)
|
||||
[](https://twitter.com/shlinkio)
|
||||
[](https://bsky.app/profile/shlink.io)
|
||||
[](https://slnk.to/donate)
|
||||
|
||||
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own domain.
|
||||
|
||||
@@ -6,6 +6,8 @@ export TEST_RUNTIME="${TEST_RUNTIME:-"rr"}" # rr is the only runtime currently s
|
||||
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
|
||||
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
|
||||
|
||||
[ "$GENERATE_COVERAGE" != 'no' ] && export XDEBUG_MODE=coverage
|
||||
|
||||
# Reset logs
|
||||
OUTPUT_LOGS=data/log/api-tests/output.log
|
||||
rm -rf data/log/api-tests
|
||||
|
||||
14
bin/test/run-cli-tests.sh
Executable file
14
bin/test/run-cli-tests.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
export APP_ENV=test
|
||||
export TEST_ENV=cli
|
||||
export DB_DRIVER="${DB_DRIVER:-"maria"}"
|
||||
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
|
||||
|
||||
[ "$GENERATE_COVERAGE" != 'no' ] && export XDEBUG_MODE=coverage
|
||||
|
||||
vendor/bin/phpunit --order-by=random --testdox --testdox-summary -c phpunit-cli.xml $*
|
||||
TESTS_EXIT_CODE=$?
|
||||
|
||||
# Exit this script with the same code as the tests. If tests failed, this script has to fail
|
||||
exit $TESTS_EXIT_CODE
|
||||
4
build.sh
4
build.sh
@@ -35,8 +35,8 @@ ${composerBin} install --no-dev --prefer-dist --optimize-autoloader --no-progres
|
||||
echo 'Deleting dev files...'
|
||||
rm composer.*
|
||||
|
||||
# Update Shlink version in config
|
||||
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
||||
# Update Shlink version
|
||||
sed -i "s/%SHLINK_VERSION%/${version}/g" module/Core/src/Config/Options/AppOptions.php
|
||||
|
||||
# Compressing file
|
||||
echo 'Compressing files...'
|
||||
|
||||
129
composer.json
129
composer.json
@@ -18,64 +18,63 @@
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-pdo": "*",
|
||||
"akrabat/ip-address-middleware": "^2.1",
|
||||
"cakephp/chronos": "^3.0.2",
|
||||
"doctrine/dbal": "^4.1",
|
||||
"doctrine/migrations": "^3.6",
|
||||
"doctrine/orm": "^3.2",
|
||||
"endroid/qr-code": "^5.0",
|
||||
"akrabat/ip-address-middleware": "^2.4",
|
||||
"cakephp/chronos": "^3.1",
|
||||
"doctrine/dbal": "^4.2",
|
||||
"doctrine/migrations": "^3.8",
|
||||
"doctrine/orm": "^3.3",
|
||||
"endroid/qr-code": "^6.0",
|
||||
"friendsofphp/proxy-manager-lts": "^1.0",
|
||||
"geoip2/geoip2": "^3.0",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"hidehalo/nanoid-php": "^1.1",
|
||||
"jaybizzle/crawler-detect": "^1.2.116",
|
||||
"laminas/laminas-config": "^3.8",
|
||||
"guzzlehttp/guzzle": "^7.9",
|
||||
"hidehalo/nanoid-php": "^2.0",
|
||||
"jaybizzle/crawler-detect": "^1.3",
|
||||
"laminas/laminas-config-aggregator": "^1.15",
|
||||
"laminas/laminas-diactoros": "^3.3",
|
||||
"laminas/laminas-inputfilter": "^2.27",
|
||||
"laminas/laminas-servicemanager": "^3.21",
|
||||
"laminas/laminas-stdlib": "^3.17",
|
||||
"matomo/matomo-php-tracker": "^3.2",
|
||||
"mezzio/mezzio": "^3.17",
|
||||
"mezzio/mezzio-fastroute": "^3.11",
|
||||
"mezzio/mezzio-problem-details": "^1.13",
|
||||
"mlocati/ip-lib": "^1.18",
|
||||
"mobiledetect/mobiledetectlib": "^4.8",
|
||||
"laminas/laminas-diactoros": "^3.5",
|
||||
"laminas/laminas-inputfilter": "^2.30",
|
||||
"laminas/laminas-servicemanager": "^3.22",
|
||||
"laminas/laminas-stdlib": "^3.19",
|
||||
"matomo/matomo-php-tracker": "^3.3",
|
||||
"mezzio/mezzio": "^3.20",
|
||||
"mezzio/mezzio-fastroute": "^3.12",
|
||||
"mezzio/mezzio-problem-details": "^1.15",
|
||||
"mlocati/ip-lib": "^1.18.1",
|
||||
"mobiledetect/mobiledetectlib": "4.8.x-dev#920c549 as 4.9",
|
||||
"pagerfanta/core": "^3.8",
|
||||
"ramsey/uuid": "^4.7",
|
||||
"shlinkio/doctrine-specification": "^2.1.1",
|
||||
"shlinkio/shlink-common": "^6.3",
|
||||
"shlinkio/shlink-config": "^3.2.1",
|
||||
"shlinkio/shlink-common": "^6.6",
|
||||
"shlinkio/shlink-config": "^3.4",
|
||||
"shlinkio/shlink-event-dispatcher": "^4.1",
|
||||
"shlinkio/shlink-importer": "^5.3.2",
|
||||
"shlinkio/shlink-installer": "^9.2",
|
||||
"shlinkio/shlink-ip-geolocation": "^4.0",
|
||||
"shlinkio/shlink-installer": "^9.3",
|
||||
"shlinkio/shlink-ip-geolocation": "^4.2",
|
||||
"shlinkio/shlink-json": "^1.1",
|
||||
"spiral/roadrunner": "^2024.1",
|
||||
"spiral/roadrunner-cli": "^2.6",
|
||||
"spiral/roadrunner-http": "^3.5",
|
||||
"spiral/roadrunner-jobs": "^4.5",
|
||||
"symfony/console": "^7.0",
|
||||
"symfony/filesystem": "^7.0",
|
||||
"symfony/lock": "^7.0",
|
||||
"symfony/process": "^7.0",
|
||||
"symfony/string": "^7.0"
|
||||
"symfony/console": "^7.1",
|
||||
"symfony/filesystem": "^7.1",
|
||||
"symfony/lock": "^7.1",
|
||||
"symfony/process": "^7.1",
|
||||
"symfony/string": "^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"devizzent/cebe-php-openapi": "^1.0.1",
|
||||
"devster/ubench": "^2.1",
|
||||
"phpstan/phpstan": "^1.11",
|
||||
"phpstan/phpstan-doctrine": "^1.4",
|
||||
"phpstan/phpstan-phpunit": "^1.4",
|
||||
"phpstan/phpstan-symfony": "^1.4",
|
||||
"phpstan/phpstan": "^2.0",
|
||||
"phpstan/phpstan-doctrine": "^2.0",
|
||||
"phpstan/phpstan-phpunit": "^2.0",
|
||||
"phpstan/phpstan-symfony": "^2.0",
|
||||
"phpunit/php-code-coverage": "^11.0",
|
||||
"phpunit/phpcov": "^10.0",
|
||||
"phpunit/phpunit": "^11.3",
|
||||
"phpunit/phpunit": "^11.4",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~2.3.0",
|
||||
"shlinkio/shlink-test-utils": "^4.1",
|
||||
"symfony/var-dumper": "^7.0",
|
||||
"veewee/composer-run-parallel": "^1.3"
|
||||
"shlinkio/php-coding-standard": "~2.4.0",
|
||||
"shlinkio/shlink-test-utils": "^4.2",
|
||||
"symfony/var-dumper": "^7.1",
|
||||
"veewee/composer-run-parallel": "^1.4"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/var-exporter": ">=6.3.9,<=6.4.0"
|
||||
@@ -114,31 +113,47 @@
|
||||
],
|
||||
"cs": "phpcs -s",
|
||||
"cs:fix": "phpcbf",
|
||||
"stan": "APP_ENV=test php vendor/bin/phpstan analyse",
|
||||
"stan": ["@putenv APP_ENV=test", "phpstan analyse"],
|
||||
"test": [
|
||||
"@parallel test:unit test:db",
|
||||
"@parallel test:api test:cli"
|
||||
],
|
||||
"test:unit": "COLUMNS=120 vendor/bin/phpunit --order-by=random --testdox --testdox-summary",
|
||||
"test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov",
|
||||
"test:unit:pretty": "@test:unit --coverage-html build/coverage-unit/coverage-html",
|
||||
"test:unit": ["@putenv COLUMNS=120", "phpunit --order-by=random --testdox --testdox-summary"],
|
||||
"test:unit:ci": ["@putenv XDEBUG_MODE=coverage", "@test:unit --coverage-php=build/coverage-unit.cov"],
|
||||
"test:unit:pretty": ["@putenv XDEBUG_MODE=coverage", "@test:unit --coverage-html build/coverage-unit/coverage-html"],
|
||||
"test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --testdox --testdox-summary -c phpunit-db.xml",
|
||||
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov",
|
||||
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite -- $*",
|
||||
"test:db:maria": "DB_DRIVER=maria composer test:db:sqlite -- $*",
|
||||
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite -- $*",
|
||||
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite -- $*",
|
||||
"test:db:sqlite": ["@putenv APP_ENV=test", "phpunit --order-by=random --testdox --testdox-summary -c phpunit-db.xml"],
|
||||
"test:db:sqlite:ci": ["@putenv XDEBUG_MODE=coverage", "@test:db:sqlite --coverage-php build/coverage-db.cov"],
|
||||
"test:db:mysql": ["@putenv DB_DRIVER=mysql", "@test:db:sqlite"],
|
||||
"test:db:maria": ["@putenv DB_DRIVER=maria", "@test:db:sqlite"],
|
||||
"test:db:postgres": ["@putenv DB_DRIVER=postgres", "@test:db:sqlite"],
|
||||
"test:db:ms": ["@putenv DB_DRIVER=mssql", "@test:db:sqlite"],
|
||||
"test:api": "bin/test/run-api-tests.sh",
|
||||
"test:api:sqlite": "DB_DRIVER=sqlite composer test:api -- $*",
|
||||
"test:api:mysql": "DB_DRIVER=mysql composer test:api -- $*",
|
||||
"test:api:maria": "DB_DRIVER=maria composer test:api -- $*",
|
||||
"test:api:mssql": "DB_DRIVER=mssql composer test:api -- $*",
|
||||
"test:api:ci": "GENERATE_COVERAGE=yes composer test:api && vendor/bin/phpcov merge build/coverage-api --php build/coverage-api.cov && rm build/coverage-api/*.cov",
|
||||
"test:api:pretty": "GENERATE_COVERAGE=yes composer test:api && vendor/bin/phpcov merge build/coverage-api --html build/coverage-api/coverage-html && rm build/coverage-api/*.cov",
|
||||
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --testdox --testdox-summary -c phpunit-cli.xml",
|
||||
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli && vendor/bin/phpcov merge build/coverage-cli --php build/coverage-cli.cov && rm build/coverage-cli/*.cov",
|
||||
"test:cli:pretty": "GENERATE_COVERAGE=yes composer test:cli && vendor/bin/phpcov merge build/coverage-cli --html build/coverage-cli/coverage-html && rm build/coverage-cli/*.cov",
|
||||
"test:api:sqlite": ["@putenv DB_DRIVER=sqlite", "@test:api"],
|
||||
"test:api:mysql": ["@putenv DB_DRIVER=mysql", "@test:api"],
|
||||
"test:api:maria": ["@putenv DB_DRIVER=maria", "@test:api"],
|
||||
"test:api:mssql": ["@putenv DB_DRIVER=mssql", "@test:api"],
|
||||
"test:api:ci": [
|
||||
"@putenv GENERATE_COVERAGE=yes",
|
||||
"@test:api",
|
||||
"phpcov merge build/coverage-api --php build/coverage-api.cov && rm build/coverage-api/*.cov"
|
||||
],
|
||||
"test:api:pretty": [
|
||||
"@putenv GENERATE_COVERAGE=yes",
|
||||
"@test:api",
|
||||
"phpcov merge build/coverage-api --html build/coverage-api/coverage-html && rm build/coverage-api/*.cov"
|
||||
],
|
||||
"test:cli": "bin/test/run-cli-tests.sh",
|
||||
"test:cli:ci": [
|
||||
"@putenv GENERATE_COVERAGE=yes",
|
||||
"@test:cli",
|
||||
"vendor/bin/phpcov merge build/coverage-cli --php build/coverage-cli.cov && rm build/coverage-cli/*.cov"
|
||||
],
|
||||
"test:cli:pretty": [
|
||||
"@putenv GENERATE_COVERAGE=yes",
|
||||
"@test:cli",
|
||||
"phpcov merge build/coverage-cli --html build/coverage-cli/coverage-html && rm build/coverage-cli/*.cov"
|
||||
],
|
||||
"swagger:validate": "php-openapi validate docs/swagger/swagger.json",
|
||||
"swagger:inline": "php-openapi inline docs/swagger/swagger.json docs/swagger/swagger-inlined.json",
|
||||
"clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php"
|
||||
|
||||
2
config/autoload/.gitignore
vendored
2
config/autoload/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
local.php
|
||||
*.local.php
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'app_options' => [
|
||||
'name' => 'Shlink',
|
||||
'version' => '%SHLINK_VERSION%',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,11 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'app_options' => [
|
||||
'version' => 'latest',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'ip_address_resolution' => [
|
||||
'headers_to_inspect' => [
|
||||
'CF-Connecting-IP',
|
||||
'X-Forwarded-For',
|
||||
'X-Forwarded',
|
||||
'Forwarded',
|
||||
'True-Client-IP',
|
||||
'X-Real-IP',
|
||||
'X-Cluster-Client-Ip',
|
||||
'Client-Ip',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -3,13 +3,18 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
return (function () {
|
||||
$isDev = EnvVars::isDevEnv();
|
||||
|
||||
'debug' => false,
|
||||
return [
|
||||
|
||||
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
|
||||
// commands don't generate a cache file that's then used by php-fpm web executions
|
||||
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',
|
||||
'debug' => $isDev,
|
||||
|
||||
];
|
||||
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
|
||||
// commands don't generate a cache file that's then used by php-fpm web executions
|
||||
ConfigAggregator::ENABLE_CACHE => ! $isDev && PHP_SAPI !== 'cli',
|
||||
|
||||
];
|
||||
})();
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Laminas\ConfigAggregator\ConfigAggregator;
|
||||
|
||||
return [
|
||||
|
||||
'debug' => true,
|
||||
ConfigAggregator::ENABLE_CACHE => false,
|
||||
|
||||
];
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return (static function (): array {
|
||||
$threshold = EnvVars::DELETE_SHORT_URL_THRESHOLD->loadFromEnv();
|
||||
|
||||
return [
|
||||
|
||||
'delete_short_urls' => [
|
||||
'check_visits_threshold' => $threshold !== null,
|
||||
'visits_threshold' => (int) ($threshold ?? DEFAULT_DELETE_SHORT_URL_THRESHOLD),
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
@@ -11,6 +11,7 @@ use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\ServerRequestFactoryInterface;
|
||||
use Psr\Http\Message\StreamFactoryInterface;
|
||||
use Psr\Http\Message\UploadedFileFactoryInterface;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Spiral\RoadRunner\Http\PSR7Worker;
|
||||
use Spiral\RoadRunner\WorkerInterface;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
@@ -36,7 +37,7 @@ return [
|
||||
'lazy_services' => [
|
||||
'proxies_target_dir' => 'data/proxies',
|
||||
'proxies_namespace' => 'ShlinkProxy',
|
||||
'write_proxy_files' => true,
|
||||
'write_proxy_files' => EnvVars::isProdEnv(),
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Psr\Log;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'lazy_services' => [
|
||||
'write_proxy_files' => false,
|
||||
],
|
||||
|
||||
'initializers' => [
|
||||
function (ContainerInterface $container, $instance): void {
|
||||
if ($instance instanceof Log\LoggerAwareInterface) {
|
||||
$instance->setLogger($container->get(Log\LoggerInterface::class));
|
||||
}
|
||||
},
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,46 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'entity_manager' => [
|
||||
'connection' => [
|
||||
// MySQL
|
||||
'user' => 'root',
|
||||
'password' => 'root',
|
||||
'driver' => 'pdo_mysql',
|
||||
'host' => 'shlink_db_mysql',
|
||||
'dbname' => 'shlink',
|
||||
// 'dbname' => 'shlink_foo',
|
||||
'charset' => 'utf8mb4',
|
||||
|
||||
// MariaDB
|
||||
// 'user' => 'root',
|
||||
// 'password' => 'root',
|
||||
// 'driver' => 'pdo_mysql',
|
||||
// 'host' => 'shlink_db_maria',
|
||||
// 'dbname' => 'shlink_foo',
|
||||
// 'charset' => 'utf8mb4',
|
||||
|
||||
// Postgres
|
||||
// 'user' => 'postgres',
|
||||
// 'password' => 'root',
|
||||
// 'driver' => 'pdo_pgsql',
|
||||
// 'host' => 'shlink_db_postgres',
|
||||
// 'dbname' => 'shlink_foo',
|
||||
// 'charset' => 'utf8',
|
||||
|
||||
// MSSQL
|
||||
// 'user' => 'sa',
|
||||
// 'password' => 'Passw0rd!',
|
||||
// 'driver' => 'pdo_sqlsrv',
|
||||
// 'host' => 'shlink_db_ms',
|
||||
// 'dbname' => 'shlink_foo',
|
||||
// 'driverOptions' => [
|
||||
// 'TrustServerCertificate' => 'true',
|
||||
// ],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
37
config/autoload/ip-address.global.php
Normal file
37
config/autoload/ip-address.global.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use RKA\Middleware\IpAddress;
|
||||
use RKA\Middleware\Mezzio\IpAddressFactory;
|
||||
|
||||
use const Shlinkio\Shlink\IP_ADDRESS_REQUEST_ATTRIBUTE;
|
||||
|
||||
return [
|
||||
|
||||
// Configuration for RKA\Middleware\IpAddress
|
||||
'rka' => [
|
||||
'ip_address' => [
|
||||
'attribute_name' => IP_ADDRESS_REQUEST_ATTRIBUTE,
|
||||
'check_proxy_headers' => true,
|
||||
'trusted_proxies' => [],
|
||||
'headers_to_inspect' => [
|
||||
'CF-Connecting-IP',
|
||||
'X-Forwarded-For',
|
||||
'X-Forwarded',
|
||||
'Forwarded',
|
||||
'True-Client-IP',
|
||||
'X-Real-IP',
|
||||
'X-Cluster-Client-Ip',
|
||||
'Client-Ip',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
IpAddress::class => IpAddressFactory::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -14,23 +14,33 @@ use Shlinkio\Shlink\Common\Logger\LoggerFactory;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Helper\RequestIdProvider;
|
||||
use Shlinkio\Shlink\EventDispatcher\Util\RequestIdProviderInterface;
|
||||
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
return (static function (): array {
|
||||
$isDev = EnvVars::isDevEnv();
|
||||
$common = [
|
||||
'level' => Level::Info->value,
|
||||
'level' => $isDev ? Level::Debug->value : Level::Info->value,
|
||||
'processors' => [RequestIdMiddleware::class],
|
||||
'line_format' =>
|
||||
'[%datetime%] [%extra.' . RequestIdMiddleware::ATTRIBUTE . '%] %channel%.%level_name% - %message%',
|
||||
];
|
||||
|
||||
// In dev env or the docker container, stream Shlink logs to stderr, otherwise send them to a file
|
||||
$useStreamForShlinkLogger = $isDev || env('SHLINK_RUNTIME') !== null;
|
||||
|
||||
return [
|
||||
|
||||
'logger' => [
|
||||
'Shlink' => [
|
||||
'Shlink' => $useStreamForShlinkLogger ? [
|
||||
'type' => LoggerType::STREAM->value,
|
||||
'destination' => 'php://stderr',
|
||||
...$common,
|
||||
] : [
|
||||
'type' => LoggerType::FILE->value,
|
||||
...$common,
|
||||
],
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Monolog\Level;
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
|
||||
return [
|
||||
|
||||
'logger' => [
|
||||
'Shlink' => [
|
||||
'type' => LoggerType::STREAM->value,
|
||||
'destination' => 'php://stderr',
|
||||
'level' => Level::Debug->value,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
|
||||
'matomo' => [
|
||||
'enabled' => (bool) EnvVars::MATOMO_ENABLED->loadFromEnv(),
|
||||
'base_url' => EnvVars::MATOMO_BASE_URL->loadFromEnv(),
|
||||
'site_id' => EnvVars::MATOMO_SITE_ID->loadFromEnv(),
|
||||
'api_token' => EnvVars::MATOMO_API_TOKEN->loadFromEnv(),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Dev matomo instance needs to be manually configured once before enabling the configuration below.
|
||||
*
|
||||
* 1. Go to http://localhost:8003 and follow the installation instructions.
|
||||
* 2. Open data/infra/matomo/config/config.ini.php and replace `trusted_hosts[] = "localhost"` with
|
||||
* `trusted_hosts[] = "localhost:8003"` (see https://github.com/matomo-org/matomo/issues/9549)
|
||||
* 3. Go to http://localhost:8003/index.php?module=SitesManager&action=index and paste the ID for the site you just
|
||||
* created into the `site_id` field below.
|
||||
* 4. Go to http://localhost:8003/index.php?module=UsersManager&action=userSecurity, scroll down, click
|
||||
* "Create new token" and once generated, paste the token into the `api_token` field below.
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
'matomo' => [
|
||||
// 'enabled' => true,
|
||||
// 'base_url' => 'http://shlink_matomo',
|
||||
// 'site_id' => '...',
|
||||
// 'api_token' => '...',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -8,34 +8,31 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Symfony\Component\Mercure\Hub;
|
||||
use Symfony\Component\Mercure\HubInterface;
|
||||
|
||||
return (static function (): array {
|
||||
$publicUrl = EnvVars::MERCURE_PUBLIC_HUB_URL->loadFromEnv();
|
||||
return [
|
||||
|
||||
return [
|
||||
// This config is used by shlink-common. Do not delete
|
||||
'mercure' => [
|
||||
'public_hub_url' => EnvVars::MERCURE_PUBLIC_HUB_URL->loadFromEnv(),
|
||||
'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL->loadFromEnv(),
|
||||
'jwt_secret' => EnvVars::MERCURE_JWT_SECRET->loadFromEnv(),
|
||||
'jwt_issuer' => 'Shlink',
|
||||
],
|
||||
|
||||
'mercure' => [
|
||||
'public_hub_url' => $publicUrl,
|
||||
'internal_hub_url' => EnvVars::MERCURE_INTERNAL_HUB_URL->loadFromEnv(),
|
||||
'jwt_secret' => EnvVars::MERCURE_JWT_SECRET->loadFromEnv(),
|
||||
'jwt_issuer' => 'Shlink',
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'delegators' => [
|
||||
LcobucciJwtProvider::class => [
|
||||
LazyServiceFactory::class,
|
||||
],
|
||||
Hub::class => [
|
||||
LazyServiceFactory::class,
|
||||
],
|
||||
'dependencies' => [
|
||||
'delegators' => [
|
||||
LcobucciJwtProvider::class => [
|
||||
LazyServiceFactory::class,
|
||||
],
|
||||
'lazy_services' => [
|
||||
'class_map' => [
|
||||
LcobucciJwtProvider::class => LcobucciJwtProvider::class,
|
||||
Hub::class => HubInterface::class,
|
||||
],
|
||||
Hub::class => [
|
||||
LazyServiceFactory::class,
|
||||
],
|
||||
],
|
||||
'lazy_services' => [
|
||||
'class_map' => [
|
||||
LcobucciJwtProvider::class => LcobucciJwtProvider::class,
|
||||
Hub::class => HubInterface::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
];
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'mercure' => [
|
||||
'public_hub_url' => 'http://localhost:8002',
|
||||
'internal_hub_url' => 'http://shlink_mercure_proxy',
|
||||
'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -11,6 +11,7 @@ use RKA\Middleware\IpAddress;
|
||||
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
|
||||
use Shlinkio\Shlink\Common\Middleware\ContentLengthMiddleware;
|
||||
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
|
||||
use Shlinkio\Shlink\Core\Geolocation\Middleware\IpGeolocationMiddleware;
|
||||
|
||||
return [
|
||||
|
||||
@@ -67,8 +68,11 @@ return [
|
||||
],
|
||||
'not-found' => [
|
||||
'middleware' => [
|
||||
// This middleware is in front of tracking actions explicitly. Putting here for orphan visits tracking
|
||||
// These two middlewares are in front of other tracking actions.
|
||||
// Putting them here for orphan visits tracking
|
||||
IpAddress::class,
|
||||
IpGeolocationMiddleware::class,
|
||||
|
||||
Core\ErrorHandler\NotFoundTypeResolverMiddleware::class,
|
||||
Core\ShortUrl\Middleware\ExtraPathRedirectMiddleware::class,
|
||||
Core\ErrorHandler\NotFoundTrackerMiddleware::class,
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
|
||||
'qr_codes' => [
|
||||
'size' => (int) EnvVars::DEFAULT_QR_CODE_SIZE->loadFromEnv(),
|
||||
'margin' => (int) EnvVars::DEFAULT_QR_CODE_MARGIN->loadFromEnv(),
|
||||
'format' => EnvVars::DEFAULT_QR_CODE_FORMAT->loadFromEnv(),
|
||||
'error_correction' => EnvVars::DEFAULT_QR_CODE_ERROR_CORRECTION->loadFromEnv(),
|
||||
'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv(),
|
||||
'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(),
|
||||
'color' => EnvVars::DEFAULT_QR_CODE_COLOR->loadFromEnv(),
|
||||
'bg_color' => EnvVars::DEFAULT_QR_CODE_BG_COLOR->loadFromEnv(),
|
||||
'logo_url' => EnvVars::DEFAULT_QR_CODE_LOGO_URL->loadFromEnv(),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -6,6 +6,7 @@ use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
|
||||
// This config is used by shlink-common. Do not delete
|
||||
'rabbitmq' => [
|
||||
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(),
|
||||
'host' => EnvVars::RABBITMQ_HOST->loadFromEnv(),
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'rabbitmq' => [
|
||||
'enabled' => true,
|
||||
'host' => 'shlink_rabbitmq',
|
||||
'port' => 5672,
|
||||
'user' => 'rabbit',
|
||||
'password' => 'rabbit',
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,20 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
|
||||
'not_found_redirects' => [
|
||||
'invalid_short_url' => EnvVars::DEFAULT_INVALID_SHORT_URL_REDIRECT->loadFromEnv(),
|
||||
'regular_404' => EnvVars::DEFAULT_REGULAR_404_REDIRECT->loadFromEnv(),
|
||||
'base_url' => EnvVars::DEFAULT_BASE_URL_REDIRECT->loadFromEnv(),
|
||||
],
|
||||
|
||||
'redirects' => [
|
||||
'redirect_status_code' => (int) EnvVars::REDIRECT_STATUS_CODE->loadFromEnv(),
|
||||
'redirect_cache_lifetime' => (int) EnvVars::REDIRECT_CACHE_LIFETIME->loadFromEnv(),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'cache' => [
|
||||
'redis' => [
|
||||
'servers' => 'tcp://shlink_redis:6379',
|
||||
// 'servers' => 'tcp://barbar@shlink_redis_acl:6379',
|
||||
// 'servers' => 'tcp://foo:bar@shlink_redis_acl:6379',
|
||||
],
|
||||
],
|
||||
|
||||
'redis' => [
|
||||
'pub_sub_enabled' => true,
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'aliases' => [
|
||||
// With this config, a user could alias 'lock_store' => 'redis_lock_store' to override the default
|
||||
// 'lock_store' => 'redis_lock_store',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,14 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
return [
|
||||
|
||||
'robots' => [
|
||||
'allow-all-short-urls' => (bool) Config\EnvVars::ROBOTS_ALLOW_ALL_SHORT_URLS->loadFromEnv(),
|
||||
'user-agents' => splitByComma(Config\EnvVars::ROBOTS_USER_AGENTS->loadFromEnv()),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -13,7 +13,7 @@ return [
|
||||
'fastroute' => [
|
||||
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
|
||||
// commands don't generate a cache file that's then used by php-fpm web executions
|
||||
FastRouteRouter::CONFIG_CACHE_ENABLED => PHP_SAPI !== 'cli',
|
||||
FastRouteRouter::CONFIG_CACHE_ENABLED => EnvVars::isProdEnv() && PHP_SAPI !== 'cli',
|
||||
FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mezzio\Router\FastRouteRouter;
|
||||
|
||||
return [
|
||||
|
||||
'router' => [
|
||||
// 'base_path' => '',
|
||||
'fastroute' => [
|
||||
FastRouteRouter::CONFIG_CACHE_ENABLED => false,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -8,6 +8,7 @@ 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\Core\Geolocation\Middleware\IpGeolocationMiddleware;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Middleware\TrimTrailingSlashMiddleware;
|
||||
use Shlinkio\Shlink\Rest\Action;
|
||||
use Shlinkio\Shlink\Rest\ConfigProvider;
|
||||
@@ -19,8 +20,6 @@ use function sprintf;
|
||||
return (static function (): array {
|
||||
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
|
||||
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
|
||||
|
||||
// TODO This should be based on config, not the env var
|
||||
$shortUrlRouteSuffix = EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv() ? '[/]' : '';
|
||||
|
||||
return [
|
||||
@@ -90,6 +89,7 @@ return (static function (): array {
|
||||
'path' => '/{shortCode}/track',
|
||||
'middleware' => [
|
||||
IpAddress::class,
|
||||
IpGeolocationMiddleware::class,
|
||||
CoreAction\PixelAction::class,
|
||||
],
|
||||
'allowed_methods' => [RequestMethodInterface::METHOD_GET],
|
||||
@@ -107,6 +107,7 @@ return (static function (): array {
|
||||
'path' => sprintf('/{shortCode}%s', $shortUrlRouteSuffix),
|
||||
'middleware' => [
|
||||
IpAddress::class,
|
||||
IpGeolocationMiddleware::class,
|
||||
TrimTrailingSlashMiddleware::class,
|
||||
CoreAction\RedirectAction::class,
|
||||
],
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
use function Shlinkio\Shlink\Core\splitByComma;
|
||||
|
||||
return [
|
||||
|
||||
'tracking' => [
|
||||
// Tells if IP addresses should be anonymized before persisting, to fulfil data protection regulations
|
||||
// This applies only if IP address tracking is enabled
|
||||
'anonymize_remote_addr' => (bool) EnvVars::ANONYMIZE_REMOTE_ADDR->loadFromEnv(),
|
||||
|
||||
// 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(),
|
||||
|
||||
// A query param that, if provided, will disable tracking of one particular visit. Always takes precedence
|
||||
'disable_track_param' => EnvVars::DISABLE_TRACK_PARAM->loadFromEnv(),
|
||||
|
||||
// If true, visits will not be tracked at all
|
||||
'disable_tracking' => (bool) EnvVars::DISABLE_TRACKING->loadFromEnv(),
|
||||
|
||||
// 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(),
|
||||
|
||||
// If true, the referrer will not be tracked
|
||||
'disable_referrer_tracking' => (bool) EnvVars::DISABLE_REFERRER_TRACKING->loadFromEnv(),
|
||||
|
||||
// If true, the user agent will not be tracked
|
||||
'disable_ua_tracking' => (bool) EnvVars::DISABLE_UA_TRACKING->loadFromEnv(),
|
||||
|
||||
// A list of IP addresses, patterns or CIDR blocks from which tracking is disabled by default
|
||||
'disable_tracking_from' => splitByComma(EnvVars::DISABLE_TRACKING_FROM->loadFromEnv()),
|
||||
],
|
||||
|
||||
];
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
|
||||
|
||||
use const Shlinkio\Shlink\MIN_SHORT_CODES_LENGTH;
|
||||
|
||||
return (static function (): array {
|
||||
$shortCodesLength = max(
|
||||
(int) EnvVars::DEFAULT_SHORT_CODES_LENGTH->loadFromEnv(),
|
||||
MIN_SHORT_CODES_LENGTH,
|
||||
);
|
||||
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv();
|
||||
$mode = ShortUrlMode::tryFrom($modeFromEnv) ?? ShortUrlMode::STRICT;
|
||||
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [ // TODO Refactor this structure to url_shortener.schema and url_shortener.default_domain
|
||||
'schema' => ((bool) EnvVars::IS_HTTPS_ENABLED->loadFromEnv()) ? 'https' : 'http',
|
||||
'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(),
|
||||
],
|
||||
'default_short_codes_length' => $shortCodesLength,
|
||||
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(),
|
||||
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(),
|
||||
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(),
|
||||
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(),
|
||||
'mode' => $mode,
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
return [
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => 'http',
|
||||
'hostname' => sprintf('localhost:%s', match (true) {
|
||||
runningInRoadRunner() => '8800',
|
||||
default => '8000',
|
||||
}),
|
||||
],
|
||||
// 'multi_segment_slugs_enabled' => true,
|
||||
// 'trailing_slash_enabled' => true,
|
||||
],
|
||||
|
||||
];
|
||||
@@ -8,10 +8,7 @@ use Laminas\ConfigAggregator;
|
||||
use Laminas\Diactoros;
|
||||
use Mezzio;
|
||||
use Mezzio\ProblemDetails;
|
||||
|
||||
use function Shlinkio\Shlink\Config\env;
|
||||
|
||||
$isTestEnv = env('APP_ENV') === 'test';
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator(
|
||||
providers: [
|
||||
@@ -29,10 +26,10 @@ return (new ConfigAggregator\ConfigAggregator(
|
||||
CLI\ConfigProvider::class,
|
||||
Rest\ConfigProvider::class,
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/{,*.}global.php'),
|
||||
// Local config should not be loaded during tests, whereas test config should be loaded ONLY during tests
|
||||
new ConfigAggregator\PhpFileProvider(
|
||||
$isTestEnv ? 'config/test/*.global.php' : 'config/autoload/{,*.}local.php',
|
||||
),
|
||||
// Test config should be loaded ONLY during tests
|
||||
EnvVars::isTestEnv()
|
||||
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
// Routes have to be loaded last
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
|
||||
],
|
||||
|
||||
@@ -21,3 +21,5 @@ const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
|
||||
const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = true;
|
||||
const DEFAULT_QR_CODE_COLOR = '#000000'; // Black
|
||||
const DEFAULT_QR_CODE_BG_COLOR = '#ffffff'; // White
|
||||
const IP_ADDRESS_REQUEST_ATTRIBUTE = 'remote_address';
|
||||
const REDIRECT_URL_REQUEST_ATTRIBUTE = 'redirect_url';
|
||||
|
||||
@@ -15,8 +15,11 @@ chdir(dirname(__DIR__));
|
||||
|
||||
require 'vendor/autoload.php';
|
||||
|
||||
// Promote env vars from installer config
|
||||
loadEnvVarsFromConfig('config/params/generated_config.php', enumValues(EnvVars::class));
|
||||
// Promote env vars from installer, dev config or test config
|
||||
loadEnvVarsFromConfig(
|
||||
EnvVars::isTestEnv() ? 'config/test/shlink_test_env.php' : 'config/params/*.php',
|
||||
enumValues(EnvVars::class),
|
||||
);
|
||||
|
||||
// This is one of the first files loaded. Configure the timezone and memory limit here
|
||||
ini_set('memory_limit', EnvVars::MEMORY_LIMIT->loadFromEnv());
|
||||
|
||||
1
config/params/.gitignore
vendored
1
config/params/.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
*
|
||||
!.gitignore
|
||||
!*.dist
|
||||
|
||||
76
config/params/shlink_dev_env.php.dist
Normal file
76
config/params/shlink_dev_env.php.dist
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
|
||||
EnvVars::APP_ENV->value => 'dev',
|
||||
// EnvVars::GEOLITE_LICENSE_KEY->value => '',
|
||||
|
||||
// URL shortener
|
||||
EnvVars::DEFAULT_DOMAIN->value => 'localhost:8800',
|
||||
EnvVars::IS_HTTPS_ENABLED->value => false,
|
||||
|
||||
// Database - MySQL
|
||||
EnvVars::DB_DRIVER->value => 'mysql',
|
||||
EnvVars::DB_USER->value => 'root',
|
||||
EnvVars::DB_PASSWORD->value => 'root',
|
||||
EnvVars::DB_NAME->value => 'shlink',
|
||||
// EnvVars::DB_NAME->value => 'shlink_foo',
|
||||
EnvVars::DB_HOST->value => 'shlink_db_mysql',
|
||||
|
||||
// Database - Maria
|
||||
// EnvVars::DB_DRIVER->value => 'maria',
|
||||
// EnvVars::DB_USER->value => 'root',
|
||||
// EnvVars::DB_PASSWORD->value => 'root',
|
||||
// EnvVars::DB_NAME->value => 'shlink_foo',
|
||||
// EnvVars::DB_HOST->value => 'shlink_db_maria',
|
||||
|
||||
// Database - Postgres
|
||||
// EnvVars::DB_DRIVER->value => 'postgres',
|
||||
// EnvVars::DB_USER->value => 'postgres',
|
||||
// EnvVars::DB_PASSWORD->value => 'root',
|
||||
// EnvVars::DB_NAME->value => 'shlink_foo',
|
||||
// EnvVars::DB_HOST->value => 'shlink_db_postgres',
|
||||
|
||||
// Database - MSSQL
|
||||
// EnvVars::DB_DRIVER->value => 'mssql',
|
||||
// EnvVars::DB_USER->value => 'sa',
|
||||
// EnvVars::DB_PASSWORD->value => 'Passw0rd!',
|
||||
// EnvVars::DB_NAME->value => 'shlink_foo',
|
||||
// EnvVars::DB_HOST->value => 'shlink_db_ms',
|
||||
|
||||
// Matomo
|
||||
// Dev matomo instance needs to be manually configured once before enabling the configuration below:
|
||||
// 1. Go to http://localhost:8003 and follow the installation instructions.
|
||||
// 2. Open data/infra/matomo/config/config.ini.php and replace `trusted_hosts[] = "localhost"` with
|
||||
// `trusted_hosts[] = "localhost:8003"` (see https://github.com/matomo-org/matomo/issues/9549)
|
||||
// 3. Go to http://localhost:8003/index.php?module=SitesManager&action=index and paste the ID for the site you just
|
||||
// created into the `MATOMO_SITE_ID` var below.
|
||||
// 4. Go to http://localhost:8003/index.php?module=UsersManager&action=userSecurity, scroll down, click
|
||||
// "Create new token" and once generated, paste the token into the `MATOMO_API_TOKEN` var below.
|
||||
// 5. Copy the config below and paste it in a new shlink-dev.local.env file.
|
||||
EnvVars::MATOMO_ENABLED->value => false,
|
||||
EnvVars::MATOMO_BASE_URL->value => 'http://shlink_matomo',
|
||||
// EnvVars::MATOMO_SITE_ID->value => ,
|
||||
// EnvVars::MATOMO_API_TOKEN->value => ,
|
||||
|
||||
// Mercure
|
||||
EnvVars::MERCURE_PUBLIC_HUB_URL->value => 'http://localhost:8002',
|
||||
EnvVars::MERCURE_INTERNAL_HUB_URL->value => 'http://shlink_mercure_proxy',
|
||||
EnvVars::MERCURE_JWT_SECRET->value => 'mercure_jwt_key_long_enough_to_avoid_error',
|
||||
|
||||
// RabbitMQ
|
||||
EnvVars::RABBITMQ_ENABLED->value => true,
|
||||
EnvVars::RABBITMQ_HOST->value => 'shlink_rabbitmq',
|
||||
EnvVars::RABBITMQ_PORT->value => 5672,
|
||||
EnvVars::RABBITMQ_USER->value => 'rabbit',
|
||||
EnvVars::RABBITMQ_PASSWORD->value => 'rabbit',
|
||||
|
||||
// Redis
|
||||
EnvVars::REDIS_PUB_SUB_ENABLED->value => true,
|
||||
EnvVars::REDIS_SERVERS->value => 'tcp://shlink_redis:6379',
|
||||
|
||||
];
|
||||
15
config/test/shlink_test_env.php
Normal file
15
config/test/shlink_test_env.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
|
||||
EnvVars::APP_ENV->value => 'test',
|
||||
|
||||
// URL shortener
|
||||
EnvVars::DEFAULT_DOMAIN->value => 's.test',
|
||||
EnvVars::IS_HTTPS_ENABLED->value => false,
|
||||
|
||||
];
|
||||
@@ -93,13 +93,6 @@ return [
|
||||
ConfigAggregator::ENABLE_CACHE => false,
|
||||
FastRouteRouter::CONFIG_CACHE_ENABLED => false,
|
||||
|
||||
'url_shortener' => [
|
||||
'domain' => [
|
||||
'schema' => 'http',
|
||||
'hostname' => 's.test',
|
||||
],
|
||||
],
|
||||
|
||||
'routes' => [
|
||||
// This route is used to test that title resolution is skipped if the long URL times out
|
||||
[
|
||||
@@ -120,13 +113,6 @@ return [
|
||||
],
|
||||
],
|
||||
|
||||
// Disable mercure integration during E2E tests
|
||||
'mercure' => [
|
||||
'public_hub_url' => null,
|
||||
'internal_hub_url' => null,
|
||||
'jwt_secret' => null,
|
||||
],
|
||||
|
||||
'dependencies' => [
|
||||
'services' => [
|
||||
'shlink_test_api_client' => new Client([
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
set -ex
|
||||
|
||||
curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
|
||||
curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
|
||||
curl https://packages.microsoft.com/config/ubuntu/24.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
|
||||
apt-get update
|
||||
ACCEPT_EULA=Y apt-get install msodbcsql18
|
||||
# apt-get install unixodbc-dev
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
FROM php:8.3-fpm-alpine3.19
|
||||
FROM php:8.3-fpm-alpine3.20
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.23
|
||||
ENV APCU_VERSION 5.1.24
|
||||
ENV PDO_SQLSRV_VERSION 5.12.0
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
ENV MS_ODBC_DOWNLOAD '7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.4.1.1
|
||||
|
||||
RUN apk update
|
||||
|
||||
@@ -46,12 +46,13 @@ RUN mkdir -p /usr/src/php/ext/apcu \
|
||||
&& rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini \
|
||||
&& echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||
|
||||
# Install pcov and sqlsrv driver
|
||||
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
# Install xdebug and sqlsrv driver
|
||||
RUN apk add --update linux-headers && \
|
||||
wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
||||
docker-php-ext-enable pdo_sqlsrv pcov && \
|
||||
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} xdebug && \
|
||||
docker-php-ext-enable pdo_sqlsrv xdebug && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
||||
|
||||
|
||||
@@ -3,5 +3,3 @@ error_reporting=-1
|
||||
log_errors_max_len=0
|
||||
zend.assertions=1
|
||||
assert.exception=1
|
||||
pcov.enabled=1
|
||||
pcov.directory=module
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
FROM php:8.3-alpine3.19
|
||||
FROM php:8.3-alpine3.20
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.23
|
||||
ENV PDO_SQLSRV_VERSION 5.12.0
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
ENV MS_ODBC_DOWNLOAD '7/6/d/76de322a-d860-4894-9945-f0cc5d6a45f8'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.4.1.1
|
||||
|
||||
RUN apk update
|
||||
|
||||
@@ -36,22 +35,13 @@ RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS linux-headers && \
|
||||
apk del .phpize-deps
|
||||
RUN docker-php-ext-install bcmath
|
||||
|
||||
# Install APCu extension
|
||||
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
|
||||
RUN mkdir -p /usr/src/php/ext/apcu \
|
||||
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1 \
|
||||
&& docker-php-ext-configure apcu \
|
||||
&& docker-php-ext-install apcu \
|
||||
&& rm /tmp/apcu.tar.gz \
|
||||
&& rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini \
|
||||
&& echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
|
||||
|
||||
# Install pcov and sqlsrv driver
|
||||
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
# Install xdebug and sqlsrv driver
|
||||
RUN apk add --update linux-headers && \
|
||||
wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
|
||||
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
|
||||
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
|
||||
docker-php-ext-enable pdo_sqlsrv pcov && \
|
||||
pecl install pdo_sqlsrv-${PDO_SQLSRV_VERSION} xdebug && \
|
||||
docker-php-ext-enable pdo_sqlsrv xdebug && \
|
||||
apk del .phpize-deps && \
|
||||
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
|
||||
|
||||
@@ -72,5 +62,7 @@ CMD \
|
||||
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
|
||||
# Download roadrunner binary
|
||||
if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; fi && \
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until ./bin/rr serve -c config/roadrunner/.rr.dev.yml; do sleep 1 ; done
|
||||
# Create env file if it does not exist yet
|
||||
if [[ ! -f "./config/params/shlink_dev_env.php" ]]; then cp ./config/params/shlink_dev_env.php.dist ./config/params/shlink_dev_env.php ; fi && \
|
||||
# Run with `exec` so that signals are properly handled
|
||||
exec ./bin/rr serve -c config/roadrunner/.rr.dev.yml
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
services:
|
||||
shlink_db_mysql:
|
||||
user: '0'
|
||||
environment:
|
||||
MYSQL_DATABASE: shlink_test
|
||||
|
||||
shlink_db_postgres:
|
||||
user: '0'
|
||||
environment:
|
||||
POSTGRES_DB: shlink_test
|
||||
|
||||
shlink_db_maria:
|
||||
user: '0'
|
||||
environment:
|
||||
MYSQL_DATABASE: shlink_test
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
services:
|
||||
shlink_php:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_roadrunner:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_db_mysql:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_db_postgres:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
|
||||
shlink_db_maria:
|
||||
user: 1000:1000
|
||||
volumes:
|
||||
- /etc/passwd:/etc/passwd:ro
|
||||
- /etc/group:/etc/group:ro
|
||||
@@ -13,6 +13,7 @@ services:
|
||||
|
||||
shlink_php:
|
||||
container_name: shlink_php
|
||||
user: 1000:1000
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/php.Dockerfile
|
||||
@@ -34,11 +35,13 @@ services:
|
||||
- shlink_matomo
|
||||
environment:
|
||||
LC_ALL: C
|
||||
DEFAULT_DOMAIN: localhost:8000
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
|
||||
shlink_roadrunner:
|
||||
container_name: shlink_roadrunner
|
||||
user: 1000:1000
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./data/infra/roadrunner.Dockerfile
|
||||
@@ -65,6 +68,7 @@ services:
|
||||
|
||||
shlink_db_mysql:
|
||||
container_name: shlink_db_mysql
|
||||
user: 1000:1000
|
||||
image: mysql:8.0
|
||||
ports:
|
||||
- "3307:3306"
|
||||
@@ -77,6 +81,7 @@ services:
|
||||
|
||||
shlink_db_postgres:
|
||||
container_name: shlink_db_postgres
|
||||
user: 1000:1000
|
||||
image: postgres:16.3-alpine
|
||||
ports:
|
||||
- "5434:5432"
|
||||
@@ -90,6 +95,7 @@ services:
|
||||
|
||||
shlink_db_maria:
|
||||
container_name: shlink_db_maria
|
||||
user: 1000:1000
|
||||
image: mariadb:10.7
|
||||
ports:
|
||||
- "3308:3306"
|
||||
@@ -112,13 +118,13 @@ services:
|
||||
|
||||
shlink_redis:
|
||||
container_name: shlink_redis
|
||||
image: redis:6.2-alpine
|
||||
image: redis:7.4-alpine
|
||||
ports:
|
||||
- "6380:6379"
|
||||
|
||||
shlink_redis_acl:
|
||||
container_name: shlink_redis_acl
|
||||
image: redis:6.2-alpine
|
||||
image: redis:7.4-alpine
|
||||
command: ["redis-server", "/usr/local/etc/redis/redis.conf"]
|
||||
ports:
|
||||
- "6382:6379"
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink;
|
||||
|
||||
use Shlinkio\Shlink\Common\Logger\LoggerType;
|
||||
|
||||
return [
|
||||
|
||||
'logger' => [
|
||||
'Shlink' => [
|
||||
'type' => LoggerType::STREAM->value,
|
||||
'destination' => 'php://stderr',
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
@@ -26,5 +26,6 @@ fi
|
||||
php vendor/bin/shlink-installer init ${flags}
|
||||
|
||||
if [ "$SHLINK_RUNTIME" = 'rr' ]; then
|
||||
./bin/rr serve -c config/roadrunner/.rr.yml
|
||||
# Run with `exec` so that signals are properly handled
|
||||
exec ./bin/rr serve -c config/roadrunner/.rr.yml
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
# Handle dev and tests config via env vars instead of local config files
|
||||
|
||||
* Status: Accepted
|
||||
* Date: 2024-10-24
|
||||
|
||||
## Context and problem statement
|
||||
|
||||
Due to the tools used by Shlink (Zend Expressive first and Mezzio later), configuration has always been handled via the config aggregator, which is a package that continues with Zend Framework 2 config management philosophy:
|
||||
|
||||
1. Define multiple config files, scoped to their own context, that are merge at runtime.
|
||||
2. Overwrite with so-called "local" config files, which define values used only during development, and should not be shipped to production.
|
||||
|
||||
However, since Shlink started to support other runtimes and added an official docker image, env vars have started to become a central part of the config definition system.
|
||||
|
||||
That has evolved into a system where production config can be read from env vars, but dev config is expected to be defined via local config files, forcing to maintain two approaches to load config that need to coexist.
|
||||
|
||||
On top of that, keeping dev configs in multiple files makes it harder to keep track of everything.
|
||||
|
||||
Because of that, I'm proposing to switch to an env-var-based approach for dev custom configs, and get rid of local config files.
|
||||
|
||||
## Considered options
|
||||
|
||||
1. Define dev env vars in a single `.env` file which is loaded to containers via docker compose `env-file` option.
|
||||
2. Define dev env vars in a single `.env` file which is loaded via RoadRunner config.
|
||||
3. Define dev env vars in a single PHP file returning a map that's then loaded with `loadEnvVarsFromConfig`.
|
||||
4. Keep local config files and don't change anything.
|
||||
|
||||
## Decision outcome
|
||||
|
||||
Defining env vars in a PHP file has the benefit that any change will take effect immediately, so the decision is to go with option 3.
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
### 1 - .env file via docker compose
|
||||
|
||||
* Good: because it does not require any special mechanism to feed the env vars into the app.
|
||||
* Good: because it's a standard format known by many.
|
||||
* Bad: because dev config gets leaked to tests when run inside the container, breaking some existing ones, and forcing to remember this for future tests.
|
||||
* Bad: because any change to the env file requires the containers to be manually restarted, or putting some new mechanism in place to restart them automatically.
|
||||
|
||||
### 2 - .env file via RoadRunner
|
||||
|
||||
* Good: because it does not require any special mechanism to feed the env vars into the app.
|
||||
* Good: because it's a standard format known by many.
|
||||
* Good: because dev config does not get leaked into tests.
|
||||
* Bad: because any change to the env file requires the containers to be manually restarted, or putting some new mechanism in place to restart them automatically.
|
||||
|
||||
### 3 - PHP file via `loadEnvVarsFromConfig`
|
||||
|
||||
* Good: because the existing call to `loadEnvVarsFromConfig` can be reused by tweaking a bit the glob pattern, so no new dependencies are needed.
|
||||
* Good: because dev config does not get leaked into tests, and test-specific env vars can be fed using the same mechanism.
|
||||
* Good: because changes are picked up instantly by both RoadRunner and php-fpm.
|
||||
* Good: because env vars can be imported from `EnvVars` class, removing the chances of human mistakes and typos.
|
||||
* Bad: because people not familiar with the project may not expect env vars to be defined in that format.
|
||||
|
||||
### 4 - keep local config
|
||||
|
||||
* Good: because no changes are needed in the project.
|
||||
* Bad: because managing multiple local config files makes things harder to maintain.
|
||||
* Bad: because setting-up the project from scratch requires more steps, or an external package to handle config files.
|
||||
* Bad: because the project needs to keep two ways to load dev configs, and reading an env var does not warranty you are getting the single source of truth.
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
Here listed you will find the different architectural decisions taken in the project, including all the reasoning behind it, options considered, and final outcome.
|
||||
|
||||
* [2023-07-09Build `latest` docker image only for actual releases](2023-07-09-build-latest-docker-image-only-for-actual-releases.md)
|
||||
* [2024-10-24 Handle dev and tests config via env vars instead of local config files](2024-10-24-handle-dev-and-tests-config-via-env-vars-instead-of-local-config-files.md)
|
||||
* [2023-07-09 Build `latest` docker image only for actual releases](2023-07-09-build-latest-docker-image-only-for-actual-releases.md)
|
||||
* [2023-01-06 Support any HTTP method in short URLs](2023-01-06-support-any-http-method-in-short-urls.md)
|
||||
* [2022-08-05 Support multi-segment custom slugs](2022-08-05-support-multi-segment-custom-slugs.md)
|
||||
* [2022-01-15 Update env vars behavior to have precedence over installer options](2022-01-15-update-env-vars-behavior-to-have-precedence-over-installer-options.md)
|
||||
|
||||
@@ -141,6 +141,14 @@
|
||||
"crawlable": {
|
||||
"type": "boolean",
|
||||
"description": "Tells if this URL will be included as 'Allow' in Shlink's robots.txt."
|
||||
},
|
||||
"forwardQuery": {
|
||||
"type": "boolean",
|
||||
"description": "Tells if this URL will forward the query params to the long URL when visited, as explained in [the docs](https://shlink.io/documentation/some-features/#query-params-forwarding)."
|
||||
},
|
||||
"hasRedirectRules": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this short URL has redirect rules attached to it or not. Use [this endpoint](https://api-spec.shlink.io/#/Redirect%20rules/listShortUrlRedirectRules) to get the actual list of rules."
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
@@ -164,7 +172,9 @@
|
||||
},
|
||||
"domain": "example.com",
|
||||
"title": "The title",
|
||||
"crawlable": false
|
||||
"crawlable": false,
|
||||
"forwardQuery": false,
|
||||
"hasRedirectRules": true
|
||||
}
|
||||
},
|
||||
"ShortUrlMeta": {
|
||||
@@ -237,6 +247,11 @@
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The originally visited URL that triggered the tracking of this visit"
|
||||
},
|
||||
"redirectUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "The URL to which the visitor was redirected, or null if a redirect did not occur, like for 404 requests or pixel tracking"
|
||||
}
|
||||
},
|
||||
"example": {
|
||||
|
||||
@@ -15,7 +15,14 @@
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["device", "language", "query-param", "ip-address"],
|
||||
"enum": [
|
||||
"device",
|
||||
"language",
|
||||
"query-param",
|
||||
"ip-address",
|
||||
"geolocation-country-code",
|
||||
"geolocation-city-name"
|
||||
],
|
||||
"description": "The type of the condition, which will determine the logic used to match it"
|
||||
},
|
||||
"matchKey": {
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"domain",
|
||||
"title",
|
||||
"crawlable",
|
||||
"forwardQuery"
|
||||
"forwardQuery",
|
||||
"hasRedirectRules"
|
||||
],
|
||||
"properties": {
|
||||
"shortCode": {
|
||||
@@ -59,6 +60,10 @@
|
||||
"forwardQuery": {
|
||||
"type": "boolean",
|
||||
"description": "Tells if this URL will forward the query params to the long URL when visited, as explained in [the docs](https://shlink.io/documentation/some-features/#query-params-forwarding)."
|
||||
},
|
||||
"hasRedirectRules": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this short URL has redirect rules attached to it or not. Use [this endpoint](https://api-spec.shlink.io/#/Redirect%20rules/listShortUrlRedirectRules) to get the actual list of rules."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
"visitedUrl": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The originally visited URL that triggered the tracking of this visit"
|
||||
},
|
||||
"redirectUrl": {
|
||||
"type": ["string", "null"],
|
||||
"description": "The URL to which the visitor was redirected, or null if a redirect did not occur, like for 404 requests or pixel tracking"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +125,15 @@
|
||||
"false"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "domain",
|
||||
"in": "query",
|
||||
"description": "Get short URLs for this particular domain only. Use **DEFAULT** keyword for default domain.",
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"security": [
|
||||
@@ -180,7 +189,9 @@
|
||||
},
|
||||
"domain": null,
|
||||
"title": "Welcome to Steam",
|
||||
"crawlable": false
|
||||
"crawlable": false,
|
||||
"forwardQuery": true,
|
||||
"hasRedirectRules": true
|
||||
},
|
||||
{
|
||||
"shortCode": "12Kb3",
|
||||
@@ -202,7 +213,9 @@
|
||||
},
|
||||
"domain": null,
|
||||
"title": null,
|
||||
"crawlable": false
|
||||
"crawlable": false,
|
||||
"forwardQuery": true,
|
||||
"hasRedirectRules": false
|
||||
},
|
||||
{
|
||||
"shortCode": "123bA",
|
||||
@@ -222,7 +235,9 @@
|
||||
},
|
||||
"domain": "example.com",
|
||||
"title": null,
|
||||
"crawlable": false
|
||||
"crawlable": false,
|
||||
"forwardQuery": false,
|
||||
"hasRedirectRules": true
|
||||
}
|
||||
],
|
||||
"pagination": {
|
||||
@@ -337,7 +352,9 @@
|
||||
},
|
||||
"domain": null,
|
||||
"title": null,
|
||||
"crawlable": false
|
||||
"crawlable": false,
|
||||
"forwardQuery": true,
|
||||
"hasRedirectRules": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,9 @@
|
||||
},
|
||||
"domain": null,
|
||||
"title": null,
|
||||
"crawlable": false
|
||||
"crawlable": false,
|
||||
"forwardQuery": true,
|
||||
"hasRedirectRules": false
|
||||
}
|
||||
},
|
||||
"text/plain": {
|
||||
|
||||
@@ -50,7 +50,9 @@
|
||||
},
|
||||
"domain": null,
|
||||
"title": null,
|
||||
"crawlable": false
|
||||
"crawlable": false,
|
||||
"forwardQuery": true,
|
||||
"hasRedirectRules": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +165,9 @@
|
||||
},
|
||||
"domain": null,
|
||||
"title": "Shlink - The URL shortener",
|
||||
"crawlable": false
|
||||
"crawlable": false,
|
||||
"forwardQuery": false,
|
||||
"hasRedirectRules": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ return [
|
||||
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,
|
||||
Command\Api\ListKeysCommand::NAME => Command\Api\ListKeysCommand::class,
|
||||
Command\Api\InitialApiKeyCommand::NAME => Command\Api\InitialApiKeyCommand::class,
|
||||
Command\Api\RenameApiKeyCommand::NAME => Command\Api\RenameApiKeyCommand::class,
|
||||
|
||||
Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class,
|
||||
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
|
||||
|
||||
@@ -7,10 +7,10 @@ namespace Shlinkio\Shlink\CLI;
|
||||
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
|
||||
use Laminas\ServiceManager\Factory\InvokableFactory;
|
||||
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
|
||||
use Shlinkio\Shlink\Core\Config\Options\TrackingOptions;
|
||||
use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainService;
|
||||
use Shlinkio\Shlink\Core\Matomo;
|
||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleService;
|
||||
use Shlinkio\Shlink\Core\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
@@ -59,6 +59,7 @@ return [
|
||||
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\ListKeysCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\InitialApiKeyCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Api\RenameApiKeyCommand::class => ConfigAbstractFactory::class,
|
||||
|
||||
Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class,
|
||||
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
|
||||
@@ -88,7 +89,7 @@ return [
|
||||
TrackingOptions::class,
|
||||
],
|
||||
Util\ProcessRunner::class => [SymfonyCli\Helper\ProcessHelper::class],
|
||||
ApiKey\RoleResolver::class => [DomainService::class, 'config.url_shortener.domain.hostname'],
|
||||
ApiKey\RoleResolver::class => [DomainService::class, UrlShortenerOptions::class],
|
||||
|
||||
Command\ShortUrl\CreateShortUrlCommand::class => [
|
||||
ShortUrl\UrlShortener::class,
|
||||
@@ -120,6 +121,7 @@ return [
|
||||
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\ListKeysCommand::class => [ApiKeyService::class],
|
||||
Command\Api\InitialApiKeyCommand::class => [ApiKeyService::class],
|
||||
Command\Api\RenameApiKeyCommand::class => [ApiKeyService::class],
|
||||
|
||||
Command\Tag\ListTagsCommand::class => [TagService::class],
|
||||
Command\Tag\RenameTagCommand::class => [TagService::class],
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\CLI\ApiKey;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
|
||||
use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||
@@ -12,11 +13,11 @@ use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
use function is_string;
|
||||
|
||||
class RoleResolver implements RoleResolverInterface
|
||||
readonly class RoleResolver implements RoleResolverInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DomainServiceInterface $domainService,
|
||||
private readonly string $defaultDomain,
|
||||
private DomainServiceInterface $domainService,
|
||||
private UrlShortenerOptions $urlShortenerOptions,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ class RoleResolver implements RoleResolverInterface
|
||||
|
||||
private function resolveRoleForAuthority(string $domainAuthority): RoleDefinition
|
||||
{
|
||||
if ($domainAuthority === $this->defaultDomain) {
|
||||
if ($domainAuthority === $this->urlShortenerOptions->defaultDomain) {
|
||||
throw InvalidRoleConfigException::forDomainOnlyWithDefaultDomain();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,39 +6,99 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\map;
|
||||
use function sprintf;
|
||||
|
||||
class DisableKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:disable';
|
||||
|
||||
public function __construct(private ApiKeyServiceInterface $apiKeyService)
|
||||
public function __construct(private readonly ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName(self::NAME)
|
||||
->setDescription('Disables an API key.')
|
||||
->addArgument('apiKey', InputArgument::REQUIRED, 'The API key to disable');
|
||||
$help = <<<HELP
|
||||
The <info>%command.name%</info> command allows you to disable an existing API key, via its name or the
|
||||
plain-text key.
|
||||
|
||||
If no arguments are provided, you will be prompted to select one of the existing non-disabled API keys.
|
||||
|
||||
<info>%command.full_name%</info>
|
||||
|
||||
You can optionally pass the API key name to be disabled. In that case <comment>--by-name</comment> is also
|
||||
required, to indicate the first argument is the API key name and not the plain-text key:
|
||||
|
||||
<info>%command.full_name% the_key_name --by-name</info>
|
||||
|
||||
You can pass the plain-text key to be disabled, but that is <options=bold>DEPRECATED</>. In next major version,
|
||||
the argument will always be assumed to be the name:
|
||||
|
||||
<info>%command.full_name% d6b6c60e-edcd-4e43-96ad-fa6b7014c143</info>
|
||||
|
||||
HELP;
|
||||
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Disables an API key by name or plain-text key (providing a plain-text key is DEPRECATED)')
|
||||
->addArgument(
|
||||
'keyOrName',
|
||||
InputArgument::OPTIONAL,
|
||||
'The API key to disable. Pass `--by-name` to indicate this value is the name and not the key.',
|
||||
)
|
||||
->addOption(
|
||||
'by-name',
|
||||
mode: InputOption::VALUE_NONE,
|
||||
description: 'Indicates the first argument is the API key name, not the plain-text key.',
|
||||
)
|
||||
->setHelp($help);
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$keyOrName = $input->getArgument('keyOrName');
|
||||
|
||||
if ($keyOrName === null) {
|
||||
$apiKeys = $this->apiKeyService->listKeys(enabledOnly: true);
|
||||
$name = (new SymfonyStyle($input, $output))->choice(
|
||||
'What API key do you want to disable?',
|
||||
map($apiKeys, static fn (ApiKey $apiKey) => $apiKey->name),
|
||||
);
|
||||
|
||||
$input->setArgument('keyOrName', $name);
|
||||
$input->setOption('by-name', true);
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$apiKey = $input->getArgument('apiKey');
|
||||
$keyOrName = $input->getArgument('keyOrName');
|
||||
$byName = $input->getOption('by-name');
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
if (! $keyOrName) {
|
||||
$io->warning('An API key name was not provided.');
|
||||
return ExitCode::EXIT_WARNING;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->apiKeyService->disable($apiKey);
|
||||
$io->success(sprintf('API key "%s" properly disabled', $apiKey));
|
||||
if ($byName) {
|
||||
$this->apiKeyService->disableByName($keyOrName);
|
||||
} else {
|
||||
$this->apiKeyService->disableByKey($keyOrName);
|
||||
}
|
||||
$io->success(sprintf('API key "%s" properly disabled', $keyOrName));
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (InvalidArgumentException $e) {
|
||||
$io->error($e->getMessage());
|
||||
|
||||
@@ -100,23 +100,26 @@ class GenerateKeyCommand extends Command
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$expirationDate = $input->getOption('expiration-date');
|
||||
|
||||
$apiKey = $this->apiKeyService->create(ApiKeyMeta::fromParams(
|
||||
$apiKeyMeta = ApiKeyMeta::fromParams(
|
||||
name: $input->getOption('name'),
|
||||
expirationDate: isset($expirationDate) ? Chronos::parse($expirationDate) : null,
|
||||
roleDefinitions: $this->roleResolver->determineRoles($input),
|
||||
));
|
||||
);
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->success(sprintf('Generated API key: "%s"', $apiKey->toString()));
|
||||
$apiKey = $this->apiKeyService->create($apiKeyMeta);
|
||||
$io->success(sprintf('Generated API key: "%s"', $apiKeyMeta->key));
|
||||
|
||||
if ($input->isInteractive()) {
|
||||
$io->warning('Save the key in a secure location. You will not be able to get it afterwards.');
|
||||
}
|
||||
|
||||
if (! ApiKey::isAdmin($apiKey)) {
|
||||
ShlinkTable::default($io)->render(
|
||||
['Role name', 'Role metadata'],
|
||||
$apiKey->mapRoles(fn (Role $role, array $meta) => [$role->value, arrayToString($meta, 0)]),
|
||||
null,
|
||||
'Roles',
|
||||
$apiKey->mapRoles(fn (Role $role, array $meta) => [$role->value, arrayToString($meta, indentSize: 0)]),
|
||||
headerTitle: 'Roles',
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class ListKeysCommand extends Command
|
||||
$messagePattern = $this->determineMessagePattern($apiKey);
|
||||
|
||||
// Set columns for this row
|
||||
$rowData = [sprintf($messagePattern, $apiKey), sprintf($messagePattern, $apiKey->name ?? '-')];
|
||||
$rowData = [sprintf($messagePattern, $apiKey->name ?? '-')];
|
||||
if (! $enabledOnly) {
|
||||
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
|
||||
}
|
||||
@@ -67,7 +67,6 @@ class ListKeysCommand extends Command
|
||||
}, $this->apiKeyService->listKeys($enabledOnly));
|
||||
|
||||
ShlinkTable::withRowSeparators($output)->render(array_filter([
|
||||
'Key',
|
||||
'Name',
|
||||
! $enabledOnly ? 'Is enabled' : null,
|
||||
'Expiration date',
|
||||
|
||||
77
module/CLI/src/Command/Api/RenameApiKeyCommand.php
Normal file
77
module/CLI/src/Command/Api/RenameApiKeyCommand.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Api;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Core\Model\Renaming;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\map;
|
||||
|
||||
class RenameApiKeyCommand extends Command
|
||||
{
|
||||
public const NAME = 'api-key:rename';
|
||||
|
||||
public function __construct(private readonly ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Renames an API key by name')
|
||||
->addArgument('oldName', InputArgument::REQUIRED, 'Current name of the API key to rename')
|
||||
->addArgument('newName', InputArgument::REQUIRED, 'New name to set to the API key');
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$oldName = $input->getArgument('oldName');
|
||||
$newName = $input->getArgument('newName');
|
||||
|
||||
if ($oldName === null) {
|
||||
$apiKeys = $this->apiKeyService->listKeys();
|
||||
$requestedOldName = $io->choice(
|
||||
'What API key do you want to rename?',
|
||||
map($apiKeys, static fn (ApiKey $apiKey) => $apiKey->name),
|
||||
);
|
||||
|
||||
$input->setArgument('oldName', $requestedOldName);
|
||||
}
|
||||
|
||||
if ($newName === null) {
|
||||
$requestedNewName = $io->ask(
|
||||
'What is the new name you want to set?',
|
||||
validator: static fn (string|null $value): string => $value !== null
|
||||
? $value
|
||||
: throw new InvalidArgumentException('The new name cannot be empty'),
|
||||
);
|
||||
|
||||
$input->setArgument('newName', $requestedNewName);
|
||||
}
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$oldName = $input->getArgument('oldName');
|
||||
$newName = $input->getArgument('newName');
|
||||
|
||||
$this->apiKeyService->renameApiKey(Renaming::fromNames($oldName, $newName));
|
||||
$io->success('API key properly renamed');
|
||||
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class ReadEnvVarCommand extends Command
|
||||
/** @var Closure(string $envVar): mixed */
|
||||
private readonly Closure $loadEnvVar;
|
||||
|
||||
public function __construct(?Closure $loadEnvVar = null)
|
||||
public function __construct(Closure|null $loadEnvVar = null)
|
||||
{
|
||||
$this->loadEnvVar = $loadEnvVar ?? static fn (string $envVar) => EnvVars::from($envVar)->loadFromEnv();
|
||||
parent::__construct();
|
||||
|
||||
@@ -74,7 +74,7 @@ class DomainRedirectsCommand extends Command
|
||||
$domainAuthority = $input->getArgument('domain');
|
||||
$domain = $this->domainService->findByAuthority($domainAuthority);
|
||||
|
||||
$ask = static function (string $message, ?string $current) use ($io): ?string {
|
||||
$ask = static function (string $message, string|null $current) use ($io): string|null {
|
||||
if ($current === null) {
|
||||
return $io->ask(sprintf('%s (Leave empty for no redirect)', $message));
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Input\ShortUrlDataInput;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
@@ -22,7 +22,7 @@ class CreateShortUrlCommand extends Command
|
||||
{
|
||||
public const NAME = 'short-url:create';
|
||||
|
||||
private ?SymfonyStyle $io;
|
||||
private SymfonyStyle $io;
|
||||
private readonly ShortUrlDataInput $shortUrlDataInput;
|
||||
|
||||
public function __construct(
|
||||
|
||||
@@ -10,9 +10,10 @@ use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
|
||||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtils;
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlWithVisitsSummary;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlWithDeps;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlsParamsInputFilter;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
||||
@@ -64,6 +65,12 @@ class ListShortUrlsCommand extends Command
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'A query used to filter results by searching for it on the longUrl and shortCode fields.',
|
||||
)
|
||||
->addOption(
|
||||
'domain',
|
||||
'd',
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Used to filter results by domain. Use DEFAULT keyword to filter by default domain',
|
||||
)
|
||||
->addOption(
|
||||
'tags',
|
||||
't',
|
||||
@@ -111,14 +118,9 @@ class ListShortUrlsCommand extends Command
|
||||
'show-api-key',
|
||||
'k',
|
||||
InputOption::VALUE_NONE,
|
||||
'Whether to display the API key from which the URL was generated or not.',
|
||||
)
|
||||
->addOption(
|
||||
'show-api-key-name',
|
||||
'm',
|
||||
InputOption::VALUE_NONE,
|
||||
'Whether to display the API key name from which the URL was generated or not.',
|
||||
)
|
||||
->addOption('show-api-key-name', 'm', InputOption::VALUE_NONE, '[DEPRECATED] Use show-api-key')
|
||||
->addOption(
|
||||
'all',
|
||||
'a',
|
||||
@@ -134,6 +136,7 @@ class ListShortUrlsCommand extends Command
|
||||
|
||||
$page = (int) $input->getOption('page');
|
||||
$searchTerm = $input->getOption('search-term');
|
||||
$domain = $input->getOption('domain');
|
||||
$tags = $input->getOption('tags');
|
||||
$tagsMode = $input->getOption('including-all-tags') === true ? TagsMode::ALL->value : TagsMode::ANY->value;
|
||||
$tags = ! empty($tags) ? explode(',', $tags) : [];
|
||||
@@ -145,6 +148,7 @@ class ListShortUrlsCommand extends Command
|
||||
|
||||
$data = [
|
||||
ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
|
||||
ShortUrlsParamsInputFilter::DOMAIN => $domain,
|
||||
ShortUrlsParamsInputFilter::TAGS => $tags,
|
||||
ShortUrlsParamsInputFilter::TAGS_MODE => $tagsMode,
|
||||
ShortUrlsParamsInputFilter::ORDER_BY => $orderBy,
|
||||
@@ -177,7 +181,7 @@ class ListShortUrlsCommand extends Command
|
||||
|
||||
/**
|
||||
* @param array<string, callable(array $serializedShortUrl, ShortUrl $shortUrl): ?string> $columnsMap
|
||||
* @return Paginator<ShortUrlWithVisitsSummary>
|
||||
* @return Paginator<ShortUrlWithDeps>
|
||||
*/
|
||||
private function renderPage(
|
||||
OutputInterface $output,
|
||||
@@ -187,7 +191,7 @@ class ListShortUrlsCommand extends Command
|
||||
): Paginator {
|
||||
$shortUrls = $this->shortUrlService->listShortUrls($params);
|
||||
|
||||
$rows = map([...$shortUrls], function (ShortUrlWithVisitsSummary $shortUrl) use ($columnsMap) {
|
||||
$rows = map([...$shortUrls], function (ShortUrlWithDeps $shortUrl) use ($columnsMap) {
|
||||
$serializedShortUrl = $this->transformer->transform($shortUrl);
|
||||
return map($columnsMap, fn (callable $call) => $call($serializedShortUrl, $shortUrl->shortUrl));
|
||||
});
|
||||
@@ -201,7 +205,7 @@ class ListShortUrlsCommand extends Command
|
||||
return $shortUrls;
|
||||
}
|
||||
|
||||
private function processOrderBy(InputInterface $input): ?string
|
||||
private function processOrderBy(InputInterface $input): string|null
|
||||
{
|
||||
$orderBy = $input->getOption('order-by');
|
||||
if (empty($orderBy)) {
|
||||
@@ -231,14 +235,10 @@ class ListShortUrlsCommand extends Command
|
||||
}
|
||||
if ($input->getOption('show-domain')) {
|
||||
$columnsMap['Domain'] = static fn (array $_, ShortUrl $shortUrl): string =>
|
||||
$shortUrl->getDomain()?->authority ?? 'DEFAULT';
|
||||
$shortUrl->getDomain()->authority ?? Domain::DEFAULT_AUTHORITY;
|
||||
}
|
||||
if ($input->getOption('show-api-key')) {
|
||||
$columnsMap['API Key'] = static fn (array $_, ShortUrl $shortUrl): string =>
|
||||
$shortUrl->authorApiKey?->__toString() ?? '';
|
||||
}
|
||||
if ($input->getOption('show-api-key-name')) {
|
||||
$columnsMap['API Key Name'] = static fn (array $_, ShortUrl $shortUrl): ?string =>
|
||||
if ($input->getOption('show-api-key') || $input->getOption('show-api-key-name')) {
|
||||
$columnsMap['API Key Name'] = static fn (array $_, ShortUrl $shortUrl): string|null =>
|
||||
$shortUrl->authorApiKey?->name;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Shlinkio\Shlink\CLI\Command\Tag;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Exception\TagConflictException;
|
||||
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
|
||||
use Shlinkio\Shlink\Core\Model\Renaming;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -40,7 +40,7 @@ class RenameTagCommand extends Command
|
||||
$newName = $input->getArgument('newName');
|
||||
|
||||
try {
|
||||
$this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName));
|
||||
$this->tagService->renameTag(Renaming::fromNames($oldName, $newName));
|
||||
$io->success('Tag properly renamed.');
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (TagNotFoundException | TagConflictException $e) {
|
||||
|
||||
@@ -61,8 +61,8 @@ abstract class AbstractVisitsListCommand extends Command
|
||||
'date' => $visit->date->toAtomString(),
|
||||
'userAgent' => $visit->userAgent,
|
||||
'potentialBot' => $visit->potentialBot,
|
||||
'country' => $visit->getVisitLocation()?->countryName ?? 'Unknown',
|
||||
'city' => $visit->getVisitLocation()?->cityName ?? 'Unknown',
|
||||
'country' => $visit->getVisitLocation()->countryName ?? 'Unknown',
|
||||
'city' => $visit->getVisitLocation()->cityName ?? 'Unknown',
|
||||
...$extraFields,
|
||||
];
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ class DownloadGeoLiteDbCommand extends Command
|
||||
{
|
||||
public const NAME = 'visit:download-db';
|
||||
|
||||
private ?ProgressBar $progressBar = null;
|
||||
private ProgressBar|null $progressBar = null;
|
||||
|
||||
public function __construct(private GeolocationDbUpdaterInterface $dbUpdater)
|
||||
{
|
||||
|
||||
@@ -13,12 +13,12 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
||||
{
|
||||
private bool $olderDbExists;
|
||||
|
||||
private function __construct(string $message, ?Throwable $previous = null)
|
||||
private function __construct(string $message, Throwable|null $previous = null)
|
||||
{
|
||||
parent::__construct($message, previous: $previous);
|
||||
}
|
||||
|
||||
public static function withOlderDb(?Throwable $prev = null): self
|
||||
public static function withOlderDb(Throwable|null $prev = null): self
|
||||
{
|
||||
$e = new self(
|
||||
'An error occurred while updating geolocation database, but an older DB is already present.',
|
||||
@@ -29,7 +29,7 @@ class GeolocationDbUpdateFailedException extends RuntimeException implements Exc
|
||||
return $e;
|
||||
}
|
||||
|
||||
public static function withoutOlderDb(?Throwable $prev = null): self
|
||||
public static function withoutOlderDb(Throwable|null $prev = null): self
|
||||
{
|
||||
$e = new self(
|
||||
'An error occurred while updating geolocation database, and an older version could not be found.',
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\CLI\Factory;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use Shlinkio\Shlink\Core\Config\Options\AppOptions;
|
||||
use Symfony\Component\Console\Application as CliApp;
|
||||
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ use Closure;
|
||||
use GeoIp2\Database\Reader;
|
||||
use MaxMind\Db\Reader\Metadata;
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||
use Shlinkio\Shlink\Core\Config\Options\TrackingOptions;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\MissingLicenseException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
@@ -40,9 +40,11 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
public function checkDbUpdate(?callable $beforeDownload = null, ?callable $handleProgress = null): GeolocationResult
|
||||
{
|
||||
if ($this->trackingOptions->disableTracking || $this->trackingOptions->disableIpTracking) {
|
||||
public function checkDbUpdate(
|
||||
callable|null $beforeDownload = null,
|
||||
callable|null $handleProgress = null,
|
||||
): GeolocationResult {
|
||||
if (! $this->trackingOptions->isGeolocationRelevant()) {
|
||||
return GeolocationResult::CHECK_SKIPPED;
|
||||
}
|
||||
|
||||
@@ -59,7 +61,7 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||
/**
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
private function downloadIfNeeded(?callable $beforeDownload, ?callable $handleProgress): GeolocationResult
|
||||
private function downloadIfNeeded(callable|null $beforeDownload, callable|null $handleProgress): GeolocationResult
|
||||
{
|
||||
if (! $this->dbUpdater->databaseFileExists()) {
|
||||
return $this->downloadNewDb(false, $beforeDownload, $handleProgress);
|
||||
@@ -105,8 +107,8 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||
*/
|
||||
private function downloadNewDb(
|
||||
bool $olderDbExists,
|
||||
?callable $beforeDownload,
|
||||
?callable $handleProgress,
|
||||
callable|null $beforeDownload,
|
||||
callable|null $handleProgress,
|
||||
): GeolocationResult {
|
||||
if ($beforeDownload !== null) {
|
||||
$beforeDownload($olderDbExists);
|
||||
@@ -124,7 +126,7 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
||||
}
|
||||
}
|
||||
|
||||
private function wrapHandleProgressCallback(?callable $handleProgress, bool $olderDbExists): ?callable
|
||||
private function wrapHandleProgressCallback(callable|null $handleProgress, bool $olderDbExists): callable|null
|
||||
{
|
||||
if ($handleProgress === null) {
|
||||
return null;
|
||||
|
||||
@@ -12,7 +12,7 @@ interface GeolocationDbUpdaterInterface
|
||||
* @throws GeolocationDbUpdateFailedException
|
||||
*/
|
||||
public function checkDbUpdate(
|
||||
?callable $beforeDownload = null,
|
||||
?callable $handleProgress = null,
|
||||
callable|null $beforeDownload = null,
|
||||
callable|null $handleProgress = null,
|
||||
): GeolocationResult;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ readonly class DateOption
|
||||
$command->addOption($name, $shortcut, InputOption::VALUE_REQUIRED, $description);
|
||||
}
|
||||
|
||||
public function get(InputInterface $input, OutputInterface $output): ?Chronos
|
||||
public function get(InputInterface $input, OutputInterface $output): Chronos|null
|
||||
{
|
||||
$value = $input->getOption($this->name);
|
||||
if (empty($value) || ! is_string($value)) {
|
||||
|
||||
@@ -11,7 +11,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
readonly final class EndDateOption
|
||||
final readonly class EndDateOption
|
||||
{
|
||||
private DateOption $dateOption;
|
||||
|
||||
@@ -23,7 +23,7 @@ readonly final class EndDateOption
|
||||
));
|
||||
}
|
||||
|
||||
public function get(InputInterface $input, OutputInterface $output): ?Chronos
|
||||
public function get(InputInterface $input, OutputInterface $output): Chronos|null
|
||||
{
|
||||
return $this->dateOption->get($input, $output);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
@@ -18,7 +18,7 @@ use function array_unique;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\flatten;
|
||||
use function Shlinkio\Shlink\Core\splitByComma;
|
||||
|
||||
readonly final class ShortUrlDataInput
|
||||
final readonly class ShortUrlDataInput
|
||||
{
|
||||
public function __construct(Command $command, private bool $longUrlAsOption = false)
|
||||
{
|
||||
|
||||
@@ -18,7 +18,7 @@ enum ShortUrlDataOption: string
|
||||
case CRAWLABLE = 'crawlable';
|
||||
case NO_FORWARD_QUERY = 'no-forward-query';
|
||||
|
||||
public function shortcut(): ?string
|
||||
public function shortcut(): string|null
|
||||
{
|
||||
return match ($this) {
|
||||
self::TAGS => 't',
|
||||
|
||||
@@ -10,7 +10,7 @@ use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
readonly final class ShortUrlIdentifierInput
|
||||
final readonly class ShortUrlIdentifierInput
|
||||
{
|
||||
public function __construct(Command $command, string $shortCodeDesc, string $domainDesc)
|
||||
{
|
||||
@@ -19,7 +19,7 @@ readonly final class ShortUrlIdentifierInput
|
||||
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, $domainDesc);
|
||||
}
|
||||
|
||||
public function shortCode(InputInterface $input): ?string
|
||||
public function shortCode(InputInterface $input): string|null
|
||||
{
|
||||
return $input->getArgument('shortCode');
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
readonly final class StartDateOption
|
||||
final readonly class StartDateOption
|
||||
{
|
||||
private DateOption $dateOption;
|
||||
|
||||
@@ -23,7 +23,7 @@ readonly final class StartDateOption
|
||||
));
|
||||
}
|
||||
|
||||
public function get(InputInterface $input, OutputInterface $output): ?Chronos
|
||||
public function get(InputInterface $input, OutputInterface $output): Chronos|null
|
||||
{
|
||||
return $this->dateOption->get($input, $output);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ use const STR_PAD_LEFT;
|
||||
|
||||
class RedirectRuleHandler implements RedirectRuleHandlerInterface
|
||||
{
|
||||
public function manageRules(StyleInterface $io, ShortUrl $shortUrl, array $rules): ?array
|
||||
public function manageRules(StyleInterface $io, ShortUrl $shortUrl, array $rules): array|null
|
||||
{
|
||||
$amountOfRules = count($rules);
|
||||
|
||||
@@ -111,6 +111,12 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
|
||||
RedirectConditionType::IP_ADDRESS => RedirectCondition::forIpAddress(
|
||||
$this->askMandatory('IP address, CIDR block or wildcard-pattern (1.2.*.*)', $io),
|
||||
),
|
||||
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => RedirectCondition::forGeolocationCountryCode(
|
||||
$this->askMandatory('Country code to match?', $io),
|
||||
),
|
||||
RedirectConditionType::GEOLOCATION_CITY_NAME => RedirectCondition::forGeolocationCityName(
|
||||
$this->askMandatory('City name to match?', $io),
|
||||
)
|
||||
};
|
||||
|
||||
$continue = $io->confirm('Do you want to add another condition?');
|
||||
@@ -213,7 +219,7 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
|
||||
|
||||
private function askMandatory(string $message, StyleInterface $io): string
|
||||
{
|
||||
return $io->ask($message, validator: function (?string $answer): string {
|
||||
return $io->ask($message, validator: function (string|null $answer): string {
|
||||
if ($answer === null) {
|
||||
throw new InvalidArgumentException('The value is mandatory');
|
||||
}
|
||||
@@ -223,6 +229,6 @@ class RedirectRuleHandler implements RedirectRuleHandlerInterface
|
||||
|
||||
private function askOptional(string $message, StyleInterface $io): string
|
||||
{
|
||||
return $io->ask($message, validator: fn (?string $answer) => $answer === null ? '' : trim($answer));
|
||||
return $io->ask($message, validator: fn (string|null $answer) => $answer === null ? '' : trim($answer));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,5 @@ interface RedirectRuleHandlerInterface
|
||||
* @param ShortUrlRedirectRule[] $rules
|
||||
* @return ShortUrlRedirectRule[]|null - A new list of rules to save, or null if no changes should be saved
|
||||
*/
|
||||
public function manageRules(StyleInterface $io, ShortUrl $shortUrl, array $rules): ?array;
|
||||
public function manageRules(StyleInterface $io, ShortUrl $shortUrl, array $rules): array|null;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class ProcessRunner implements ProcessRunnerInterface
|
||||
{
|
||||
private Closure $createProcess;
|
||||
|
||||
public function __construct(private ProcessHelper $helper, ?callable $createProcess = null)
|
||||
public function __construct(private ProcessHelper $helper, callable|null $createProcess = null)
|
||||
{
|
||||
$this->createProcess = $createProcess !== null
|
||||
? $createProcess(...)
|
||||
|
||||
@@ -34,8 +34,12 @@ final class ShlinkTable
|
||||
return new self($baseTable);
|
||||
}
|
||||
|
||||
public function render(array $headers, array $rows, ?string $footerTitle = null, ?string $headerTitle = null): void
|
||||
{
|
||||
public function render(
|
||||
array $headers,
|
||||
array $rows,
|
||||
string|null $footerTitle = null,
|
||||
string|null $headerTitle = null,
|
||||
): void {
|
||||
$style = Table::getStyleDefinition(self::DEFAULT_STYLE_NAME);
|
||||
$style->setFooterTitleFormat(self::TABLE_TITLE_STYLE)
|
||||
->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
|
||||
|
||||
@@ -8,6 +8,7 @@ use PHPUnit\Framework\Attributes\Test;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\CreateShortUrlCommand;
|
||||
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
|
||||
|
||||
class CreateShortUrlTest extends CliTestCase
|
||||
@@ -26,6 +27,6 @@ class CreateShortUrlTest extends CliTestCase
|
||||
self::assertStringContainsString('Generated short URL: http://' . $defaultDomain . '/' . $slug, $output);
|
||||
|
||||
[$listOutput] = $this->exec([ListShortUrlsCommand::NAME, '--show-domain', '--search-term', $slug]);
|
||||
self::assertStringContainsString('DEFAULT', $listOutput);
|
||||
self::assertStringContainsString(Domain::DEFAULT_AUTHORITY, $listOutput);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user