mirror of
https://github.com/shlinkio/shlink.git
synced 2025-02-24 19:29:04 +03:00
commit
c80ec54508
159 changed files with 2019 additions and 739 deletions
51
.github/DISCUSSION_TEMPLATE/help-wanted.yml
vendored
Normal file
51
.github/DISCUSSION_TEMPLATE/help-wanted.yml
vendored
Normal file
|
@ -0,0 +1,51 @@
|
|||
title: 'Help wanted'
|
||||
body:
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Shlink version
|
||||
placeholder: x.y.z
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: PHP version
|
||||
placeholder: x.y.z
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: How do you serve Shlink
|
||||
options:
|
||||
- Self-hosted Apache
|
||||
- Self-hosted nginx
|
||||
- Self-hosted openswoole
|
||||
- Self-hosted RoadRunner
|
||||
- Openswoole Docker image
|
||||
- RoadRunner Docker image
|
||||
- Other (explain in summary)
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Database engine
|
||||
options:
|
||||
- MySQL
|
||||
- MariaDB
|
||||
- PostgreSQL
|
||||
- MicrosoftSQL
|
||||
- SQLite
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Database version
|
||||
placeholder: x.y.z
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
value: '<!-- Describe your issue, question or request here. -->'
|
||||
|
38
.github/ISSUE_TEMPLATE/Bug.md
vendored
38
.github/ISSUE_TEMPLATE/Bug.md
vendored
|
@ -1,38 +0,0 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Something on shlink is broken or not working as documented?
|
||||
labels: bug
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
||||
-->
|
||||
|
||||
#### How Shlink is set up
|
||||
|
||||
* Shlink Version: x.y.z
|
||||
* PHP Version: x.y.z
|
||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Self-hosted RoadRunner|Openswoole Docker image|RoadRunner Docker image
|
||||
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Provide a summary describing the problem you are experiencing. -->
|
||||
|
||||
#### Current behavior
|
||||
|
||||
<!-- How is it actually behaving (and it shouldn't)? -->
|
||||
|
||||
#### Expected behavior
|
||||
|
||||
<!-- How did you expect it to behave? -->
|
||||
|
||||
#### How to reproduce
|
||||
|
||||
<!-- Provide steps to reproduce the bug. -->
|
64
.github/ISSUE_TEMPLATE/Bug.yml
vendored
Normal file
64
.github/ISSUE_TEMPLATE/Bug.yml
vendored
Normal file
|
@ -0,0 +1,64 @@
|
|||
name: Bug report
|
||||
description: Something on Shlink is broken or not working as documented?
|
||||
labels: ['bug']
|
||||
body:
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Shlink version
|
||||
placeholder: x.y.z
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: PHP version
|
||||
placeholder: x.y.z
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: How do you serve Shlink
|
||||
options:
|
||||
- Self-hosted Apache
|
||||
- Self-hosted nginx
|
||||
- Self-hosted openswoole
|
||||
- Self-hosted RoadRunner
|
||||
- Openswoole Docker image
|
||||
- RoadRunner Docker image
|
||||
- Other (explain in summary)
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Database engine
|
||||
options:
|
||||
- MySQL
|
||||
- MariaDB
|
||||
- PostgreSQL
|
||||
- MicrosoftSQL
|
||||
- SQLite
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Database version
|
||||
placeholder: x.y.z
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Current behavior
|
||||
value: '<!-- How is it actually behaving (and it should not)? -->'
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
value: '<!-- How did you expect it to behave? -->'
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: How to reproduce
|
||||
value: '<!-- Provide steps to reproduce the bug. -->'
|
19
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
19
.github/ISSUE_TEMPLATE/Feature_Request.md
vendored
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Do you find shlink is missing some important feature that would make it more useful?
|
||||
labels: feature
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
||||
-->
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Describe the new feature you would like to request. -->
|
16
.github/ISSUE_TEMPLATE/Feature_Request.yml
vendored
Normal file
16
.github/ISSUE_TEMPLATE/Feature_Request.yml
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
name: Feature request
|
||||
description: Do you find Shlink is missing some important feature that would make it more useful?
|
||||
labels: ['feature']
|
||||
body:
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Summary
|
||||
value: '<!-- Describe the new feature you would like to request. -->'
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: Use case
|
||||
value: '<!-- Explain why do you think this feature would be useful, and what problems would it help to solve. -->'
|
26
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
26
.github/ISSUE_TEMPLATE/Question_Support.md
vendored
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
name: Question - Support
|
||||
about: Do you have a problem setting up or using shlink?
|
||||
labels: question
|
||||
---
|
||||
|
||||
<!--
|
||||
Before opening an issue, just take into account that this is a completely free of charge and open source project.
|
||||
I'm always happy to help and provide support, but some understanding will be expected.
|
||||
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
|
||||
You may also be asked to provide tests or ways to reproduce reported bugs.
|
||||
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
|
||||
|
||||
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
|
||||
-->
|
||||
|
||||
#### How Shlink is set up
|
||||
|
||||
* Shlink Version: x.y.z
|
||||
* PHP Version: x.y.z
|
||||
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Self-hosted RoadRunner|Openswoole Docker image|RoadRunner Docker image
|
||||
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
|
||||
|
||||
#### Summary
|
||||
|
||||
<!-- Describe the issue you are facing here. -->
|
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
5
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
blank_issues_enabled: true
|
||||
contact_links:
|
||||
- name: Question - Support
|
||||
about: Do you need help setting up or using Shlink?
|
||||
url: https://github.com/shlinkio/shlink/discussions/new?category=help-wanted
|
2
.github/actions/ci-setup/action.yml
vendored
2
.github/actions/ci-setup/action.yml
vendored
|
@ -43,5 +43,5 @@ runs:
|
|||
ini-values: pcov.directory=module
|
||||
- name: Install dependencies
|
||||
if: ${{ inputs.install-deps == 'yes' }}
|
||||
run: composer install --no-interaction --prefer-dist
|
||||
run: composer install --no-interaction --prefer-dist ${{ inputs.php-version == '8.3' && '--ignore-platform-reqs' || '' }}
|
||||
shell: bash
|
||||
|
|
9
.github/workflows/ci-db-tests.yml
vendored
9
.github/workflows/ci-db-tests.yml
vendored
|
@ -13,11 +13,12 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
env:
|
||||
LC_ALL: C
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install MSSQL ODBC
|
||||
if: ${{ inputs.platform == 'ms' }}
|
||||
run: sudo ./data/infra/ci/install-ms-odbc.sh
|
||||
|
@ -27,7 +28,7 @@ jobs:
|
|||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0, pdo_sqlsrv-5.10.1
|
||||
php-extensions: openswoole-22.1.0, pdo_sqlsrv-5.11.1
|
||||
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
|
||||
- name: Create test database
|
||||
if: ${{ inputs.platform == 'ms' }}
|
||||
|
@ -36,7 +37,7 @@ jobs:
|
|||
run: composer test:db:${{ inputs.platform }}
|
||||
- name: Upload code coverage
|
||||
uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.php-version == '8.1' && inputs.platform == 'sqlite:ci' }}
|
||||
if: ${{ matrix.php-version == '8.2' && inputs.platform == 'sqlite:ci' }}
|
||||
with:
|
||||
name: coverage-db
|
||||
path: |
|
||||
|
|
2
.github/workflows/ci-docker-image-build.yml
vendored
2
.github/workflows/ci-docker-image-build.yml
vendored
|
@ -10,5 +10,5 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- run: docker build -t shlink-docker-image:temp .
|
||||
|
|
7
.github/workflows/ci-mutation-tests.yml
vendored
7
.github/workflows/ci-mutation-tests.yml
vendored
|
@ -13,13 +13,14 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
|
|
9
.github/workflows/ci-tests.yml
vendored
9
.github/workflows/ci-tests.yml
vendored
|
@ -13,9 +13,10 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Start postgres database server
|
||||
if: ${{ inputs.test-group == 'api' }}
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
|
@ -25,11 +26,11 @@ jobs:
|
|||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
|
||||
- run: composer test:${{ inputs.test-group }}:ci
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: ${{ matrix.php-version == '8.1' }}
|
||||
if: ${{ matrix.php-version == '8.2' }}
|
||||
with:
|
||||
name: coverage-${{ inputs.test-group }}
|
||||
path: |
|
||||
|
|
17
.github/workflows/ci.yml
vendored
17
.github/workflows/ci.yml
vendored
|
@ -29,14 +29,14 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
php-version: ['8.2']
|
||||
command: ['cs', 'stan', 'swagger:validate']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
|
||||
- run: composer ${{ matrix.command }}
|
||||
|
||||
|
@ -59,17 +59,18 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
php-version: ['8.2', '8.3']
|
||||
continue-on-error: ${{ matrix.php-version == '8.3' }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
|
||||
- uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
tools: composer
|
||||
- run: composer install --no-interaction --prefer-dist --ignore-platform-req=ext-openswoole
|
||||
- run: composer install --no-interaction --prefer-dist --ignore-platform-req=ext-openswoole ${{ matrix.php-version == '8.3' && '--ignore-platform-reqs' || '' }}
|
||||
- run: ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr
|
||||
- run: composer test:api:rr
|
||||
|
||||
|
@ -135,10 +136,10 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
php-version: ['8.2']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
- name: Use PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
|
|
10
.github/workflows/publish-docker-image.yml
vendored
10
.github/workflows/publish-docker-image.yml
vendored
|
@ -2,16 +2,6 @@ name: Build and publish docker image
|
|||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'LICENSE'
|
||||
- '.*'
|
||||
- '*.md'
|
||||
- '*.xml'
|
||||
- '*.yml*'
|
||||
- '*.json5'
|
||||
- '*.neon'
|
||||
branches:
|
||||
- develop
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
|
|
8
.github/workflows/publish-release.yml
vendored
8
.github/workflows/publish-release.yml
vendored
|
@ -10,14 +10,14 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1', '8.2']
|
||||
php-version: ['8.2', '8.3']
|
||||
swoole: ['yes', 'no']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||
install-deps: 'no'
|
||||
- if: ${{ matrix.swoole == 'yes' }}
|
||||
|
@ -33,7 +33,7 @@ jobs:
|
|||
needs: ['build']
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
path: build
|
||||
|
|
8
.github/workflows/publish-swagger-spec.yml
vendored
8
.github/workflows/publish-swagger-spec.yml
vendored
|
@ -10,9 +10,9 @@ jobs:
|
|||
runs-on: ubuntu-22.04
|
||||
strategy:
|
||||
matrix:
|
||||
php-version: ['8.1']
|
||||
php-version: ['8.2']
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Determine version
|
||||
id: determine_version
|
||||
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
||||
|
@ -20,13 +20,13 @@ jobs:
|
|||
- uses: './.github/actions/ci-setup'
|
||||
with:
|
||||
php-version: ${{ matrix.php-version }}
|
||||
php-extensions: openswoole-22.0.0
|
||||
php-extensions: openswoole-22.1.0
|
||||
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
|
||||
- run: composer swagger:inline
|
||||
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
||||
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json
|
||||
- name: Publish spec
|
||||
uses: JamesIves/github-pages-deploy-action@4.4.1
|
||||
uses: JamesIves/github-pages-deploy-action@v4
|
||||
with:
|
||||
token: ${{ secrets.OAS_PUBLISH_TOKEN }}
|
||||
repository-name: 'shlinkio/shlink-open-api-specs'
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,6 +9,7 @@ vendor/
|
|||
data/database.sqlite
|
||||
data/shlink-tests.db
|
||||
data/GeoLite2-City.*
|
||||
data/infra/matomo
|
||||
docs/swagger-ui*
|
||||
docs/mercure.html
|
||||
docker-compose.override.yml
|
||||
|
|
54
CHANGELOG.md
54
CHANGELOG.md
|
@ -4,6 +4,58 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## [3.7.0] - 2023-11-25
|
||||
### Added
|
||||
* [#1798](https://github.com/shlinkio/shlink/issues/1798) Experimental support to send visits to an external Matomo instance.
|
||||
* [#1780](https://github.com/shlinkio/shlink/issues/1780) Add new `NO_ORPHAN_VISITS` API key role.
|
||||
|
||||
Keys with this role will always get `0` when fetching orphan visits.
|
||||
|
||||
When trying to delete orphan visits the result will also be `0` and no visits will actually get deleted.
|
||||
|
||||
* [#1879](https://github.com/shlinkio/shlink/issues/1879) Cache namespace can now be customized via config option or `CACHE_NAMESPACE` env var.
|
||||
|
||||
This is important if you are running multiple Shlink instance on the same server, or they share the same Redis instance (even more so if they are on different versions).
|
||||
|
||||
* [#1905](https://github.com/shlinkio/shlink/issues/1905) Add support for PHP 8.3.
|
||||
* [#1927](https://github.com/shlinkio/shlink/issues/1927) Allow redis credentials be URL-decoded before passing them to connection.
|
||||
* [#1834](https://github.com/shlinkio/shlink/issues/1834) Add support for redis encrypted connections using SSL/TLS.
|
||||
|
||||
Encryption should work out of the box if servers schema is set tp `tls` or `rediss`, including support for self-signed certificates.
|
||||
|
||||
This has been tested with AWS ElasticCache using in-transit encryption, and with Digital Ocean Redis database.
|
||||
|
||||
* [#1906](https://github.com/shlinkio/shlink/issues/1906) Add support for RabbitMQ encrypted connections using SSL/TLS.
|
||||
|
||||
In order to enable SLL, you need to pass `RABBITMQ_USE_SSL=true` or the corresponding config option.
|
||||
|
||||
Connections using self-signed certificates should work out of the box.
|
||||
|
||||
This has been tested with AWS RabbitMQ using in-transit encryption, and with CloudAMQP.
|
||||
|
||||
### Changed
|
||||
* [#1799](https://github.com/shlinkio/shlink/issues/1799) RoadRunner/openswoole jobs are not run anymore for tasks that are actually disabled.
|
||||
|
||||
For example, if you did not enable RabbitMQ real-time updates, instead of triggering a job that ends immediately, the job will not even be enqueued.
|
||||
|
||||
* [#1835](https://github.com/shlinkio/shlink/issues/1835) Docker image is now built only when a release is tagged, and new tags are included, for minor and major versions.
|
||||
* [#1055](https://github.com/shlinkio/shlink/issues/1055) Update OAS definition to v3.1.
|
||||
* [#1885](https://github.com/shlinkio/shlink/issues/1885) Update to chronos 3.0.
|
||||
* [#1896](https://github.com/shlinkio/shlink/issues/1896) Requests to health endpoint are no longer logged.
|
||||
* [#1877](https://github.com/shlinkio/shlink/issues/1877) Print a warning when manually running `visit:download-db` command and a GeoLite2 license was not provided.
|
||||
|
||||
### Deprecated
|
||||
* [#1783](https://github.com/shlinkio/shlink/issues/1783) Deprecated support for openswoole. RoadRunner is the best replacement, with the same capabilities, but much easier and convenient to install and manage.
|
||||
|
||||
### Removed
|
||||
* [#1790](https://github.com/shlinkio/shlink/issues/1790) Drop support for PHP 8.1.
|
||||
|
||||
### Fixed
|
||||
* [#1819](https://github.com/shlinkio/shlink/issues/1819) Fix incorrect timeout when running DB commands during Shlink start-up.
|
||||
* [#1901](https://github.com/shlinkio/shlink/issues/1901) Do not allow short URLs with custom slugs containing URL-reserved characters, as they will not work at all afterward.
|
||||
* [#1900](https://github.com/shlinkio/shlink/issues/1900) Fix short URL visits deletion when multi-segment slugs are enabled.
|
||||
|
||||
|
||||
## [3.6.4] - 2023-09-23
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
@ -112,7 +164,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this
|
|||
* Versions with `-openswoole` suffix (like `3.6.0-openswoole`) will always use openswoole as the runtime, even if default one changes in the future.
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
* Deprecated `ENABLE_PERIODIC_VISIT_LOCATE` env var. Use an external mechanism to automate visit locations.
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
|
|
@ -6,9 +6,9 @@ You will also see how to ensure the code fulfills the expected code checks, and
|
|||
|
||||
## System dependencies
|
||||
|
||||
The project provides all its dependencies as docker containers through a docker-compose configuration.
|
||||
The project provides all its dependencies as docker containers through a `docker compose` configuration.
|
||||
|
||||
Because of this, the only actual dependencies are [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
|
||||
Because of this, the only actual dependencies are [docker](https://docs.docker.com/get-docker/) and [docker compose](https://docs.docker.com/compose/install/).
|
||||
|
||||
## Setting up the project
|
||||
|
||||
|
@ -21,7 +21,7 @@ Then you will have to follow these steps:
|
|||
For example the `common.local.php.dist` file should be copied as `common.local.php`.
|
||||
|
||||
* Copy the file `docker-compose.override.yml.dist` by also removing the `dist` extension.
|
||||
* Start-up the project by running `docker-compose up`.
|
||||
* 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.
|
||||
|
||||
|
@ -31,7 +31,7 @@ Then you will have to follow these steps:
|
|||
* Run `./indocker bin/cli db:migrate` to get database migrations up to date.
|
||||
* Run `./indocker bin/cli api-key:generate` to get your first API key generated.
|
||||
|
||||
Once you finish this, you will have the project exposed in ports `8000` through nginx+php-fpm and `8080` through openswoole.
|
||||
Once you finish this, you will have the project exposed in ports `8800` through RoadRunner, `8080` through openswoole and `8000` through nginx+php-fpm.
|
||||
|
||||
> Note: The `indocker` shell script is a helper tool used to run commands inside the main docker container.
|
||||
|
||||
|
@ -73,12 +73,12 @@ shlink
|
|||
|
||||
The purposes of every folder are:
|
||||
|
||||
* `bin`: It contains the CLI tools. The `cli` one is the main entry point to run shlink from the command line.
|
||||
* `bin`: It contains the CLI tools. The `cli` one is the main entry point to run Shlink from the command line.
|
||||
* `config`: Contains application-wide configurations, which are later merged with the ones provided by every module.
|
||||
* `data`: Common runtime-generated git-ignored assets, like logs, caches, etc.
|
||||
* `docs`: Any project documentation is stored here, like API spec definitions or architectural decision records.
|
||||
* `module`: Contains a sub-folder for every module in the project. Modules contain the source code, tests and configurations for every context in the project.
|
||||
* `public`: Few assets (like `favicon.ico` or `robots.txt`) and the web entry point are stored here. This web entry point is not used when serving the app with openswoole.
|
||||
* `public`: Few assets (like `favicon.ico` or `robots.txt`) and the web entry point are stored here. This web entry point is not used when serving the app with RoadRunner or openswoole.
|
||||
|
||||
## Project tests
|
||||
|
||||
|
@ -94,7 +94,7 @@ In order to ensure stability and no regressions are introduced while developing
|
|||
|
||||
The project provides some tooling to run them against any of the supported database engines.
|
||||
|
||||
* **API tests**: These are E2E tests that spin up an instance of the app with openswoole, and test it from the outside by interacting with the REST API.
|
||||
* **API tests**: These are E2E tests that spin up an instance of the app with RoadRunner or openswoole, and test it from the outside by interacting with the REST API.
|
||||
|
||||
These are the best tests to catch regressions, and to verify everything behaves as expected.
|
||||
|
||||
|
@ -125,6 +125,12 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed,
|
|||
* Run `./indocker composer infect:test` to run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/).
|
||||
* Run `./indocker composer ci` to run all previous commands together, parallelizing non-conflicting tasks as much as possible.
|
||||
|
||||
## Testing endpoints
|
||||
|
||||
The project provides a Swagger UI container for dev envs, which can be accessed in http://localhost:8005.
|
||||
|
||||
It will automatically load the contents of `docs/swagger`, so you can make any updates and they will get reflected.
|
||||
|
||||
## Pull request process
|
||||
|
||||
**Important!**: Before starting to work on a pull request, make sure you always [open an issue](https://github.com/shlinkio/shlink/issues/new/choose) first.
|
||||
|
|
|
@ -7,8 +7,8 @@ ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
|
|||
ARG SHLINK_USER_ID='root'
|
||||
ENV SHLINK_USER_ID ${SHLINK_USER_ID}
|
||||
|
||||
ENV OPENSWOOLE_VERSION 22.0.0
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
ENV OPENSWOOLE_VERSION 22.1.0
|
||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
ENV LC_ALL 'C'
|
||||
|
@ -29,6 +29,7 @@ RUN \
|
|||
# Install openswoole and sqlsrv driver for x86_64 builds
|
||||
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||
# Openswoole is deprecated. Remove in v4.0.0
|
||||
pecl install openswoole-${OPENSWOOLE_VERSION} && \
|
||||
docker-php-ext-enable openswoole ; \
|
||||
fi; \
|
||||
|
@ -49,6 +50,7 @@ RUN apk add --no-cache git && \
|
|||
# FIXME Ignoring ext-openswoole platform req, as it makes install fail with roadrunner, even though it's a dev dependency and we are passing --no-dev
|
||||
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole && \
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
|
||||
# Openswoole is deprecated. Remove in v4.0.0
|
||||
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
|
||||
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then \
|
||||
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction --ignore-platform-req=ext-openswoole ; \
|
||||
|
|
|
@ -32,11 +32,11 @@ You can learn how to use the official docker image by reading [the docs](https:/
|
|||
|
||||
The idea is that you can just generate a container using the image and provide the custom config via env vars.
|
||||
|
||||
## Self hosted
|
||||
## Self-hosted
|
||||
|
||||
First, make sure the host where you are going to run shlink fulfills these requirements:
|
||||
|
||||
* PHP 8.1 or 8.2
|
||||
* PHP 8.2 or 8.3
|
||||
* The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath.
|
||||
* apcu extension is recommended if you don't plan to use openswoole.
|
||||
* xml extension is required if you want to generate QR codes in svg format.
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
export APP_ENV=test
|
||||
export TEST_ENV=api
|
||||
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}"
|
||||
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}" # Openswoole is deprecated. Remove in v4.0.0
|
||||
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
|
||||
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
|
||||
|
||||
|
|
7
build.sh
7
build.sh
|
@ -10,6 +10,7 @@ fi
|
|||
version=$1
|
||||
noSwoole=$2
|
||||
phpVersion=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
|
||||
# Openswoole is deprecated. Remove in v4.0.0
|
||||
[[ $noSwoole ]] && swooleSuffix="" || swooleSuffix="_openswoole"
|
||||
distId="shlink${version}_php${phpVersion}${swooleSuffix}_dist"
|
||||
builtContent="./build/${distId}"
|
||||
|
@ -30,7 +31,8 @@ cd "${builtContent}"
|
|||
|
||||
# Install dependencies
|
||||
echo "Installing dependencies with $composerBin..."
|
||||
composerFlags="--optimize-autoloader --no-progress --no-interaction"
|
||||
# Deprecated. Do not ignore PHP platform req for Shlink v4.0.0
|
||||
composerFlags="--optimize-autoloader --no-progress --no-interaction --ignore-platform-req=php+"
|
||||
${composerBin} self-update
|
||||
${composerBin} install --no-dev --prefer-dist $composerFlags
|
||||
|
||||
|
@ -38,6 +40,7 @@ if [[ $noSwoole ]]; then
|
|||
# If generating a dist not for openswoole, uninstall mezzio-swoole
|
||||
${composerBin} remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev $composerFlags
|
||||
else
|
||||
# Deprecated. Remove in Shlink v4.0.0
|
||||
# If generating a dist for openswoole, uninstall RoadRunner
|
||||
${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev $composerFlags
|
||||
fi
|
||||
|
@ -46,7 +49,7 @@ fi
|
|||
echo 'Deleting dev files...'
|
||||
rm composer.*
|
||||
|
||||
# Update shlink version in config
|
||||
# Update Shlink version in config
|
||||
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
|
||||
|
||||
# Compressing file
|
||||
|
|
|
@ -12,72 +12,73 @@
|
|||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php": "^8.2",
|
||||
"ext-curl": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-json": "*",
|
||||
"ext-pdo": "*",
|
||||
"akrabat/ip-address-middleware": "^2.1",
|
||||
"cakephp/chronos": "~2.3.3",
|
||||
"doctrine/migrations": "^3.5",
|
||||
"doctrine/orm": "^2.14",
|
||||
"endroid/qr-code": "^4.7",
|
||||
"cakephp/chronos": "^3.0.2",
|
||||
"doctrine/migrations": "^3.6",
|
||||
"doctrine/orm": "^2.16",
|
||||
"endroid/qr-code": "^4.8",
|
||||
"friendsofphp/proxy-manager-lts": "^1.0",
|
||||
"geoip2/geoip2": "^2.13",
|
||||
"guzzlehttp/guzzle": "^7.5",
|
||||
"happyr/doctrine-specification": "^2.0",
|
||||
"jaybizzle/crawler-detect": "^1.2.112",
|
||||
"jaybizzle/crawler-detect": "^1.2.116",
|
||||
"laminas/laminas-config": "^3.8",
|
||||
"laminas/laminas-config-aggregator": "^1.13",
|
||||
"laminas/laminas-diactoros": "^2.24",
|
||||
"laminas/laminas-inputfilter": "^2.24",
|
||||
"laminas/laminas-servicemanager": "^3.20",
|
||||
"laminas/laminas-stdlib": "^3.16",
|
||||
"laminas/laminas-diactoros": "^2.25",
|
||||
"laminas/laminas-inputfilter": "^2.27",
|
||||
"laminas/laminas-servicemanager": "^3.21",
|
||||
"laminas/laminas-stdlib": "^3.17",
|
||||
"league/uri": "^6.8",
|
||||
"lstrojny/functional-php": "^1.17",
|
||||
"mezzio/mezzio": "^3.15",
|
||||
"mezzio/mezzio-fastroute": "^3.8",
|
||||
"mezzio/mezzio-problem-details": "^1.11",
|
||||
"mezzio/mezzio-swoole": "^4.6",
|
||||
"matomo/matomo-php-tracker": "^3.2",
|
||||
"mezzio/mezzio": "^3.17",
|
||||
"mezzio/mezzio-fastroute": "^3.10",
|
||||
"mezzio/mezzio-problem-details": "^1.13",
|
||||
"mezzio/mezzio-swoole": "^4.7",
|
||||
"mlocati/ip-lib": "^1.18",
|
||||
"mobiledetect/mobiledetectlib": "^3.74",
|
||||
"ocramius/proxy-manager": "^2.14",
|
||||
"pagerfanta/core": "^3.7",
|
||||
"mobiledetect/mobiledetectlib": "^4.8",
|
||||
"pagerfanta/core": "^3.8",
|
||||
"php-middleware/request-id": "^4.1",
|
||||
"pugx/shortid-php": "^1.1",
|
||||
"ramsey/uuid": "^4.7",
|
||||
"shlinkio/shlink-common": "^5.6",
|
||||
"shlinkio/shlink-config": "^2.4",
|
||||
"shlinkio/shlink-event-dispatcher": "^3.0",
|
||||
"shlinkio/shlink-importer": "^5.1",
|
||||
"shlinkio/shlink-installer": "^8.5",
|
||||
"shlinkio/shlink-ip-geolocation": "^3.2",
|
||||
"shlinkio/shlink-json": "^1.0",
|
||||
"spiral/roadrunner": "^2023.1",
|
||||
"shlinkio/shlink-common": "^5.7",
|
||||
"shlinkio/shlink-config": "^2.5",
|
||||
"shlinkio/shlink-event-dispatcher": "^3.1",
|
||||
"shlinkio/shlink-importer": "^5.2",
|
||||
"shlinkio/shlink-installer": "^8.6",
|
||||
"shlinkio/shlink-ip-geolocation": "^3.3",
|
||||
"shlinkio/shlink-json": "^1.1",
|
||||
"spiral/roadrunner": "^2023.2",
|
||||
"spiral/roadrunner-cli": "^2.5",
|
||||
"spiral/roadrunner-http": "^3.0",
|
||||
"spiral/roadrunner-http": "^3.1",
|
||||
"spiral/roadrunner-jobs": "^4.0",
|
||||
"symfony/console": "^6.2",
|
||||
"symfony/filesystem": "^6.2",
|
||||
"symfony/lock": "^6.2",
|
||||
"symfony/process": "^6.2",
|
||||
"symfony/string": "^6.2"
|
||||
"symfony/console": "^6.3",
|
||||
"symfony/filesystem": "^6.3",
|
||||
"symfony/lock": "^6.3",
|
||||
"symfony/process": "^6.3",
|
||||
"symfony/string": "^6.3"
|
||||
},
|
||||
"require-dev": {
|
||||
"cebe/php-openapi": "^1.7",
|
||||
"devizzent/cebe-php-openapi": "^1.0.1",
|
||||
"devster/ubench": "^2.1",
|
||||
"infection/infection": "^0.27",
|
||||
"openswoole/ide-helper": "~22.0.0",
|
||||
"phpstan/phpstan": "^1.9",
|
||||
"phpstan/phpstan": "^1.10",
|
||||
"phpstan/phpstan-doctrine": "^1.3",
|
||||
"phpstan/phpstan-phpunit": "^1.3",
|
||||
"phpstan/phpstan-symfony": "^1.2",
|
||||
"phpunit/php-code-coverage": "^10.0",
|
||||
"phpunit/phpunit": "~10.1.0",
|
||||
"phpstan/phpstan-symfony": "^1.3",
|
||||
"phpunit/php-code-coverage": "^10.1",
|
||||
"phpunit/phpunit": "^10.4",
|
||||
"roave/security-advisories": "dev-master",
|
||||
"shlinkio/php-coding-standard": "~2.3.0",
|
||||
"shlinkio/shlink-test-utils": "~3.6.0",
|
||||
"symfony/var-dumper": "^6.2",
|
||||
"veewee/composer-run-parallel": "^1.2"
|
||||
"shlinkio/shlink-test-utils": "^3.8",
|
||||
"symfony/var-dumper": "^6.3",
|
||||
"veewee/composer-run-parallel": "^1.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
@ -137,7 +138,7 @@
|
|||
"infect:ci:base": "infection --threads=max --only-covered --skip-initial-tests",
|
||||
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=80",
|
||||
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json5",
|
||||
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json5",
|
||||
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=95 --configuration=infection-api.json5",
|
||||
"infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=90 --configuration=infection-cli.json5",
|
||||
"infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api infect:ci:cli",
|
||||
"infect:test": [
|
||||
|
@ -148,6 +149,10 @@
|
|||
"@test:unit:ci",
|
||||
"@infect:ci:unit"
|
||||
],
|
||||
"infect:test:db": [
|
||||
"@test:db:sqlite:ci",
|
||||
"@infect:ci:db"
|
||||
],
|
||||
"infect:test:api": [
|
||||
"@test:api:ci",
|
||||
"@infect:ci:api"
|
||||
|
|
|
@ -11,12 +11,13 @@ return (static function (): array {
|
|||
'redis' => [
|
||||
'servers' => $redisServers,
|
||||
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
|
||||
'decode_credentials' => (bool) EnvVars::REDIS_DECODE_CREDENTIALS->loadFromEnv(false),
|
||||
],
|
||||
];
|
||||
|
||||
return [
|
||||
'cache' => [
|
||||
'namespace' => 'Shlink',
|
||||
'namespace' => EnvVars::CACHE_NAMESPACE->loadFromEnv('Shlink'),
|
||||
...$cacheRedisBlock,
|
||||
],
|
||||
'redis' => $redis,
|
||||
|
|
|
@ -11,6 +11,7 @@ return [
|
|||
|
||||
'installer' => [
|
||||
'enabled_options' => [
|
||||
Option\Server\RuntimeConfigOption::class,
|
||||
Option\Database\DatabaseDriverConfigOption::class,
|
||||
Option\Database\DatabaseNameConfigOption::class,
|
||||
Option\Database\DatabaseHostConfigOption::class,
|
||||
|
@ -28,9 +29,11 @@ return [
|
|||
Option\Visit\VisitsThresholdConfigOption::class,
|
||||
Option\BasePathConfigOption::class,
|
||||
Option\TimezoneConfigOption::class,
|
||||
Option\Cache\CacheNamespaceConfigOption::class,
|
||||
Option\Worker\TaskWorkerNumConfigOption::class,
|
||||
Option\Worker\WebWorkerNumConfigOption::class,
|
||||
Option\Redis\RedisServersConfigOption::class,
|
||||
Option\Redis\RedisDecodeCredentialsConfigOption::class,
|
||||
Option\Redis\RedisSentinelServiceConfigOption::class,
|
||||
Option\Redis\RedisPubSubConfigOption::class,
|
||||
Option\UrlShortener\ShortCodeLengthOption::class,
|
||||
|
@ -61,10 +64,15 @@ return [
|
|||
Option\QrCode\DefaultRoundBlockSizeConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqEnabledConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqHostConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqUseSslConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqPortConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqUserConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqPasswordConfigOption::class,
|
||||
Option\RabbitMq\RabbitMqVhostConfigOption::class,
|
||||
Option\Matomo\MatomoEnabledConfigOption::class,
|
||||
Option\Matomo\MatomoBaseUrlConfigOption::class,
|
||||
Option\Matomo\MatomoSiteIdConfigOption::class,
|
||||
Option\Matomo\MatomoApiTokenConfigOption::class,
|
||||
],
|
||||
|
||||
'installation_commands' => [
|
||||
|
|
|
@ -52,6 +52,7 @@ return (static function (): array {
|
|||
],
|
||||
],
|
||||
|
||||
// Deprecated. Remove in Shlink 4.0.0
|
||||
'mezzio-swoole' => [
|
||||
'swoole-http-server' => [
|
||||
'logger' => [
|
||||
|
|
16
config/autoload/matomo.global.php
Normal file
16
config/autoload/matomo.global.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\EnvVars;
|
||||
|
||||
return [
|
||||
|
||||
'matomo' => [
|
||||
'enabled' => (bool) EnvVars::MATOMO_ENABLED->loadFromEnv(false),
|
||||
'base_url' => EnvVars::MATOMO_BASE_URL->loadFromEnv(),
|
||||
'site_id' => EnvVars::MATOMO_SITE_ID->loadFromEnv(),
|
||||
'api_token' => EnvVars::MATOMO_API_TOKEN->loadFromEnv(),
|
||||
],
|
||||
|
||||
];
|
26
config/autoload/matomo.local.php.dist
Normal file
26
config/autoload/matomo.local.php.dist
Normal file
|
@ -0,0 +1,26 @@
|
|||
<?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' => '...',
|
||||
],
|
||||
|
||||
];
|
|
@ -9,6 +9,7 @@ return [
|
|||
'rabbitmq' => [
|
||||
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(false),
|
||||
'host' => EnvVars::RABBITMQ_HOST->loadFromEnv(),
|
||||
'use_ssl' => (bool) EnvVars::RABBITMQ_USE_SSL->loadFromEnv(false),
|
||||
'port' => (int) EnvVars::RABBITMQ_PORT->loadFromEnv('5672'),
|
||||
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
|
||||
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
|
||||
|
|
|
@ -32,8 +32,11 @@ return (static function (): array {
|
|||
...ConfigProvider::applyRoutesPrefix([
|
||||
Action\HealthAction::getRouteDef(),
|
||||
|
||||
// Visits
|
||||
// Visits.
|
||||
// These routes must go first, as they have a more specific path, otherwise, when multi-segment slugs
|
||||
// are enabled, routes with a less-specific path might match first
|
||||
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\Visit\TagVisitsAction::getRouteDef(),
|
||||
Action\Visit\DomainVisitsAction::getRouteDef(),
|
||||
Action\Visit\GlobalVisitsAction::getRouteDef(),
|
||||
|
@ -54,7 +57,6 @@ return (static function (): array {
|
|||
]),
|
||||
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\DeleteShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ResolveShortUrlAction::getRouteDef([$dropDomainMiddleware]),
|
||||
Action\ShortUrl\ListShortUrlsAction::getRouteDef(),
|
||||
|
||||
|
|
|
@ -2,11 +2,26 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Tools\Console\ConsoleRunner;
|
||||
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager;
|
||||
use Doctrine\Migrations\Configuration\Migration\ConfigurationArray;
|
||||
use Doctrine\Migrations\DependencyFactory;
|
||||
|
||||
// This file is currently used by doctrine migrations only
|
||||
|
||||
return (static function () {
|
||||
/** @var EntityManager $em */
|
||||
$migrationsConfig = [
|
||||
'migrations_paths' => [
|
||||
'ShlinkMigrations' => 'data/migrations',
|
||||
],
|
||||
'table_storage' => [
|
||||
'table_name' => 'migrations',
|
||||
],
|
||||
'custom_template' => 'data/migrations_template.txt',
|
||||
];
|
||||
$em = include __DIR__ . '/entity-manager.php';
|
||||
return ConsoleRunner::createHelperSet($em);
|
||||
|
||||
return DependencyFactory::fromEntityManager(
|
||||
new ConfigurationArray($migrationsConfig),
|
||||
new ExistingEntityManager($em),
|
||||
);
|
||||
})();
|
||||
|
|
|
@ -22,33 +22,39 @@ use const PHP_SAPI;
|
|||
$isTestEnv = env('APP_ENV') === 'test';
|
||||
$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner();
|
||||
|
||||
return (new ConfigAggregator\ConfigAggregator([
|
||||
! $isTestEnv
|
||||
? new EnvVarLoaderProvider('config/params/generated_config.php', enumValues(Core\Config\EnvVars::class))
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
Mezzio\ConfigProvider::class,
|
||||
Mezzio\Router\ConfigProvider::class,
|
||||
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
|
||||
$enableSwoole && class_exists(Swoole\ConfigProvider::class)
|
||||
? Swoole\ConfigProvider::class
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
ProblemDetails\ConfigProvider::class,
|
||||
Diactoros\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
Config\ConfigProvider::class,
|
||||
Importer\ConfigProvider::class,
|
||||
IpGeolocation\ConfigProvider::class,
|
||||
EventDispatcher\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
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'),
|
||||
// Routes have to be loaded last
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
|
||||
], 'data/cache/app_config.php', [
|
||||
Core\Config\PostProcessor\BasePathPrefixer::class,
|
||||
Core\Config\PostProcessor\MultiSegmentSlugProcessor::class,
|
||||
Core\Config\PostProcessor\ShortUrlMethodsProcessor::class,
|
||||
]))->getMergedConfig();
|
||||
return (new ConfigAggregator\ConfigAggregator(
|
||||
providers: [
|
||||
! $isTestEnv
|
||||
? new EnvVarLoaderProvider('config/params/generated_config.php', enumValues(Core\Config\EnvVars::class))
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
Mezzio\ConfigProvider::class,
|
||||
Mezzio\Router\ConfigProvider::class,
|
||||
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
|
||||
$enableSwoole && class_exists(Swoole\ConfigProvider::class)
|
||||
? Swoole\ConfigProvider::class
|
||||
: new ConfigAggregator\ArrayProvider([]),
|
||||
ProblemDetails\ConfigProvider::class,
|
||||
Diactoros\ConfigProvider::class,
|
||||
Common\ConfigProvider::class,
|
||||
Config\ConfigProvider::class,
|
||||
Importer\ConfigProvider::class,
|
||||
IpGeolocation\ConfigProvider::class,
|
||||
EventDispatcher\ConfigProvider::class,
|
||||
Core\ConfigProvider::class,
|
||||
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',
|
||||
),
|
||||
// Routes have to be loaded last
|
||||
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
|
||||
],
|
||||
cachedConfigFile: 'data/cache/app_config.php',
|
||||
postProcessors: [
|
||||
Core\Config\PostProcessor\BasePathPrefixer::class,
|
||||
Core\Config\PostProcessor\MultiSegmentSlugProcessor::class,
|
||||
Core\Config\PostProcessor\ShortUrlMethodsProcessor::class,
|
||||
],
|
||||
))->getMergedConfig();
|
||||
|
|
|
@ -13,6 +13,7 @@ chdir(dirname(__DIR__));
|
|||
require 'vendor/autoload.php';
|
||||
|
||||
// Workaround to make this compatible with both openswoole 22 and earlier versions.
|
||||
// Openswoole support is deprecated. Remove in v4.0.0
|
||||
if (! function_exists('swoole_set_process_name')) {
|
||||
// phpcs:disable
|
||||
function swoole_set_process_name(string $name): void
|
||||
|
|
|
@ -11,7 +11,7 @@ server {
|
|||
|
||||
location ~ \.php$ {
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi.conf;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ FROM php:8.2-fpm-alpine3.17
|
|||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ FROM php:8.2-alpine3.17
|
|||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
|||
|
||||
ENV APCU_VERSION 5.1.21
|
||||
ENV INOTIFY_VERSION 3.0.0
|
||||
ENV OPENSWOOLE_VERSION 22.0.0
|
||||
ENV PDO_SQLSRV_VERSION 5.10.1
|
||||
ENV OPENSWOOLE_VERSION 22.1.0
|
||||
ENV PDO_SQLSRV_VERSION 5.11.1
|
||||
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
|
||||
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ version: '3'
|
|||
services:
|
||||
shlink_nginx:
|
||||
container_name: shlink_nginx
|
||||
image: nginx:1.19.6-alpine
|
||||
image: nginx:1.25-alpine
|
||||
ports:
|
||||
- "8000:80"
|
||||
volumes:
|
||||
|
@ -33,6 +33,7 @@ services:
|
|||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
- shlink_matomo
|
||||
environment:
|
||||
LC_ALL: C
|
||||
extra_hosts:
|
||||
|
@ -40,7 +41,7 @@ services:
|
|||
|
||||
shlink_swoole_proxy:
|
||||
container_name: shlink_swoole_proxy
|
||||
image: nginx:1.19.6-alpine
|
||||
image: nginx:1.25-alpine
|
||||
ports:
|
||||
- "8002:80"
|
||||
volumes:
|
||||
|
@ -70,6 +71,7 @@ services:
|
|||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
- shlink_matomo
|
||||
environment:
|
||||
LC_ALL: C
|
||||
extra_hosts:
|
||||
|
@ -95,6 +97,7 @@ services:
|
|||
- shlink_mercure
|
||||
- shlink_mercure_proxy
|
||||
- shlink_rabbitmq
|
||||
- shlink_matomo
|
||||
environment:
|
||||
LC_ALL: C
|
||||
extra_hosts:
|
||||
|
@ -164,7 +167,7 @@ services:
|
|||
|
||||
shlink_mercure_proxy:
|
||||
container_name: shlink_mercure_proxy
|
||||
image: nginx:1.19.6-alpine
|
||||
image: nginx:1.25-alpine
|
||||
ports:
|
||||
- "8001:80"
|
||||
volumes:
|
||||
|
@ -175,7 +178,7 @@ services:
|
|||
|
||||
shlink_mercure:
|
||||
container_name: shlink_mercure
|
||||
image: dunglas/mercure:v0.14
|
||||
image: dunglas/mercure:v0.15
|
||||
ports:
|
||||
- "3080:80"
|
||||
environment:
|
||||
|
@ -186,10 +189,36 @@ services:
|
|||
|
||||
shlink_rabbitmq:
|
||||
container_name: shlink_rabbitmq
|
||||
image: rabbitmq:3.9-management-alpine
|
||||
image: rabbitmq:3.11-management-alpine
|
||||
ports:
|
||||
- "15672:15672"
|
||||
- "5672:5672"
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: "rabbit"
|
||||
RABBITMQ_DEFAULT_PASS: "rabbit"
|
||||
|
||||
shlink_swagger_ui:
|
||||
container_name: shlink_swagger_ui
|
||||
image: swaggerapi/swagger-ui:v5.9.1
|
||||
ports:
|
||||
- "8005:8080"
|
||||
volumes:
|
||||
- ./docs/swagger:/app
|
||||
|
||||
shlink_matomo:
|
||||
container_name: shlink_matomo
|
||||
image: matomo:4.15-apache
|
||||
ports:
|
||||
- "8003:80"
|
||||
volumes:
|
||||
# Matomo does not persist port in trusted hosts. This volume is needed to edit config afterward
|
||||
# https://github.com/matomo-org/matomo/issues/9549
|
||||
- ./data/infra/matomo:/var/www/html
|
||||
links:
|
||||
- shlink_db_mysql
|
||||
environment:
|
||||
MATOMO_DATABASE_HOST: "shlink_db_mysql"
|
||||
MATOMO_DATABASE_ADAPTER: "mysql"
|
||||
MATOMO_DATABASE_DBNAME: "matomo"
|
||||
MATOMO_DATABASE_USERNAME: "root"
|
||||
MATOMO_DATABASE_PASSWORD: "root"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
This image provides an easy way to set up [shlink](https://shlink.io) on a container-based runtime.
|
||||
|
||||
It exposes a shlink instance served with [openswoole](https://openswoole.com/), which can be linked to external databases to persist data.
|
||||
It exposes a shlink instance served with [RoadRunner](https://roadrunner.dev) or [openswoole](https://openswoole.com/), which can be linked to external databases to persist data.
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
|
@ -3,10 +3,10 @@ set -e
|
|||
|
||||
cd /etc/shlink
|
||||
|
||||
flags="--clear-db-cache"
|
||||
flags="--no-interaction --clear-db-cache"
|
||||
|
||||
# Skip downloading GeoLite2 db file if the license key env var was not defined or skipping was explicitly set
|
||||
if [ -z "${GEOLITE_LICENSE_KEY}" ] || [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" == "true" ]; then
|
||||
if [ -z "${GEOLITE_LICENSE_KEY}" ] || [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" = "true" ]; then
|
||||
flags="${flags} --skip-download-geolite"
|
||||
fi
|
||||
|
||||
|
@ -25,10 +25,11 @@ if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ] && [ "${SHLINK_USER_ID}" = "ro
|
|||
/usr/sbin/crond &
|
||||
fi
|
||||
|
||||
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then
|
||||
if [ "$SHLINK_RUNTIME" = 'openswoole' ]; then
|
||||
# Openswoole is deprecated. Remove in Shlink 4.0.0
|
||||
# When restarting the container, openswoole might think it is already in execution
|
||||
# This forces the app to be started every second until the exit code is 0
|
||||
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
|
||||
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then
|
||||
elif [ "$SHLINK_RUNTIME" = 'rr' ]; then
|
||||
./bin/rr serve -c config/roadrunner/.rr.yml
|
||||
fi
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
# Build `latest` docker image only for actual releases
|
||||
|
||||
* Status: Accepted
|
||||
* Date: 2023-07-09
|
||||
|
||||
## Context and problem statement
|
||||
|
||||
Historically, this project has re-tagged the `latest´ docker image every time a PR was merged into default branch.
|
||||
|
||||
The reason was to be able to:
|
||||
|
||||
* Periodically test the docker building and publishing process.
|
||||
* Provide "partial" images for quick testing of new "un-released" features.
|
||||
|
||||
However, this was considered non-stable, and not recommended to use in production. Instead, a convenient `stable` tag was provided, which was re-tagged for every new non-beta/non-alpha release.
|
||||
|
||||
The approach described above for `latest` has some problems, though:
|
||||
|
||||
* Many people ignore the recommendation of not using it in production. There have even been reports of bugs on things which were, technically speaking, not yet released.
|
||||
* Since it is not always built for an actual new project version, the project itself cannot inform about anything other than `latest`, which can quickly become a lie if you don't update your local version.
|
||||
|
||||
## Considered options
|
||||
|
||||
* Try to provide a pseudo-version when `latest` is built. Something like `<prev_version>-<commit_hash>.
|
||||
* Change how `latest` is published, and start tagging it only for actual new version releases.
|
||||
* Same as the above, but exclude alpha/beta versions, deprecating `stable` tag.
|
||||
|
||||
## Decision outcome
|
||||
|
||||
Since testing un-released features has never been needed, it is probably a not-very useful thing to have.
|
||||
|
||||
Periodically testing the build and publish process can also be moved somewhere else, like a testing "hidden" account.
|
||||
|
||||
Also, having `stable` with non-alpha/non-beta releases seems sensible, so the decision is to "Change how `latest` is published, and start tagging it only for actual new version releases".
|
||||
|
||||
## Pros and Cons of the Options
|
||||
|
||||
### Try to provide a pseudo-version when `latest` is built.
|
||||
|
||||
* Good: because we keep publishing process intact, from a user point of view.
|
||||
* Bad: because it requires adding some non-trivial logic to the image building, which needs to find out what was the latest stable release.
|
||||
|
||||
### Make `latest` hold latest published version, including unstable releases.
|
||||
|
||||
* Good: because it provides a way for users to test bleeding-edge features, with less risk than relying on the very last content from default branch.
|
||||
* Good: because it allows for `stable` to be used together with `latest`.
|
||||
* Bad: because partial features cannot be tested without publishing an alpha or beta version.
|
||||
|
||||
### Make `latest` hold latest published version, excluding unstable releases.
|
||||
|
||||
* Bad: because there's no longer a way to test bleeding-edge features, other than installing that specific version.
|
||||
* Bad: because it drives `stable` useless, which means it needs to be deprecated, documented, and eventually removed.
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
Here listed you will find the different architectural decisions taken in the project, including all the reasoning behind it, options considered, and final outcome.
|
||||
|
||||
* [2023-07-09Build `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)
|
||||
|
|
|
@ -3,18 +3,15 @@
|
|||
"properties": {
|
||||
"android": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
"type": ["string"]
|
||||
},
|
||||
"ios": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
"type": ["string"]
|
||||
},
|
||||
"desktop": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
|
||||
"type": "string",
|
||||
"nullable": false
|
||||
"type": ["string"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
}],
|
||||
"properties": {
|
||||
"android": {
|
||||
"nullable": true
|
||||
"type": ["null"]
|
||||
},
|
||||
"ios": {
|
||||
"nullable": true
|
||||
"type": ["null"]
|
||||
},
|
||||
"desktop": {
|
||||
"nullable": true
|
||||
"type": ["null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,18 +2,15 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"baseUrlRedirect": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "URL to redirect to when a user hits the domain's base URL"
|
||||
},
|
||||
"regular404Redirect": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "URL to redirect to when a user hits a not found URL other than an invalid short URL"
|
||||
},
|
||||
"invalidShortUrlRedirect": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "URL to redirect to when a user hits an invalid short URL"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
}],
|
||||
"properties": {
|
||||
"visitedUrl": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "The originally visited URL that triggered the tracking of this visit"
|
||||
},
|
||||
"type": {
|
||||
|
|
|
@ -55,13 +55,11 @@
|
|||
"$ref": "./ShortUrlMeta.json"
|
||||
},
|
||||
"domain": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "The domain in which the short URL was created. Null if it belongs to default domain."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"type": ["string", "null"],
|
||||
"description": "A descriptive title of the short URL."
|
||||
},
|
||||
"crawlable": {
|
||||
|
|
|
@ -10,18 +10,15 @@
|
|||
},
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
"type": ["number", "null"]
|
||||
},
|
||||
"validateUrl": {
|
||||
"deprecated": true,
|
||||
|
@ -36,9 +33,8 @@
|
|||
"description": "The list of tags to set to the short URL."
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "A descriptive title of the short URL.",
|
||||
"nullable": true
|
||||
"type": ["string", "null"],
|
||||
"description": "A descriptive title of the short URL."
|
||||
},
|
||||
"crawlable": {
|
||||
"type": "boolean",
|
||||
|
|
|
@ -4,18 +4,15 @@
|
|||
"properties": {
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"validUntil": {
|
||||
"description": "The date (in ISO-8601 format) until which this short code will be valid",
|
||||
"type": "string",
|
||||
"nullable": true
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"maxVisits": {
|
||||
"description": "The maximum number of allowed visits for this short code",
|
||||
"type": "number",
|
||||
"nullable": true
|
||||
"type": ["number", "null"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"openapi": "3.0.3",
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "Shlink",
|
||||
"description": "Shlink, the self-hosted URL shortener",
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
return [
|
||||
|
||||
'migrations_paths' => [
|
||||
'ShlinkMigrations' => 'data/migrations',
|
||||
],
|
||||
'table_storage' => [
|
||||
'table_name' => 'migrations',
|
||||
],
|
||||
'custom_template' => 'data/migrations_template.txt',
|
||||
|
||||
];
|
|
@ -24,6 +24,7 @@ class RoleResolver implements RoleResolverInterface
|
|||
{
|
||||
$domainAuthority = $input->getOption(Role::DOMAIN_SPECIFIC->paramName());
|
||||
$author = $input->getOption(Role::AUTHORED_SHORT_URLS->paramName());
|
||||
$noOrphanVisits = $input->getOption(Role::NO_ORPHAN_VISITS->paramName());
|
||||
|
||||
if ($author) {
|
||||
yield RoleDefinition::forAuthoredShortUrls();
|
||||
|
@ -31,6 +32,9 @@ class RoleResolver implements RoleResolverInterface
|
|||
if (is_string($domainAuthority)) {
|
||||
yield $this->resolveRoleForAuthority($domainAuthority);
|
||||
}
|
||||
if ($noOrphanVisits) {
|
||||
yield RoleDefinition::forNoOrphanVisits();
|
||||
}
|
||||
}
|
||||
|
||||
private function resolveRoleForAuthority(string $domainAuthority): RoleDefinition
|
||||
|
|
|
@ -36,6 +36,8 @@ class GenerateKeyCommand extends Command
|
|||
{
|
||||
$authorOnly = Role::AUTHORED_SHORT_URLS->paramName();
|
||||
$domainOnly = Role::DOMAIN_SPECIFIC->paramName();
|
||||
$noOrphanVisits = Role::NO_ORPHAN_VISITS->paramName();
|
||||
|
||||
$help = <<<HELP
|
||||
The <info>%command.name%</info> generates a new valid API key.
|
||||
|
||||
|
@ -53,7 +55,8 @@ class GenerateKeyCommand extends Command
|
|||
|
||||
* Can interact with short URLs created with this API key: <info>%command.full_name% --{$authorOnly}</info>
|
||||
* Can interact with short URLs for one domain: <info>%command.full_name% --{$domainOnly}=example.com</info>
|
||||
* Both: <info>%command.full_name% --{$authorOnly} --{$domainOnly}=example.com</info>
|
||||
* Cannot see orphan visits: <info>%command.full_name% --{$noOrphanVisits}</info>
|
||||
* All: <info>%command.full_name% --{$authorOnly} --{$domainOnly}=example.com --{$noOrphanVisits}</info>
|
||||
HELP;
|
||||
|
||||
$this
|
||||
|
@ -86,6 +89,12 @@ class GenerateKeyCommand extends Command
|
|||
Role::DOMAIN_SPECIFIC->value,
|
||||
),
|
||||
)
|
||||
->addOption(
|
||||
$noOrphanVisits,
|
||||
'o',
|
||||
InputOption::VALUE_NONE,
|
||||
sprintf('Adds the "%s" role to the new API key.', Role::NO_ORPHAN_VISITS->value),
|
||||
)
|
||||
->setHelp($help);
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ class ListKeysCommand extends Command
|
|||
|
||||
public const NAME = 'api-key:list';
|
||||
|
||||
public function __construct(private ApiKeyServiceInterface $apiKeyService)
|
||||
public function __construct(private readonly ApiKeyServiceInterface $apiKeyService)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
@ -60,10 +60,7 @@ class ListKeysCommand extends Command
|
|||
}
|
||||
$rowData[] = $expiration?->toAtomString() ?? '-';
|
||||
$rowData[] = ApiKey::isAdmin($apiKey) ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
||||
fn (Role $role, array $meta) =>
|
||||
empty($meta)
|
||||
? $role->toFriendlyName()
|
||||
: sprintf('%s: %s', $role->toFriendlyName(), Role::domainAuthorityFromMeta($meta)),
|
||||
fn (Role $role, array $meta) => $role->toFriendlyName($meta),
|
||||
));
|
||||
|
||||
return $rowData;
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Visit;
|
|||
|
||||
use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
|
@ -41,7 +42,7 @@ class DownloadGeoLiteDbCommand extends Command
|
|||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
try {
|
||||
$this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) use ($io): void {
|
||||
$result = $this->dbUpdater->checkDbUpdate(function (bool $olderDbExists) use ($io): void {
|
||||
$io->text(sprintf('<fg=blue>%s GeoLite2 db file...</>', $olderDbExists ? 'Updating' : 'Downloading'));
|
||||
$this->progressBar = new ProgressBar($io);
|
||||
}, function (int $total, int $downloaded): void {
|
||||
|
@ -49,6 +50,11 @@ class DownloadGeoLiteDbCommand extends Command
|
|||
$this->progressBar?->setProgress($downloaded);
|
||||
});
|
||||
|
||||
if ($result === GeolocationResult::LICENSE_MISSING) {
|
||||
$io->warning('It was not possible to download GeoLite2 db, because a license was not provided.');
|
||||
return ExitCode::EXIT_WARNING;
|
||||
}
|
||||
|
||||
if ($this->progressBar === null) {
|
||||
$io->info('GeoLite2 db file is up to date.');
|
||||
} else {
|
||||
|
@ -58,21 +64,26 @@ class DownloadGeoLiteDbCommand extends Command
|
|||
|
||||
return ExitCode::EXIT_SUCCESS;
|
||||
} catch (GeolocationDbUpdateFailedException $e) {
|
||||
$olderDbExists = $e->olderDbExists();
|
||||
|
||||
if ($olderDbExists) {
|
||||
$io->warning(
|
||||
'GeoLite2 db file update failed. Visits will continue to be located with the old version.',
|
||||
);
|
||||
} else {
|
||||
$io->error('GeoLite2 db file download failed. It will not be possible to locate visits.');
|
||||
}
|
||||
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $io);
|
||||
}
|
||||
|
||||
return $olderDbExists ? ExitCode::EXIT_WARNING : ExitCode::EXIT_FAILURE;
|
||||
return $this->processGeoLiteUpdateError($e, $io);
|
||||
}
|
||||
}
|
||||
|
||||
private function processGeoLiteUpdateError(GeolocationDbUpdateFailedException $e, SymfonyStyle $io): int
|
||||
{
|
||||
$olderDbExists = $e->olderDbExists();
|
||||
|
||||
if ($olderDbExists) {
|
||||
$io->warning(
|
||||
'GeoLite2 db file update failed. Visits will continue to be located with the old version.',
|
||||
);
|
||||
} else {
|
||||
$io->error('GeoLite2 db file download failed. It will not be possible to locate visits.');
|
||||
}
|
||||
|
||||
if ($io->isVerbose()) {
|
||||
$this->getApplication()?->renderThrowable($e, $io);
|
||||
}
|
||||
|
||||
return $olderDbExists ? ExitCode::EXIT_WARNING : ExitCode::EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
$buildTimestamp = $this->resolveBuildTimestamp($meta);
|
||||
$buildDate = Chronos::createFromTimestamp($buildTimestamp);
|
||||
|
||||
return Chronos::now()->gt($buildDate->addDays(35));
|
||||
return Chronos::now()->greaterThan($buildDate->addDays(35));
|
||||
}
|
||||
|
||||
private function resolveBuildTimestamp(Metadata $meta): int
|
||||
|
@ -108,8 +108,7 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
|
|||
$this->dbUpdater->downloadFreshCopy($this->wrapHandleProgressCallback($handleProgress, $olderDbExists));
|
||||
return $olderDbExists ? GeolocationResult::DB_UPDATED : GeolocationResult::DB_CREATED;
|
||||
} catch (MissingLicenseException) {
|
||||
// If there's no license key, just ignore the error
|
||||
return GeolocationResult::CHECK_SKIPPED;
|
||||
return GeolocationResult::LICENSE_MISSING;
|
||||
} catch (DbUpdateException | WrongIpException $e) {
|
||||
throw $olderDbExists
|
||||
? GeolocationDbUpdateFailedException::withOlderDb($e)
|
||||
|
|
|
@ -5,6 +5,7 @@ namespace Shlinkio\Shlink\CLI\GeoLite;
|
|||
enum GeolocationResult
|
||||
{
|
||||
case CHECK_SKIPPED;
|
||||
case LICENSE_MISSING;
|
||||
case DB_CREATED;
|
||||
case DB_UPDATED;
|
||||
case DB_IS_UP_TO_DATE;
|
||||
|
|
|
@ -19,11 +19,7 @@ use function unlink;
|
|||
|
||||
class ImportShortUrlsTest extends CliTestCase
|
||||
{
|
||||
/**
|
||||
* @var false|string|null
|
||||
* @todo Use native type once PHP 8.1 support is dropped
|
||||
*/
|
||||
private mixed $tempCsvFile = null;
|
||||
private false|string|null $tempCsvFile = null;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
|
|
|
@ -24,36 +24,40 @@ class ListApiKeysTest extends CliTestCase
|
|||
|
||||
public static function provideFlags(): iterable
|
||||
{
|
||||
$expiredApiKeyDate = Chronos::now()->subDay()->startOfDay()->toAtomString();
|
||||
$expiredApiKeyDate = Chronos::now()->subDays(1)->startOfDay()->toAtomString();
|
||||
$enabledOnlyOutput = <<<OUT
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| Key | Name | Expiration date | Roles |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| valid_api_key | - | - | Admin |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| expired_api_key | - | {$expiredApiKeyDate} | Admin |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| author_api_key | - | - | Author only |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
| domain_api_key | - | - | Domain only: example.com |
|
||||
+-----------------+------+---------------------------+--------------------------+
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| Key | Name | Expiration date | Roles |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| valid_api_key | - | - | Admin |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| expired_api_key | - | {$expiredApiKeyDate} | Admin |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| author_api_key | - | - | Author only |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| domain_api_key | - | - | Domain only: example.com |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
| no_orphans_api_key | - | - | No orphan visits |
|
||||
+--------------------+------+---------------------------+--------------------------+
|
||||
|
||||
OUT;
|
||||
|
||||
yield 'no flags' => [[], <<<OUT
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| Key | Name | Is enabled | Expiration date | Roles |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| valid_api_key | - | +++ | - | Admin |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| disabled_api_key | - | --- | - | Admin |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| expired_api_key | - | --- | {$expiredApiKeyDate} | Admin |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| author_api_key | - | +++ | - | Author only |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
| domain_api_key | - | +++ | - | Domain only: example.com |
|
||||
+------------------+------+------------+---------------------------+--------------------------+
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| Key | Name | Is enabled | Expiration date | Roles |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| valid_api_key | - | +++ | - | Admin |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| disabled_api_key | - | --- | - | Admin |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| expired_api_key | - | --- | {$expiredApiKeyDate} | Admin |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| author_api_key | - | +++ | - | Author only |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| domain_api_key | - | +++ | - | Domain only: example.com |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
| no_orphans_api_key | - | +++ | - | No orphan visits |
|
||||
+--------------------+------+------------+---------------------------+--------------------------+
|
||||
|
||||
OUT];
|
||||
yield '-e' => [['-e'], $enabledOnlyOutput];
|
||||
|
|
|
@ -10,20 +10,18 @@ use PHPUnit\Framework\TestCase;
|
|||
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
|
||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DisableKeyCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->createMock(ApiKeyServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DisableKeyCommand($this->apiKeyService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DisableKeyCommand($this->apiKeyService));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -13,14 +13,12 @@ use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
|
|||
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class GenerateKeyCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
|
@ -31,7 +29,7 @@ class GenerateKeyCommandTest extends TestCase
|
|||
$roleResolver->method('determineRoles')->with($this->isInstanceOf(InputInterface::class))->willReturn([]);
|
||||
|
||||
$command = new GenerateKeyCommand($this->apiKeyService, $roleResolver);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -11,21 +11,19 @@ use PHPUnit\Framework\TestCase;
|
|||
use Shlinkio\Shlink\CLI\Command\Api\InitialApiKeyCommand;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class InitialApiKeyCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->createMock(ApiKeyServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new InitialApiKeyCommand($this->apiKeyService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new InitialApiKeyCommand($this->apiKeyService));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideParams')]
|
||||
|
|
|
@ -15,20 +15,18 @@ use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
|||
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class ListKeysCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ApiKeyServiceInterface $apiKeyService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->apiKeyService = $this->createMock(ApiKeyServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ListKeysCommand($this->apiKeyService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new ListKeysCommand($this->apiKeyService));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideKeysAndOutputs')]
|
||||
|
|
|
@ -18,7 +18,7 @@ use PHPUnit\Framework\MockObject\MockObject;
|
|||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
|
@ -27,8 +27,6 @@ use Symfony\Component\Process\PhpExecutableFinder;
|
|||
|
||||
class CreateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ProcessRunnerInterface $processHelper;
|
||||
private MockObject & Connection $regularConn;
|
||||
|
@ -63,7 +61,7 @@ class CreateDatabaseCommandTest extends TestCase
|
|||
$noDbNameConn->method('createSchemaManager')->withAnyParameters()->willReturn($this->schemaManager);
|
||||
|
||||
$command = new CreateDatabaseCommand($locker, $this->processHelper, $phpExecutableFinder, $em, $noDbNameConn);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -9,7 +9,7 @@ use PHPUnit\Framework\MockObject\MockObject;
|
|||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
|
||||
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
use Symfony\Component\Lock\LockFactory;
|
||||
|
@ -18,8 +18,6 @@ use Symfony\Component\Process\PhpExecutableFinder;
|
|||
|
||||
class MigrateDatabaseCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ProcessRunnerInterface $processHelper;
|
||||
|
||||
|
@ -36,7 +34,7 @@ class MigrateDatabaseCommandTest extends TestCase
|
|||
$this->processHelper = $this->createMock(ProcessRunnerInterface::class);
|
||||
|
||||
$command = new MigrateDatabaseCommand($locker, $this->processHelper, $phpExecutableFinder);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -14,22 +14,20 @@ use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
|||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function substr_count;
|
||||
|
||||
class DomainRedirectsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & DomainServiceInterface $domainService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->domainService = $this->createMock(DomainServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DomainRedirectsCommand($this->domainService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DomainRedirectsCommand($this->domainService));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideDomains')]
|
||||
|
|
|
@ -17,13 +17,11 @@ use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
|||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class GetDomainVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
||||
|
@ -33,7 +31,7 @@ class GetDomainVisitsCommandTest extends TestCase
|
|||
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
||||
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
|
||||
|
||||
$this->commandTester = $this->testerForCommand(
|
||||
$this->commandTester = CliTestUtils::testerForCommand(
|
||||
new GetDomainVisitsCommand($this->visitsHelper, $this->stringifier),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,20 +15,18 @@ use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
|||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class ListDomainsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & DomainServiceInterface $domainService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->domainService = $this->createMock(DomainServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ListDomainsCommand($this->domainService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new ListDomainsCommand($this->domainService));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideInputsAndOutputs')]
|
||||
|
|
|
@ -20,14 +20,12 @@ use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
|||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\UrlShorteningResult;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class CreateShortUrlCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & UrlShortenerInterface $urlShortener;
|
||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
||||
|
@ -45,7 +43,7 @@ class CreateShortUrlCommandTest extends TestCase
|
|||
defaultShortCodesLength: 5,
|
||||
),
|
||||
);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -12,7 +12,7 @@ use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
|
|||
use Shlinkio\Shlink\Core\Exception;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function sprintf;
|
||||
|
@ -21,15 +21,13 @@ use const PHP_EOL;
|
|||
|
||||
class DeleteShortUrlCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & DeleteShortUrlServiceInterface $service;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->service = $this->createMock(DeleteShortUrlServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteShortUrlCommand($this->service));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DeleteShortUrlCommand($this->service));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -13,20 +13,18 @@ use Shlinkio\Shlink\CLI\Util\ExitCode;
|
|||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\Model\BulkDeleteResult;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DeleteShortUrlVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ShortUrlVisitsDeleterInterface $deleter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->deleter = $this->createMock(ShortUrlVisitsDeleterInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteShortUrlVisitsCommand($this->deleter));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DeleteShortUrlVisitsCommand($this->deleter));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideCancellingInputs')]
|
||||
|
|
|
@ -20,7 +20,7 @@ use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
|||
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||
|
@ -28,8 +28,6 @@ use function sprintf;
|
|||
|
||||
class GetShortUrlVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
||||
|
||||
|
@ -37,7 +35,7 @@ class GetShortUrlVisitsCommandTest extends TestCase
|
|||
{
|
||||
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
||||
$command = new GetShortUrlVisitsCommand($this->visitsHelper);
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -21,7 +21,7 @@ use Shlinkio\Shlink\Core\ShortUrl\ShortUrlListServiceInterface;
|
|||
use Shlinkio\Shlink\Core\ShortUrl\Transformer\ShortUrlDataTransformer;
|
||||
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function count;
|
||||
|
@ -29,8 +29,6 @@ use function explode;
|
|||
|
||||
class ListShortUrlsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ShortUrlListServiceInterface $shortUrlService;
|
||||
|
||||
|
@ -40,7 +38,7 @@ class ListShortUrlsCommandTest extends TestCase
|
|||
$command = new ListShortUrlsCommand($this->shortUrlService, new ShortUrlDataTransformer(
|
||||
new ShortUrlStringifier([]),
|
||||
));
|
||||
$this->commandTester = $this->testerForCommand($command);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -12,7 +12,7 @@ use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
|||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function sprintf;
|
||||
|
@ -21,15 +21,13 @@ use const PHP_EOL;
|
|||
|
||||
class ResolveUrlCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & ShortUrlResolverInterface $urlResolver;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->urlResolver = $this->createMock(ShortUrlResolverInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ResolveUrlCommand($this->urlResolver));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new ResolveUrlCommand($this->urlResolver));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -9,20 +9,18 @@ use PHPUnit\Framework\MockObject\MockObject;
|
|||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DeleteTagsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & TagServiceInterface $tagService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->createMock(TagServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteTagsCommand($this->tagService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DeleteTagsCommand($this->tagService));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -17,13 +17,11 @@ use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
|||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class GetTagVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
||||
|
@ -33,7 +31,7 @@ class GetTagVisitsCommandTest extends TestCase
|
|||
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
||||
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
|
||||
|
||||
$this->commandTester = $this->testerForCommand(
|
||||
$this->commandTester = CliTestUtils::testerForCommand(
|
||||
new GetTagVisitsCommand($this->visitsHelper, $this->stringifier),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,20 +12,18 @@ use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
|
|||
use Shlinkio\Shlink\Common\Paginator\Paginator;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class ListTagsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & TagServiceInterface $tagService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->createMock(TagServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new ListTagsCommand($this->tagService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new ListTagsCommand($this->tagService));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -12,20 +12,18 @@ use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
|
|||
use Shlinkio\Shlink\Core\Tag\Entity\Tag;
|
||||
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
|
||||
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class RenameTagCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & TagServiceInterface $tagService;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->tagService = $this->createMock(TagServiceInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new RenameTagCommand($this->tagService));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new RenameTagCommand($this->tagService));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -11,20 +11,18 @@ use Shlinkio\Shlink\CLI\Command\Visit\DeleteOrphanVisitsCommand;
|
|||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use Shlinkio\Shlink\Core\Model\BulkDeleteResult;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsDeleterInterface;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class DeleteOrphanVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & VisitsDeleterInterface $deleter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->deleter = $this->createMock(VisitsDeleterInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DeleteOrphanVisitsCommand($this->deleter));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DeleteOrphanVisitsCommand($this->deleter));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -13,22 +13,20 @@ use Shlinkio\Shlink\CLI\Exception\GeolocationDbUpdateFailedException;
|
|||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdaterInterface;
|
||||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||
use Shlinkio\Shlink\CLI\Util\ExitCode;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class DownloadGeoLiteDbCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & GeolocationDbUpdaterInterface $dbUpdater;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->dbUpdater = $this->createMock(GeolocationDbUpdaterInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new DownloadGeoLiteDbCommand($this->dbUpdater));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new DownloadGeoLiteDbCommand($this->dbUpdater));
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideFailureParams')]
|
||||
|
@ -74,6 +72,21 @@ class DownloadGeoLiteDbCommandTest extends TestCase
|
|||
];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function warningIsPrintedWhenLicenseIsMissing(): void
|
||||
{
|
||||
$this->dbUpdater->expects($this->once())->method('checkDbUpdate')->withAnyParameters()->willReturn(
|
||||
GeolocationResult::LICENSE_MISSING,
|
||||
);
|
||||
|
||||
$this->commandTester->execute([]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
self::assertStringContainsString('[WARNING] It was not possible to download GeoLite2 db', $output);
|
||||
self::assertSame(ExitCode::EXIT_WARNING, $exitCode);
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideSuccessParams')]
|
||||
public function printsExpectedMessageWhenNoErrorOccurs(callable $checkUpdateBehavior, string $expectedMessage): void
|
||||
{
|
||||
|
|
|
@ -17,13 +17,11 @@ use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
|||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class GetNonOrphanVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
||||
private MockObject & ShortUrlStringifierInterface $stringifier;
|
||||
|
@ -33,7 +31,7 @@ class GetNonOrphanVisitsCommandTest extends TestCase
|
|||
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
||||
$this->stringifier = $this->createMock(ShortUrlStringifierInterface::class);
|
||||
|
||||
$this->commandTester = $this->testerForCommand(
|
||||
$this->commandTester = CliTestUtils::testerForCommand(
|
||||
new GetNonOrphanVisitsCommand($this->visitsHelper, $this->stringifier),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,20 +15,18 @@ use Shlinkio\Shlink\Core\Visit\Entity\VisitLocation;
|
|||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class GetOrphanVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & VisitsStatsHelperInterface $visitsHelper;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->visitsHelper = $this->createMock(VisitsStatsHelperInterface::class);
|
||||
$this->commandTester = $this->testerForCommand(new GetOrphanVisitsCommand($this->visitsHelper));
|
||||
$this->commandTester = CliTestUtils::testerForCommand(new GetOrphanVisitsCommand($this->visitsHelper));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
|
|
|
@ -21,7 +21,7 @@ use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelperInterface;
|
|||
use Shlinkio\Shlink\Core\Visit\Model\Visitor;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\WrongIpException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Model\Location;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Exception\RuntimeException;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
@ -34,8 +34,6 @@ use const PHP_EOL;
|
|||
|
||||
class LocateVisitsCommandTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private CommandTester $commandTester;
|
||||
private MockObject & VisitLocatorInterface $visitService;
|
||||
private MockObject & VisitToLocationHelperInterface $visitToLocation;
|
||||
|
@ -53,8 +51,8 @@ class LocateVisitsCommandTest extends TestCase
|
|||
|
||||
$command = new LocateVisitsCommand($this->visitService, $this->visitToLocation, $locker);
|
||||
|
||||
$this->downloadDbCommand = $this->createCommandMock(DownloadGeoLiteDbCommand::NAME);
|
||||
$this->commandTester = $this->testerForCommand($command, $this->downloadDbCommand);
|
||||
$this->downloadDbCommand = CliTestUtils::createCommandMock(DownloadGeoLiteDbCommand::NAME);
|
||||
$this->commandTester = CliTestUtils::testerForCommand($command, $this->downloadDbCommand);
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideArgs')]
|
||||
|
|
|
@ -9,12 +9,10 @@ use PHPUnit\Framework\Attributes\Test;
|
|||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||
use ShlinkioTest\Shlink\CLI\CliTestUtilsTrait;
|
||||
use ShlinkioTest\Shlink\CLI\Util\CliTestUtils;
|
||||
|
||||
class ApplicationFactoryTest extends TestCase
|
||||
{
|
||||
use CliTestUtilsTrait;
|
||||
|
||||
private ApplicationFactory $factory;
|
||||
|
||||
protected function setUp(): void
|
||||
|
@ -32,8 +30,8 @@ class ApplicationFactoryTest extends TestCase
|
|||
'baz' => 'baz',
|
||||
],
|
||||
]);
|
||||
$sm->setService('foo', $this->createCommandMock('foo'));
|
||||
$sm->setService('bar', $this->createCommandMock('bar'));
|
||||
$sm->setService('foo', CliTestUtils::createCommandMock('foo'));
|
||||
$sm->setService('bar', CliTestUtils::createCommandMock('bar'));
|
||||
|
||||
$instance = ($this->factory)($sm);
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater;
|
|||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationResult;
|
||||
use Shlinkio\Shlink\Core\Options\TrackingOptions;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\DbUpdateException;
|
||||
use Shlinkio\Shlink\IpGeolocation\Exception\MissingLicenseException;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdaterInterface;
|
||||
use Symfony\Component\Lock;
|
||||
use Throwable;
|
||||
|
@ -37,6 +38,21 @@ class GeolocationDbUpdaterTest extends TestCase
|
|||
$this->lock->method('acquire')->with($this->isTrue())->willReturn(true);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function properResultIsReturnedWhenLicenseIsMissing(): void
|
||||
{
|
||||
$mustBeUpdated = fn () => self::assertTrue(true);
|
||||
|
||||
$this->dbUpdater->expects($this->once())->method('databaseFileExists')->willReturn(false);
|
||||
$this->dbUpdater->expects($this->once())->method('downloadFreshCopy')->willThrowException(
|
||||
new MissingLicenseException(''),
|
||||
);
|
||||
$this->geoLiteDbReader->expects($this->never())->method('metadata');
|
||||
|
||||
$result = $this->geolocationDbUpdater()->checkDbUpdate($mustBeUpdated);
|
||||
self::assertEquals(GeolocationResult::LICENSE_MISSING, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function exceptionIsThrownWhenOlderDbDoesNotExistAndDownloadFails(): void
|
||||
{
|
||||
|
|
|
@ -2,20 +2,34 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\CLI;
|
||||
namespace ShlinkioTest\Shlink\CLI\Util;
|
||||
|
||||
use PHPUnit\Framework\Assert;
|
||||
use PHPUnit\Framework\MockObject\Generator\Generator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputDefinition;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
trait CliTestUtilsTrait
|
||||
class CliTestUtils
|
||||
{
|
||||
private function createCommandMock(string $name): MockObject & Command
|
||||
public static function createCommandMock(string $name): MockObject & Command
|
||||
{
|
||||
$command = $this->createMock(Command::class);
|
||||
static $generator = null;
|
||||
|
||||
if ($generator === null) {
|
||||
$generator = new Generator();
|
||||
}
|
||||
|
||||
$command = $generator->testDouble(
|
||||
Command::class,
|
||||
mockObject: true,
|
||||
callOriginalConstructor: false,
|
||||
callOriginalClone: false,
|
||||
cloneArguments: false,
|
||||
allowMockingUnknownTypes: false,
|
||||
);
|
||||
$command->method('getName')->willReturn($name);
|
||||
$command->method('isEnabled')->willReturn(true);
|
||||
$command->method('getAliases')->willReturn([]);
|
||||
|
@ -25,7 +39,7 @@ trait CliTestUtilsTrait
|
|||
return $command;
|
||||
}
|
||||
|
||||
private function testerForCommand(Command $mainCommand, Command ...$extraCommands): CommandTester
|
||||
public static function testerForCommand(Command $mainCommand, Command ...$extraCommands): CommandTester
|
||||
{
|
||||
$app = new Application();
|
||||
$app->add($mainCommand);
|
|
@ -9,7 +9,6 @@ use Laminas\ServiceManager\Factory\InvokableFactory;
|
|||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Shlinkio\Shlink\Common\Doctrine\EntityRepositoryFactory;
|
||||
use Shlinkio\Shlink\Config\Factory\ValinorConfigFactory;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
|
@ -93,6 +92,9 @@ return [
|
|||
Importer\ImportedLinksProcessor::class => ConfigAbstractFactory::class,
|
||||
|
||||
Crawling\CrawlingHelper::class => ConfigAbstractFactory::class,
|
||||
|
||||
Matomo\MatomoOptions::class => [ValinorConfigFactory::class, 'config.matomo'],
|
||||
Matomo\MatomoTrackerBuilder::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
|
||||
'aliases' => [
|
||||
|
@ -101,6 +103,8 @@ return [
|
|||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
Matomo\MatomoTrackerBuilder::class => [Matomo\MatomoOptions::class],
|
||||
|
||||
ErrorHandler\NotFoundTypeResolverMiddleware::class => ['config.router.base_path'],
|
||||
ErrorHandler\NotFoundTrackerMiddleware::class => [Visit\RequestTracker::class],
|
||||
ErrorHandler\NotFoundRedirectHandler::class => [
|
||||
|
|
|
@ -9,144 +9,189 @@ use Psr\EventDispatcher\EventDispatcherInterface;
|
|||
use Shlinkio\Shlink\CLI\GeoLite\GeolocationDbUpdater;
|
||||
use Shlinkio\Shlink\Common\Cache\RedisPublishingHelper;
|
||||
use Shlinkio\Shlink\Common\Mercure\MercureHubPublishingHelper;
|
||||
use Shlinkio\Shlink\Common\Mercure\MercureOptions;
|
||||
use Shlinkio\Shlink\Common\RabbitMq\RabbitMqPublishingHelper;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitLocator;
|
||||
use Shlinkio\Shlink\Core\Visit\Geolocation\VisitToLocationHelper;
|
||||
use Shlinkio\Shlink\EventDispatcher\Listener\EnabledListenerCheckerInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options;
|
||||
use Shlinkio\Shlink\IpGeolocation\Resolver\IpLocationResolverInterface;
|
||||
|
||||
return [
|
||||
use function Shlinkio\Shlink\Config\runningInOpenswoole;
|
||||
use function Shlinkio\Shlink\Config\runningInRoadRunner;
|
||||
|
||||
'events' => [
|
||||
'regular' => [
|
||||
EventDispatcher\Event\UrlVisited::class => [
|
||||
EventDispatcher\LocateVisit::class,
|
||||
],
|
||||
EventDispatcher\Event\GeoLiteDbCreated::class => [
|
||||
EventDispatcher\LocateUnlocatedVisits::class,
|
||||
],
|
||||
return (static function (): array {
|
||||
$regularEvents = [
|
||||
EventDispatcher\Event\UrlVisited::class => [
|
||||
EventDispatcher\LocateVisit::class,
|
||||
],
|
||||
'async' => [
|
||||
EventDispatcher\Event\VisitLocated::class => [
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class,
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class,
|
||||
],
|
||||
EventDispatcher\Event\ShortUrlCreated::class => [
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class,
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class,
|
||||
],
|
||||
EventDispatcher\Event\GeoLiteDbCreated::class => [
|
||||
EventDispatcher\LocateUnlocatedVisits::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
$asyncEvents = [
|
||||
EventDispatcher\Event\VisitLocated::class => [
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class,
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class,
|
||||
],
|
||||
EventDispatcher\Event\ShortUrlCreated::class => [
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class,
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class,
|
||||
],
|
||||
];
|
||||
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\LocateUnlocatedVisits::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class => ConfigAbstractFactory::class,
|
||||
// Send visits to matomo asynchronously if the runtime allows it
|
||||
if (runningInRoadRunner() || runningInOpenswoole()) {
|
||||
$asyncEvents[EventDispatcher\Event\VisitLocated::class][] = EventDispatcher\Matomo\SendVisitToMatomo::class;
|
||||
} else {
|
||||
$regularEvents[EventDispatcher\Event\VisitLocated::class] = [EventDispatcher\Matomo\SendVisitToMatomo::class];
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
'events' => [
|
||||
'regular' => $regularEvents,
|
||||
'async' => $asyncEvents,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
'dependencies' => [
|
||||
'factories' => [
|
||||
EventDispatcher\LocateVisit::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Matomo\SendVisitToMatomo::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\LocateUnlocatedVisits::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => ConfigAbstractFactory::class,
|
||||
EventDispatcher\UpdateGeoLiteDb::class => ConfigAbstractFactory::class,
|
||||
|
||||
EventDispatcher\Helper\EnabledListenerChecker::class => ConfigAbstractFactory::class,
|
||||
],
|
||||
|
||||
'aliases' => [
|
||||
EnabledListenerCheckerInterface::class => EventDispatcher\Helper\EnabledListenerChecker::class,
|
||||
],
|
||||
|
||||
'delegators' => [
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
EventDispatcher\LocateVisit::class => [
|
||||
IpLocationResolverInterface::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
DbUpdater::class,
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, VisitToLocationHelper::class],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
'httpClient',
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Options\WebhookOptions::class,
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||
Options\AppOptions::class,
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
MercureHubPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
MercureHubPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
RabbitMqPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||
Options\RabbitMqOptions::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
RabbitMqPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Options\RabbitMqOptions::class,
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
RedisPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
'config.redis.pub_sub_enabled',
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
RedisPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
'config.redis.pub_sub_enabled',
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
EventDispatcher\CloseDbConnectionEventListenerDelegator::class,
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
EventDispatcher\LocateVisit::class => [
|
||||
IpLocationResolverInterface::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
DbUpdater::class,
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
EventDispatcher\LocateUnlocatedVisits::class => [VisitLocator::class, VisitToLocationHelper::class],
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => [
|
||||
'httpClient',
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Options\WebhookOptions::class,
|
||||
ShortUrl\Transformer\ShortUrlDataTransformer::class,
|
||||
Options\AppOptions::class,
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class => [
|
||||
MercureHubPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => [
|
||||
MercureHubPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class => [
|
||||
RabbitMqPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Visit\Transformer\OrphanVisitDataTransformer::class,
|
||||
Options\RabbitMqOptions::class,
|
||||
],
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => [
|
||||
RabbitMqPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
Options\RabbitMqOptions::class,
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class => [
|
||||
RedisPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
'config.redis.pub_sub_enabled',
|
||||
],
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => [
|
||||
RedisPublishingHelper::class,
|
||||
EventDispatcher\PublishingUpdatesGenerator::class,
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
'config.redis.pub_sub_enabled',
|
||||
],
|
||||
EventDispatcher\UpdateGeoLiteDb::class => [
|
||||
GeolocationDbUpdater::class,
|
||||
'Logger_Shlink',
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
],
|
||||
EventDispatcher\Matomo\SendVisitToMatomo::class => [
|
||||
'em',
|
||||
'Logger_Shlink',
|
||||
ShortUrlStringifier::class,
|
||||
Matomo\MatomoOptions::class,
|
||||
Matomo\MatomoTrackerBuilder::class,
|
||||
],
|
||||
|
||||
];
|
||||
EventDispatcher\UpdateGeoLiteDb::class => [
|
||||
GeolocationDbUpdater::class,
|
||||
'Logger_Shlink',
|
||||
EventDispatcherInterface::class,
|
||||
],
|
||||
|
||||
EventDispatcher\Helper\EnabledListenerChecker::class => [
|
||||
Options\RabbitMqOptions::class,
|
||||
'config.redis.pub_sub_enabled',
|
||||
MercureOptions::class,
|
||||
Options\WebhookOptions::class,
|
||||
GeoLite2Options::class,
|
||||
MatomoOptions::class,
|
||||
],
|
||||
],
|
||||
|
||||
];
|
||||
})();
|
||||
|
|
|
@ -17,25 +17,32 @@ enum EnvVars: string
|
|||
case DB_UNIX_SOCKET = 'DB_UNIX_SOCKET';
|
||||
case DB_PORT = 'DB_PORT';
|
||||
case GEOLITE_LICENSE_KEY = 'GEOLITE_LICENSE_KEY';
|
||||
case CACHE_NAMESPACE = 'CACHE_NAMESPACE';
|
||||
case REDIS_SERVERS = 'REDIS_SERVERS';
|
||||
case REDIS_SENTINEL_SERVICE = 'REDIS_SENTINEL_SERVICE';
|
||||
case REDIS_DECODE_CREDENTIALS = 'REDIS_DECODE_CREDENTIALS';
|
||||
case REDIS_PUB_SUB_ENABLED = 'REDIS_PUB_SUB_ENABLED';
|
||||
case MERCURE_PUBLIC_HUB_URL = 'MERCURE_PUBLIC_HUB_URL';
|
||||
case MERCURE_INTERNAL_HUB_URL = 'MERCURE_INTERNAL_HUB_URL';
|
||||
case MERCURE_JWT_SECRET = 'MERCURE_JWT_SECRET';
|
||||
case DEFAULT_QR_CODE_SIZE = 'DEFAULT_QR_CODE_SIZE';
|
||||
case DEFAULT_QR_CODE_MARGIN = 'DEFAULT_QR_CODE_MARGIN';
|
||||
case DEFAULT_QR_CODE_FORMAT = 'DEFAULT_QR_CODE_FORMAT';
|
||||
case DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION';
|
||||
case DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE';
|
||||
case RABBITMQ_ENABLED = 'RABBITMQ_ENABLED';
|
||||
case RABBITMQ_HOST = 'RABBITMQ_HOST';
|
||||
case RABBITMQ_PORT = 'RABBITMQ_PORT';
|
||||
case RABBITMQ_USER = 'RABBITMQ_USER';
|
||||
case RABBITMQ_PASSWORD = 'RABBITMQ_PASSWORD';
|
||||
case RABBITMQ_VHOST = 'RABBITMQ_VHOST';
|
||||
case RABBITMQ_USE_SSL = 'RABBITMQ_USE_SSL';
|
||||
/** @deprecated */
|
||||
case RABBITMQ_LEGACY_VISITS_PUBLISHING = 'RABBITMQ_LEGACY_VISITS_PUBLISHING';
|
||||
case MATOMO_ENABLED = 'MATOMO_ENABLED';
|
||||
case MATOMO_BASE_URL = 'MATOMO_BASE_URL';
|
||||
case MATOMO_SITE_ID = 'MATOMO_SITE_ID';
|
||||
case MATOMO_API_TOKEN = 'MATOMO_API_TOKEN';
|
||||
case DEFAULT_QR_CODE_SIZE = 'DEFAULT_QR_CODE_SIZE';
|
||||
case DEFAULT_QR_CODE_MARGIN = 'DEFAULT_QR_CODE_MARGIN';
|
||||
case DEFAULT_QR_CODE_FORMAT = 'DEFAULT_QR_CODE_FORMAT';
|
||||
case DEFAULT_QR_CODE_ERROR_CORRECTION = 'DEFAULT_QR_CODE_ERROR_CORRECTION';
|
||||
case DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = 'DEFAULT_QR_CODE_ROUND_BLOCK_SIZE';
|
||||
case DEFAULT_INVALID_SHORT_URL_REDIRECT = 'DEFAULT_INVALID_SHORT_URL_REDIRECT';
|
||||
case DEFAULT_REGULAR_404_REDIRECT = 'DEFAULT_REGULAR_404_REDIRECT';
|
||||
case DEFAULT_BASE_URL_REDIRECT = 'DEFAULT_BASE_URL_REDIRECT';
|
||||
|
|
|
@ -79,6 +79,7 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe
|
|||
yield from $apiKey?->mapRoles(fn (Role $role, array $meta) => match ($role) {
|
||||
Role::DOMAIN_SPECIFIC => ['d', new IsDomain(Role::domainIdFromMeta($meta))],
|
||||
Role::AUTHORED_SHORT_URLS => ['s', new BelongsToApiKey($apiKey)],
|
||||
default => null,
|
||||
}) ?? [];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,17 +9,19 @@ use Shlinkio\Shlink\EventDispatcher\Util\JsonUnserializable;
|
|||
|
||||
abstract class AbstractVisitEvent implements JsonSerializable, JsonUnserializable
|
||||
{
|
||||
final public function __construct(public readonly string $visitId)
|
||||
{
|
||||
final public function __construct(
|
||||
public readonly string $visitId,
|
||||
public readonly ?string $originalIpAddress = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return ['visitId' => $this->visitId];
|
||||
return ['visitId' => $this->visitId, 'originalIpAddress' => $this->originalIpAddress];
|
||||
}
|
||||
|
||||
public static function fromPayload(array $payload): self
|
||||
{
|
||||
return new static($payload['visitId'] ?? '');
|
||||
return new static($payload['visitId'] ?? '', $payload['originalIpAddress'] ?? null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,18 +6,4 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
|
|||
|
||||
final class UrlVisited extends AbstractVisitEvent
|
||||
{
|
||||
private ?string $originalIpAddress = null;
|
||||
|
||||
public static function withOriginalIpAddress(string $visitId, ?string $originalIpAddress): self
|
||||
{
|
||||
$instance = new self($visitId);
|
||||
$instance->originalIpAddress = $originalIpAddress;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
public function originalIpAddress(): ?string
|
||||
{
|
||||
return $this->originalIpAddress;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher\Helper;
|
||||
|
||||
use Shlinkio\Shlink\Common\Mercure\MercureOptions;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\Options\RabbitMqOptions;
|
||||
use Shlinkio\Shlink\Core\Options\WebhookOptions;
|
||||
use Shlinkio\Shlink\EventDispatcher\Listener\EnabledListenerCheckerInterface;
|
||||
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2Options;
|
||||
|
||||
class EnabledListenerChecker implements EnabledListenerCheckerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RabbitMqOptions $rabbitMqOptions,
|
||||
private readonly bool $redisPubSubEnabled,
|
||||
private readonly MercureOptions $mercureOptions,
|
||||
private readonly WebhookOptions $webhookOptions,
|
||||
private readonly GeoLite2Options $geoLiteOptions,
|
||||
private readonly MatomoOptions $matomoOptions,
|
||||
) {
|
||||
}
|
||||
|
||||
public function shouldRegisterListener(string $event, string $listener, bool $isAsync): bool
|
||||
{
|
||||
if (! $isAsync) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return match ($listener) {
|
||||
EventDispatcher\RabbitMq\NotifyVisitToRabbitMq::class,
|
||||
EventDispatcher\RabbitMq\NotifyNewShortUrlToRabbitMq::class => $this->rabbitMqOptions->enabled,
|
||||
EventDispatcher\RedisPubSub\NotifyVisitToRedis::class,
|
||||
EventDispatcher\RedisPubSub\NotifyNewShortUrlToRedis::class => $this->redisPubSubEnabled,
|
||||
EventDispatcher\Mercure\NotifyVisitToMercure::class,
|
||||
EventDispatcher\Mercure\NotifyNewShortUrlToMercure::class => $this->mercureOptions->isEnabled(),
|
||||
EventDispatcher\Matomo\SendVisitToMatomo::class => $this->matomoOptions->enabled,
|
||||
EventDispatcher\NotifyVisitToWebHooks::class => $this->webhookOptions->hasWebhooks(),
|
||||
EventDispatcher\UpdateGeoLiteDb::class => $this->geoLiteOptions->hasLicenseKey(),
|
||||
default => false, // Any unknown async listener should not be enabled by default
|
||||
};
|
||||
}
|
||||
}
|
|
@ -41,8 +41,8 @@ class LocateVisit
|
|||
return;
|
||||
}
|
||||
|
||||
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit);
|
||||
$this->eventDispatcher->dispatch(new VisitLocated($visitId));
|
||||
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress, $visit);
|
||||
$this->eventDispatcher->dispatch(new VisitLocated($visitId, $shortUrlVisited->originalIpAddress));
|
||||
}
|
||||
|
||||
private function locateVisit(string $visitId, ?string $originalIpAddress, Visit $visit): void
|
||||
|
|
89
module/Core/src/EventDispatcher/Matomo/SendVisitToMatomo.php
Normal file
89
module/Core/src/EventDispatcher/Matomo/SendVisitToMatomo.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\EventDispatcher\Matomo;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\EventDispatcher\Event\VisitLocated;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoTrackerBuilderInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
|
||||
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
|
||||
use Throwable;
|
||||
|
||||
class SendVisitToMatomo
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly ShortUrlStringifier $shortUrlStringifier,
|
||||
private readonly MatomoOptions $matomoOptions,
|
||||
private readonly MatomoTrackerBuilderInterface $trackerBuilder,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(VisitLocated $visitLocated): void
|
||||
{
|
||||
if (! $this->matomoOptions->enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$visitId = $visitLocated->visitId;
|
||||
|
||||
/** @var Visit|null $visit */
|
||||
$visit = $this->em->find(Visit::class, $visitId);
|
||||
if ($visit === null) {
|
||||
$this->logger->warning('Tried to send visit with id "{visitId}" to matomo, but it does not exist.', [
|
||||
'visitId' => $visitId,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$tracker = $this->trackerBuilder->buildMatomoTracker();
|
||||
|
||||
$tracker
|
||||
->setUrl($this->resolveUrlToTrack($visit))
|
||||
->setCustomTrackingParameter('type', $visit->type()->value)
|
||||
->setUserAgent($visit->userAgent())
|
||||
->setUrlReferrer($visit->referer());
|
||||
|
||||
$location = $visit->getVisitLocation();
|
||||
if ($location !== null) {
|
||||
$tracker
|
||||
->setCity($location->getCityName())
|
||||
->setCountry($location->getCountryName())
|
||||
->setLatitude($location->getLatitude())
|
||||
->setLongitude($location->getLongitude());
|
||||
}
|
||||
|
||||
// Set not obfuscated IP if possible, as matomo handles obfuscation itself
|
||||
$ip = $visitLocated->originalIpAddress ?? $visit->getRemoteAddr();
|
||||
if ($ip !== null) {
|
||||
$tracker->setIp($ip);
|
||||
}
|
||||
|
||||
if ($visit->isOrphan()) {
|
||||
$tracker->setCustomTrackingParameter('orphan', 'true');
|
||||
}
|
||||
|
||||
// Send empty document title to avoid different actions to be created by matomo
|
||||
$tracker->doTrackPageView('');
|
||||
} catch (Throwable $e) {
|
||||
// Capture all exceptions to make sure this does not interfere with the regular execution
|
||||
$this->logger->error('An error occurred while trying to send visit to Matomo. {e}', ['e' => $e]);
|
||||
}
|
||||
}
|
||||
|
||||
public function resolveUrlToTrack(Visit $visit): string
|
||||
{
|
||||
$shortUrl = $visit->getShortUrl();
|
||||
if ($shortUrl === null) {
|
||||
return $visit->visitedUrl() ?? '';
|
||||
}
|
||||
|
||||
return $this->shortUrlStringifier->stringify($shortUrl);
|
||||
}
|
||||
}
|
|
@ -139,7 +139,7 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
|
|||
$importedVisits = 0;
|
||||
foreach ($iterable as $importedOrphanVisit) {
|
||||
// Skip visits which are older than the most recent already imported visit's date
|
||||
if ($mostRecentOrphanVisit?->getDate()->gte(normalizeDate($importedOrphanVisit->date))) {
|
||||
if ($mostRecentOrphanVisit?->getDate()->greaterThanOrEquals(normalizeDate($importedOrphanVisit->date))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ final class ShortUrlImporting
|
|||
$importedVisits = 0;
|
||||
foreach ($visits as $importedVisit) {
|
||||
// Skip visits which are older than the most recent already imported visit's date
|
||||
if ($mostRecentImportedDate?->gte(normalizeDate($importedVisit->date))) {
|
||||
if ($mostRecentImportedDate?->greaterThanOrEquals(normalizeDate($importedVisit->date))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
|
27
module/Core/src/Matomo/MatomoOptions.php
Normal file
27
module/Core/src/Matomo/MatomoOptions.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Matomo;
|
||||
|
||||
class MatomoOptions
|
||||
{
|
||||
public function __construct(
|
||||
public readonly bool $enabled = false,
|
||||
public readonly ?string $baseUrl = null,
|
||||
/** @var numeric-string|int|null */
|
||||
private readonly string|int|null $siteId = null,
|
||||
public readonly ?string $apiToken = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function siteId(): ?int
|
||||
{
|
||||
if ($this->siteId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We enforce site ID to be hydrated as a numeric string or int, so it's safe to cast to int here
|
||||
return (int) $this->siteId;
|
||||
}
|
||||
}
|
48
module/Core/src/Matomo/MatomoTrackerBuilder.php
Normal file
48
module/Core/src/Matomo/MatomoTrackerBuilder.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Matomo;
|
||||
|
||||
use MatomoTracker;
|
||||
use Shlinkio\Shlink\Core\Exception\RuntimeException;
|
||||
|
||||
class MatomoTrackerBuilder implements MatomoTrackerBuilderInterface
|
||||
{
|
||||
public const MATOMO_DEFAULT_TIMEOUT = 10; // Time in seconds
|
||||
|
||||
public function __construct(private readonly MatomoOptions $options)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws RuntimeException If there's any missing matomo parameter
|
||||
*/
|
||||
public function buildMatomoTracker(): MatomoTracker
|
||||
{
|
||||
$siteId = $this->options->siteId();
|
||||
if ($siteId === null || $this->options->baseUrl === null || $this->options->apiToken === null) {
|
||||
throw new RuntimeException(
|
||||
'Cannot create MatomoTracker. Either site ID, base URL or api token are not defined',
|
||||
);
|
||||
}
|
||||
|
||||
// Create a new MatomoTracker on every request, because it infers request info during construction
|
||||
$tracker = new MatomoTracker($siteId, $this->options->baseUrl);
|
||||
$tracker
|
||||
// Token required to set the IP and location
|
||||
->setTokenAuth($this->options->apiToken)
|
||||
// Ensure params are not sent in the URL, for security reasons
|
||||
->setRequestMethodNonBulk('POST')
|
||||
// Set a reasonable timeout
|
||||
->setRequestTimeout(self::MATOMO_DEFAULT_TIMEOUT)
|
||||
->setRequestConnectTimeout(self::MATOMO_DEFAULT_TIMEOUT);
|
||||
|
||||
// We don't want to bulk send, as every request to Shlink will create a new tracker
|
||||
$tracker->disableBulkTracking();
|
||||
// Disable cookies, as they are ignored anyway
|
||||
$tracker->disableCookieSupport();
|
||||
|
||||
return $tracker;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue