Compare commits

...

150 Commits

Author SHA1 Message Date
Alejandro Celaya
16f64f6247 Merge pull request #2064 from shlinkio/develop
Release 4.0.3
2024-03-15 23:07:25 +01:00
Alejandro Celaya
98992c656f Merge pull request #2063 from acelaya-forks/feature/non-http-url
Fix error when redirecting to a non-http URL
2024-03-15 23:05:02 +01:00
Alejandro Celaya
053e026982 Fix error when redirecting to a non-http URL 2024-03-15 23:00:40 +01:00
Alejandro Celaya
74180a4381 Merge pull request #2062 from acelaya-forks/feature/string-db-credentials
Feature/string db credentials
2024-03-15 18:33:29 +01:00
Alejandro Celaya
293725f933 Update changelog 2024-03-15 18:22:27 +01:00
Alejandro Celaya
c33f8d0ea2 Cast database credentials to string when read from the env 2024-03-15 14:32:53 +01:00
Alejandro Celaya
0f2cd3cb7f Remove useless return annotation 2024-03-12 09:09:59 +01:00
Alejandro Celaya
2441ac5e77 Update Bug template 2024-03-12 09:00:49 +01:00
Alejandro Celaya
f248001460 Merge pull request #2053 from shlinkio/develop
Release 4.0.2
2024-03-09 09:49:51 +01:00
Alejandro Celaya
1fe2c93946 Merge pull request #2051 from acelaya-forks/feature/fix-geolite-update
Fix infinite GeoLite2 downloads
2024-03-09 09:39:31 +01:00
Alejandro Celaya
a3d50605c1 Update changelog 2024-03-09 09:32:05 +01:00
Alejandro Celaya
5427152f15 Make sure GeoLite2 db file is always read from the filesystem befor etrying to operate on it 2024-03-09 09:30:05 +01:00
Alejandro Celaya
a4e9c2fdde Merge pull request #2046 from shlinkio/develop
Release 4.0.1
2024-03-08 08:57:40 +01:00
Alejandro Celaya
e244b2dc51 Add v4.0.1 to changelog 2024-03-08 08:56:55 +01:00
Alejandro Celaya
31dea8fa99 Merge pull request #2045 from acelaya-forks/feature/match-languages-fix
Ensure language redirect conditions do not match for too low quality accepted languages
2024-03-07 23:23:58 +01:00
Alejandro Celaya
be8cf56240 Ensure language redirect conditions do not match for too low quality accepted languages 2024-03-07 23:21:31 +01:00
Alejandro Celaya
0bc7412430 Fix incorrect redirect condition type definiition 2024-03-05 15:09:44 +01:00
Alejandro Celaya
6d56e92306 Merge pull request #2042 from acelaya-forks/feature/qr-code-color-args
Document color and bgColor QR code query params
2024-03-05 11:16:49 +01:00
Alejandro Celaya
97c94f8fcc Document color and bgColor QR code query params 2024-03-05 11:14:07 +01:00
Alejandro Celaya
92b5a5296d Merge pull request #2039 from shlinkio/develop
Release 4.0.0
2024-03-03 19:23:35 +01:00
Alejandro Celaya
febca6d441 Small reword in UPGRADE guide 2024-03-03 19:16:46 +01:00
Alejandro Celaya
bf29abc468 Merge pull request #2040 from acelaya-forks/feature/redirect-checks
Feature/redirect checks
2024-03-03 18:54:15 +01:00
Alejandro Celaya
97cb30565c Add v4.0.0 2024-03-03 18:50:23 +01:00
Alejandro Celaya
9809f050ef Update changelog 2024-03-03 18:19:40 +01:00
Alejandro Celaya
7ecfb24584 Merge pull request #2038 from acelaya-forks/feature/redirect-rules-cli
Add command to manage the redirect rules for a short URLs
2024-03-03 13:19:42 +01:00
Alejandro Celaya
4aa65f750e Add CLI test for manage redirects command, to cover validation errors 2024-03-03 13:16:37 +01:00
Alejandro Celaya
63c533fa62 Fix incorrect rule selection when deleting rules with same long URL 2024-03-03 12:59:58 +01:00
Alejandro Celaya
8751d6c315 Add unit test for RedirectRuleHandler 2024-03-03 12:51:17 +01:00
Alejandro Celaya
eb40dc2d5d Add unit test for ShortUrlRedirectRuleService::saveRulesForShortUrl 2024-03-03 10:36:17 +01:00
Alejandro Celaya
c9d1a955b9 Add ManageRedirectRulesCommand unit test 2024-03-03 10:27:21 +01:00
Alejandro Celaya
c346fd0602 Reduce duplicated code when parsing short codes and domains from CLI 2024-03-03 10:11:12 +01:00
Alejandro Celaya
a45550b0c6 Extract logic to determine list of rules from ManageRedirectRulesCommand to a helper service 2024-03-03 09:51:56 +01:00
Alejandro Celaya
a843c59d77 Fix inconsistencies when editing rules and saving a mix of new and old ones 2024-03-03 09:09:43 +01:00
Alejandro Celaya
3bfb29a51c Test new methods for RedirectCondition and ShortUrlRedirectRule 2024-03-03 08:47:31 +01:00
Alejandro Celaya
d8ede3263f Implement command to manage redirect rules for a short URL 2024-03-02 23:01:49 +01:00
Alejandro Celaya
c36e43e249 Merge pull request #2037 from acelaya-forks/feature/improve-rules-swagger
Define different swagger models for get and set redirect rules
2024-03-01 08:58:11 +01:00
Alejandro Celaya
52150b3067 Define different swagger models for get and set redirect rules 2024-03-01 08:56:03 +01:00
Alejandro Celaya
e7796cc917 Merge pull request #2033 from acelaya-forks/feature/redirect-rule-creation
Create endpoint to set redirect rules for a short URL
2024-02-29 20:23:42 +01:00
Alejandro Celaya
7f560e6a65 Add SetRedirectRulesAction API test 2024-02-29 20:20:04 +01:00
Alejandro Celaya
8f233221e5 Add SetRedirectRulesAction unit test 2024-02-29 20:14:15 +01:00
Alejandro Celaya
f700abd65d Add tests for ShortUrlRedirectionRuleService::setRulesForShortUrl 2024-02-29 19:55:34 +01:00
Alejandro Celaya
f9e4d6d617 Create RedirectRulesDataTest 2024-02-29 19:17:10 +01:00
Alejandro Celaya
d9286765e1 Create endpoint to set redirect rules for a short URL 2024-02-29 10:16:41 +01:00
Alejandro Celaya
a7cde9364a Merge pull request #2035 from acelaya-forks/feature/improve-rules-persistence
Remove name and uniqueness in redirect condition table
2024-02-29 09:26:51 +01:00
Alejandro Celaya
070d74830b Remove name and uniqueness in redirect condition table 2024-02-29 09:05:30 +01:00
Alejandro Celaya
23c07c4e82 Merge pull request #2031 from acelaya-forks/feature/redirect-rules-api
Create endpoint to list redirect rules for a specific short URL
2024-02-28 09:17:05 +01:00
Alejandro Celaya
ab7824aa85 Add unit test for ListRedirectRulesAction 2024-02-28 09:14:27 +01:00
Alejandro Celaya
67bafbe44e Add API test for redirect rules list 2024-02-28 08:55:44 +01:00
Alejandro Celaya
c4805b8152 Remove old error models and examples from swagger spec 2024-02-28 08:39:26 +01:00
Alejandro Celaya
33729289c7 Create endpoint to list redirect rules for a specific short URL 2024-02-27 21:00:53 +01:00
Alejandro Celaya
721e3d9ef9 Merge pull request #2030 from acelaya-forks/feature/device-redirect-rules
Feature/device redirect rules
2024-02-27 19:27:59 +01:00
Alejandro Celaya
a72e22e046 Unit-test RedirectCondition for devices 2024-02-27 19:24:26 +01:00
Alejandro Celaya
36749658da Remove device long URLs support 2024-02-27 18:46:49 +01:00
Alejandro Celaya
4ad3dc0bc7 Create new migration to drop old device_long_urls table 2024-02-27 09:09:03 +01:00
Alejandro Celaya
73864b923d Add migration to migrate device_long_urls to redirect_rules 2024-02-26 23:42:37 +01:00
Alejandro Celaya
71277e979a Merge pull request #2026 from acelaya-forks/feature/language-redirects
Feature/language redirects
2024-02-26 20:10:26 +01:00
Alejandro Celaya
60fef3de74 Merge lines 2024-02-26 20:06:15 +01:00
Alejandro Celaya
0fe503fa0e Update changelog 2024-02-26 19:59:34 +01:00
Alejandro Celaya
db02d9f1ba Finalize logic to dynamically match accept language rules 2024-02-26 19:58:46 +01:00
Alejandro Celaya
89a987d03a Merge pull request #2024 from acelaya-forks/feature/rule-based-redirects
Logic to resolve the long URL to redirect to for a short URL
2024-02-26 19:11:13 +01:00
Alejandro Celaya
3284cea6f2 Update changelog 2024-02-26 19:08:21 +01:00
Alejandro Celaya
df5ad554c1 Add E2E tests for dynamic rule-based redirects 2024-02-26 19:05:39 +01:00
Alejandro Celaya
07ae92943d Add test for ShortUrlRedirectResolver rule matching 2024-02-25 23:09:16 +01:00
Alejandro Celaya
175712d4a9 Add test for ShortUrlRedirectRule request matching 2024-02-25 19:38:54 +01:00
Alejandro Celaya
3f1b253c31 Add test for RedirectCondition request matching 2024-02-25 19:21:39 +01:00
Alejandro Celaya
202d0b86b3 Extract logic to match every type of redirect condition to its own private method 2024-02-25 17:13:54 +01:00
Alejandro Celaya
4e87affb0b Take redirect rules into consideration when resolving the long URL for a short URL 2024-02-25 12:34:27 +01:00
Alejandro Celaya
7f83d37b3c Add logic to match redirect conditions based on query params or language 2024-02-25 12:34:27 +01:00
Alejandro Celaya
09e81b00c5 Create component to resolve the long URL to redirect to for a short URL 2024-02-25 12:34:27 +01:00
Alejandro Celaya
68b77e22c5 Merge pull request #2025 from acelaya-forks/feature/old-migrations
Delete old migrations
2024-02-25 12:33:49 +01:00
Alejandro Celaya
c5ddd8302a Delete old migrations 2024-02-25 12:28:20 +01:00
Alejandro Celaya
1a0fe0429a Merge pull request #2022 from acelaya-forks/feature/redirect-rules-persistence
Create migration for new rules and conditions tables
2024-02-24 20:27:47 +01:00
Alejandro Celaya
6646232311 Set eager loading for redirect rule conditions 2024-02-24 20:24:41 +01:00
Alejandro Celaya
c1e88c3e83 Use named args to avoid passing default values for args 2024-02-24 18:21:53 +01:00
Alejandro Celaya
c91a534d1a Create new entities for redirect rules 2024-02-24 18:17:09 +01:00
Alejandro Celaya
752100f1ce Create migration for new rules and conditions tables 2024-02-24 17:30:46 +01:00
Alejandro Celaya
dae083c540 Merge pull request #2023 from acelaya-forks/feature/unused-env-vars
Remove unused entries from EnvVars enum
2024-02-24 17:29:46 +01:00
Alejandro Celaya
857c3a4f8d Remove unused entries from EnvVars enum 2024-02-24 17:26:12 +01:00
Alejandro Celaya
acc4c4756e Simplify and normalize API tests script 2024-02-23 19:30:58 +01:00
Alejandro Celaya
0bacb215c5 Merge pull request #2019 from acelaya-forks/feature/long-url-as-text
Change long URL columns to TEXT type
2024-02-22 09:39:46 +01:00
Alejandro Celaya
d1a6e60b01 Add migration to update long URLs columns to text type 2024-02-22 09:35:14 +01:00
Alejandro Celaya
8f954151ca Change long URL columns to TEXT type 2024-02-21 19:43:02 +01:00
Alejandro Celaya
145d4eaaed Merge pull request #2020 from acelaya-forks/feature/path-prefix
Feature/path prefix
2024-02-21 19:41:19 +01:00
Alejandro Celaya
7673232793 Add --path-prefix to short URL creation 2024-02-21 19:38:11 +01:00
Alejandro Celaya
f08951a9b9 Add unit test for short URL path prefix 2024-02-21 19:24:30 +01:00
Alejandro Celaya
ff963a9df4 Add API test for short URL path prefix 2024-02-21 19:14:30 +01:00
Alejandro Celaya
f30c74b987 Prepend path prefix to generated short code or custom slug 2024-02-21 18:06:06 +01:00
Alejandro Celaya
467dbdd183 Update to latest shlink-common 2024-02-21 17:57:45 +01:00
Alejandro Celaya
0e78deb8f2 Refactor ShortUrlInputFilter for creation and edition 2024-02-21 10:12:40 +01:00
Alejandro Celaya
50cc7ae632 Fix donate URL 2024-02-20 22:33:10 +01:00
Alejandro Celaya
512d765d60 Merge pull request #2018 from acelaya-forks/feature/remove-infection
Remove dependency on infection and mutation tests
2024-02-20 22:28:48 +01:00
Alejandro Celaya
7b9331bd14 Merge DB test jobs into one with a matrix 2024-02-20 22:26:04 +01:00
Alejandro Celaya
4f5ce9fb43 Remove dependency on infection and mutation tests 2024-02-20 22:20:16 +01:00
Alejandro Celaya
83f73eb631 Merge pull request #2017 from acelaya-forks/feature/improve-e2e-coverage
Simplify and improve how code coverage is generated in API and CLI tests
2024-02-20 22:03:17 +01:00
Alejandro Celaya
3f1b89d665 Install dependencies in upload-coverage CI job 2024-02-20 18:30:34 +01:00
Alejandro Celaya
8f6fc97fc8 Simplify and improve how code coverage is generated in API and CLI tests 2024-02-20 18:23:37 +01:00
Alejandro Celaya
a463e6f9d7 Merge pull request #2014 from acelaya-forks/feature/qr-code-improvements
Allow customizing color, background color and logo in QR codes
2024-02-19 23:22:57 +01:00
Alejandro Celaya
2a0364ca8f Update changelog 2024-02-19 23:13:09 +01:00
Alejandro Celaya
23e9ed93bb Add test for QrCodeAction with logo URL 2024-02-19 23:10:51 +01:00
Alejandro Celaya
689343d1c9 Test QR codes logic when providing a color 2024-02-18 21:02:35 +01:00
Alejandro Celaya
d01dc334d7 Update to endroid/qr-code 5 2024-02-18 19:58:19 +01:00
Alejandro Celaya
58a3791a5c Allow customizing color, background color and logo in QR codes 2024-02-18 14:22:25 +01:00
Alejandro Celaya
1a133af141 Merge pull request #2013 from acelaya-forks/feature/title-resolution-timeout
Add a 5-second timeout to title resolution
2024-02-18 14:21:46 +01:00
Alejandro Celaya
938fb6509e Add API test to verify titles for timing-out long URLs 2024-02-18 12:45:59 +01:00
Alejandro Celaya
d3bfd99210 Add a 3-second timeout to title resolution 2024-02-18 11:32:31 +01:00
Alejandro Celaya
3a1740fdca Merge pull request #2012 from acelaya-forks/feature/remove-validate-url
Feature/remove validate url
2024-02-17 12:13:05 +01:00
Alejandro Celaya
e3de403c6c Remove support to validate long URLs during short URL creation/edition 2024-02-17 12:02:57 +01:00
Alejandro Celaya
5c1ab02753 Update shlink dependencies 2024-02-17 10:59:06 +01:00
Alejandro Celaya
e5713df008 Merge pull request #2011 from acelaya-forks/feature/update-symfony
Update to Symfony 7
2024-02-17 10:52:41 +01:00
Alejandro Celaya
95ea64980b Update to Symfony 7 2024-02-17 10:46:29 +01:00
Alejandro Celaya
c0a77b790d Merge pull request #2009 from acelaya-forks/feature/doctrine-orm-3
Update to doctrine ORM 3.0
2024-02-17 10:36:29 +01:00
Alejandro Celaya
e073b4331a Update to doctrine ORM 3.0 2024-02-17 10:23:37 +01:00
Alejandro Celaya
e919901487 Merge pull request #2007 from acelaya-forks/feature/coverage-improvements
Feature/coverage improvements
2024-02-17 08:46:18 +01:00
Alejandro Celaya
13f9f106be Update to event-dispatcher without swoole 2024-02-17 08:41:53 +01:00
Alejandro Celaya
e9c7053ef5 Move code around generating code coverage to test-utils lib 2024-02-17 08:41:17 +01:00
Alejandro Celaya
62051c8809 Merge pull request #2006 from acelaya-forks/feature/fix-ci
Recover prev approach to generate API tests coverage
2024-02-16 23:46:06 +01:00
Alejandro Celaya
0a6a794e23 Recover prev approach to generate API tests coverage 2024-02-16 23:32:20 +01:00
Alejandro Celaya
01846657d1 Merge pull request #2003 from acelaya-forks/feature/improve-coverage-collection
Move E2E coverage collectors to shlink-test-utils
2024-02-16 23:27:44 +01:00
Alejandro Celaya
dd7545afdf Move E2E coverage collectors to shlink-test-utils 2024-02-16 23:25:42 +01:00
Alejandro Celaya
9296013596 Merge pull request #2005 from acelaya-forks/feature/remove-swoole
Drop support for openswoole
2024-02-16 23:18:39 +01:00
Alejandro Celaya
8015c6cc88 Skip extensions cache if there are no extensions to install 2024-02-16 23:11:38 +01:00
Alejandro Celaya
8c93444286 Do not set default value for php-extensions input in ci-setup action 2024-02-16 23:08:03 +01:00
Alejandro Celaya
96ed7cae0d Drop support for openswoole 2024-02-16 23:02:46 +01:00
Alejandro Celaya
72c4628b79 Merge pull request #2002 from acelaya-forks/feature/rr-e2e-tests
Feature/rr e2e tests
2024-02-16 07:57:04 +01:00
Alejandro Celaya
1117631717 Move rr tests config back to the config/roadrunner folder 2024-02-16 00:01:35 +01:00
Alejandro Celaya
60176060cb Update changelog 2024-02-15 23:42:03 +01:00
Alejandro Celaya
d949b54ef4 Switch to roadrunner as the default runtime for API tests 2024-02-15 23:39:50 +01:00
Alejandro Celaya
720db64a03 Switch to RoadRunner as default API test runtime 2024-02-14 09:31:26 +01:00
Alejandro Celaya
37e0978bfc Merge pull request #1999 from acelaya-forks/feature/remove-deprecated-stuff
Removed deprecated features
2024-02-13 22:52:00 +01:00
Alejandro Celaya
cf355b0b69 Update shlink-common and shlink-installer 2024-02-13 22:46:25 +01:00
Alejandro Celaya
f2edb54b8b Document changes from v3 to v4 2024-02-13 09:30:37 +01:00
Alejandro Celaya
13ec27039d Ensure non-root user in Dockerfile 2024-02-13 08:55:22 +01:00
Alejandro Celaya
ad3805a560 Removed deprecated features 2024-02-12 23:18:30 +01:00
Alejandro Celaya
cc4afa7b62 Merge pull request #1998 from acelaya-forks/feature/default-php-8.3
Feature/default php 8.3
2024-02-11 11:56:20 +01:00
Alejandro Celaya
7a6bfed445 Update changelog 2024-02-11 11:23:34 +01:00
Alejandro Celaya
f2a7b687a9 Update docker images to PHP 8.3 2024-02-11 11:22:48 +01:00
Alejandro Celaya
522d021264 Merge pull request #1995 from acelaya-forks/feature/orphan-visits-filter
Support filtering orphan visits by type in VisitRepository
2024-02-10 18:37:17 +01:00
Alejandro Celaya
14a0db1f34 Update changelog 2024-02-10 18:28:54 +01:00
Alejandro Celaya
430883987a Add API test for type-filtering in orphan visits list 2024-02-10 18:26:19 +01:00
Alejandro Celaya
f17b641d46 Allow filtering orphan visits by type from the CLI 2024-02-10 18:19:28 +01:00
Alejandro Celaya
48a8290e92 Allow type filter property for orphan visits list 2024-02-10 17:51:42 +01:00
Alejandro Celaya
46acf4de1c Support filtering orphan visits by type in VisitRepository 2024-02-10 13:58:03 +01:00
Alejandro Celaya
17792a1603 Merge pull request #1994 from acelaya-forks/feature/docker-compose-secrets
Support loading env vars from secret files
2024-02-10 10:12:02 +01:00
Alejandro Celaya
a8611f5d80 Support loading env vars from secret files 2024-02-10 09:54:59 +01:00
Alejandro Celaya
deef938e97 Merge pull request #1991 from acelaya-forks/feature/remove-league-uri
Feature/remove league uri
2024-02-05 23:12:17 +01:00
Alejandro Celaya
e014cfa72a Remove dependency on league/uri 2024-02-05 23:05:31 +01:00
Alejandro Celaya
aa242eba25 Remove League\Uri from ShortUrlRedirectionBuilder 2024-02-05 22:13:59 +01:00
Alejandro Celaya
0ac5569d60 Merge pull request #1989 from acelaya-forks/feature/request-id-lib
Feature/request id lib
2024-02-04 14:38:49 +01:00
Alejandro Celaya
7c3e3442c2 Update changelog 2024-02-04 14:18:16 +01:00
Alejandro Celaya
0f894dcdfe Replace request-id middleware dependency with userland code 2024-02-04 14:16:42 +01:00
311 changed files with 4877 additions and 5466 deletions

View File

@@ -19,7 +19,6 @@ indocker
docker-*
phpstan.neon
php*xml*
infection*
**/test*
build*
**/.*

View File

@@ -20,10 +20,8 @@ body:
options:
- Self-hosted Apache
- Self-hosted nginx
- Self-hosted openswoole
- Self-hosted RoadRunner
- Openswoole Docker image
- RoadRunner Docker image
- Docker image
- Other (explain in summary)
- type: dropdown
validations:

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,2 @@
github: ['acelaya']
custom: ['https://acel.me/donate']
custom: ['https://slnk.to/donate']

View File

@@ -22,10 +22,8 @@ body:
options:
- Self-hosted Apache
- Self-hosted nginx
- Self-hosted openswoole
- Self-hosted RoadRunner
- Openswoole Docker image
- RoadRunner Docker image
- Docker image
- Other (explain in summary)
- type: dropdown
validations:
@@ -60,5 +58,10 @@ body:
validations:
required: true
attributes:
label: How to reproduce
value: '<!-- Provide steps to reproduce the bug. -->'
label: Minimum steps to reproduce
value: |
<!--
Emphasis in MINIMUM: What is the simplest way to reproduce the bug?
Avoid things like "Create a kubernetes cluster", or anything related with cloud providers, as that is rarely the root cause and the bug may be closed as "not reproducible".
If you can provide a simple docker compose config, that's even better.
-->

View File

@@ -12,7 +12,6 @@ inputs:
php-extensions:
description: 'The PHP extensions to install'
required: false
default: ''
extensions-cache-key:
description: 'The key used to cache PHP extensions. If empty value is provided, extension caching is disabled'
required: true
@@ -21,6 +20,7 @@ runs:
using: composite
steps:
- name: Setup cache environment
if: ${{ inputs.php-extensions }}
id: extcache
uses: shivammathur/cache-extensions@v1
with:
@@ -28,7 +28,8 @@ runs:
extensions: ${{ inputs.php-extensions }}
key: ${{ inputs.extensions-cache-key }}
- name: Cache extensions
uses: actions/cache@v3
if: ${{ inputs.php-extensions }}
uses: actions/cache@v4
with:
path: ${{ steps.extcache.outputs.dir }}
key: ${{ steps.extcache.outputs.key }}
@@ -43,5 +44,5 @@ runs:
ini-values: pcov.directory=module
- name: Install dependencies
if: ${{ inputs.install-deps == 'yes' }}
run: composer install --no-interaction --prefer-dist ${{ inputs.php-version == '8.3' && '--ignore-platform-reqs' || '' }}
run: composer install --no-interaction --prefer-dist
shell: bash

View File

@@ -14,7 +14,6 @@ jobs:
strategy:
matrix:
php-version: ['8.2', '8.3']
continue-on-error: ${{ matrix.php-version == '8.3' }}
env:
LC_ALL: C
steps:
@@ -28,7 +27,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-22.1.0, pdo_sqlsrv-5.11.1
php-extensions: pdo_sqlsrv-5.12.0
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
- name: Create test database
if: ${{ inputs.platform == 'ms' }}

View File

@@ -1,46 +0,0 @@
name: Mutation tests
on:
workflow_call:
inputs:
test-group:
type: string
required: true
description: One of unit, db, api or cli
jobs:
mutation-tests:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.2', '8.3']
continue-on-error: ${{ matrix.php-version == '8.3' }}
steps:
- uses: actions/checkout@v4
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-22.1.0
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
- uses: actions/download-artifact@v4
with:
name: coverage-${{ inputs.test-group }}
path: build
- name: Resolve infection args
id: infection_args
run: echo "args=--logger-github=false" >> $GITHUB_OUTPUT
# TODO Try to filter mutation tests to improve execution times. Investigate why --git-diff-lines --git-diff-base=develop does not work
# run: |
# BRANCH="${GITHUB_REF#refs/heads/}" |
# if [[ $BRANCH == 'main' || $BRANCH == 'develop' ]]; then
# echo "args=--logger-github=false" >> $GITHUB_OUTPUT
# else
# echo "args=--logger-github=false --git-diff-lines --git-diff-base=develop" >> $GITHUB_OUTPUT
# fi;
shell: bash
- if: ${{ inputs.test-group == 'unit' }}
run: composer infect:ci:unit -- ${{ steps.infection_args.outputs.args }}
env:
INFECTION_BADGE_API_KEY: ${{ secrets.INFECTION_BADGE_API_KEY }}
- if: ${{ inputs.test-group != 'unit' }}
run: composer infect:ci:${{ inputs.test-group }} -- ${{ steps.infection_args.outputs.args }}

View File

@@ -14,7 +14,8 @@ jobs:
strategy:
matrix:
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@v4
- name: Start postgres database server
@@ -26,8 +27,10 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-22.1.0
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
- name: Download RoadRunner binary
if: ${{ inputs.test-group == 'api' }}
run: ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr
- run: composer test:${{ inputs.test-group }}:ci
- uses: actions/upload-artifact@v4
if: ${{ matrix.php-version == '8.2' }}

View File

@@ -8,7 +8,6 @@ on:
- '*.md'
- '*.xml'
- '*.yml*'
- '*.json5'
- '*.neon'
push:
branches:
@@ -21,7 +20,6 @@ on:
- '*.md'
- '*.xml'
- '*.yml*'
- '*.json5'
- '*.neon'
jobs:
@@ -36,7 +34,6 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-22.1.0
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
- run: composer ${{ matrix.command }}
@@ -50,89 +47,25 @@ jobs:
with:
test-group: cli
openswoole-api-tests:
api-tests:
uses: './.github/workflows/ci-tests.yml'
with:
test-group: api
roadrunner-api-tests:
runs-on: ubuntu-22.04
db-tests:
strategy:
matrix:
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@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 ${{ 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
sqlite-db-tests:
platform: ['sqlite:ci', 'mysql', 'maria', 'postgres', 'ms']
uses: './.github/workflows/ci-db-tests.yml'
with:
platform: 'sqlite:ci'
mysql-db-tests:
uses: './.github/workflows/ci-db-tests.yml'
with:
platform: 'mysql'
maria-db-tests:
uses: './.github/workflows/ci-db-tests.yml'
with:
platform: 'maria'
postgres-db-tests:
uses: './.github/workflows/ci-db-tests.yml'
with:
platform: 'postgres'
ms-db-tests:
uses: './.github/workflows/ci-db-tests.yml'
with:
platform: 'ms'
unit-mutation-tests:
needs:
- unit-tests
uses: './.github/workflows/ci-mutation-tests.yml'
with:
test-group: unit
db-mutation-tests:
needs:
- sqlite-db-tests
uses: './.github/workflows/ci-mutation-tests.yml'
with:
test-group: db
api-mutation-tests:
needs:
- openswoole-api-tests
uses: './.github/workflows/ci-mutation-tests.yml'
with:
test-group: api
cli-mutation-tests:
needs:
- cli-tests
uses: './.github/workflows/ci-mutation-tests.yml'
with:
test-group: cli
platform: ${{ matrix.platform }}
upload-coverage:
needs:
- unit-tests
- openswoole-api-tests
- api-tests
- cli-tests
- sqlite-db-tests
- db-tests
runs-on: ubuntu-22.04
strategy:
matrix:
@@ -141,11 +74,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Use PHP
uses: shivammathur/setup-php@v2
uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
coverage: pcov
ini-values: pcov.directory=module
extensions-cache-key: tests-extensions-${{ matrix.php-version }}
- uses: actions/download-artifact@v4
with:
path: build
@@ -153,19 +85,14 @@ jobs:
- run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
- run: mv build/coverage-cli/coverage-cli.cov build/coverage-cli.cov
- run: wget https://phar.phpunit.de/phpcov-9.0.0.phar
- run: php phpcov-9.0.0.phar merge build --clover build/clover.xml
- run: vendor/bin/phpcov merge build --clover build/clover.xml
- name: Publish coverage
uses: codecov/codecov-action@v1
uses: codecov/codecov-action@v4
with:
file: ./build/clover.xml
delete-artifacts:
needs:
- unit-mutation-tests
- db-mutation-tests
- api-mutation-tests
- cli-mutation-tests
- upload-coverage
runs-on: ubuntu-22.04
steps:

View File

@@ -15,13 +15,6 @@ jobs:
- runtime: 'rr'
tag-suffix: 'roadrunner'
platforms: 'linux/arm64/v8,linux/amd64'
- runtime: 'openswoole'
tag-suffix: 'openswoole'
platforms: 'linux/arm/v7,linux/arm64/v8,linux/amd64'
- runtime: 'rr'
tag-suffix: 'non-root'
platforms: 'linux/arm64/v8,linux/amd64'
user-id: '1001'
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
secrets: inherit
with:
@@ -31,4 +24,3 @@ jobs:
tags-suffix: ${{ matrix.tag-suffix }}
extra-build-args: |
SHLINK_RUNTIME=${{ matrix.runtime }}
SHLINK_USER_ID=${{ matrix.user-id && matrix.user-id || 'root' }}

View File

@@ -11,22 +11,17 @@ jobs:
strategy:
matrix:
php-version: ['8.2', '8.3']
swoole: ['yes', 'no']
steps:
- uses: actions/checkout@v4
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-22.1.0
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
install-deps: 'no'
- if: ${{ matrix.swoole == 'yes' }}
run: ./build.sh ${GITHUB_REF#refs/tags/v}
- if: ${{ matrix.swoole == 'no' }}
run: ./build.sh ${GITHUB_REF#refs/tags/v} --no-swoole
- run: ./build.sh ${GITHUB_REF#refs/tags/v}
- uses: actions/upload-artifact@v4
with:
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
name: dist-files-${{ matrix.php-version }}
path: build
publish:

View File

@@ -20,7 +20,6 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
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 }}

3
.gitignore vendored
View File

@@ -1,6 +1,6 @@
.idea
bin/rr
config/roadrunner/.pid
.pid
build
!docker/build
composer.lock
@@ -15,3 +15,4 @@ docs/mercure.html
docker-compose.override.yml
.phpunit.result.cache
docs/swagger/swagger-inlined.json
phpcov*

View File

@@ -4,13 +4,111 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
## [4.0.3] - 2024-03-15
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#2058](https://github.com/shlinkio/shlink/issues/2058) Fix DB credentials provided as env vars being casted to `int` if they include only numbers.
* [#2060](https://github.com/shlinkio/shlink/issues/2060) Fix error when trying to redirect to a non-http long URL.
## [4.0.2] - 2024-03-09
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#2021](https://github.com/shlinkio/shlink/issues/2021) Fix infinite GeoLite2 downloads.
## [4.0.1] - 2024-03-08
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#2041](https://github.com/shlinkio/shlink/issues/2041) Document missing `color` and `bgColor` params for the QR code route in the OAS docs.
* [#2043](https://github.com/shlinkio/shlink/issues/2043) Fix language redirect conditions matching too low quality accepted languages.
## [4.0.0] - 2024-03-03
### Added
* [#1914](https://github.com/shlinkio/shlink/issues/1914) Add new dynamic redirects engine based on rules. Rules are conditions checked against the visitor's request, and when matching, they can result in a redirect to a different long URL.
Rules can be based on things like the presence of specific params, headers, locations, etc. This version ships with three initial rule condition types: device, query param and language.
* [#1902](https://github.com/shlinkio/shlink/issues/1902) Add dynamic redirects based on query parameters.
This is implemented on top of the new [rule-based redirects](https://github.com/shlinkio/shlink/discussions/1912).
* [#1915](https://github.com/shlinkio/shlink/issues/1915) Add dynamic redirects based on accept language.
This is implemented on top of the new [rule-based redirects](https://github.com/shlinkio/shlink/discussions/1912).
* [#1868](https://github.com/shlinkio/shlink/issues/1868) Add support for [docker compose secrets](https://docs.docker.com/compose/use-secrets/) to the docker image.
* [#1979](https://github.com/shlinkio/shlink/issues/1979) Allow orphan visits lists to be filtered by type.
This is supported both by the `GET /visits/orphan` API endpoint via `type=...` query param, and by the `visit:orphan` CLI command, via `--type` flag.
* [#1904](https://github.com/shlinkio/shlink/issues/1904) Allow to customize QR codes foreground color, background color and logo.
* [#1884](https://github.com/shlinkio/shlink/issues/1884) Allow a path prefix to be provided during short URL creation.
This can be useful to let Shlink generate partially random URLs, but with a known prefix.
Path prefixes are validated and filtered taking multi-segment slugs into consideration, which means slashes are replaced with dashes as long as multi-segment slugs are disabled.
### Changed
* [#1935](https://github.com/shlinkio/shlink/issues/1935) Replace dependency on abandoned `php-middleware/request-id` with userland simple middleware.
* [#1988](https://github.com/shlinkio/shlink/issues/1988) Remove dependency on `league\uri` package.
* [#1909](https://github.com/shlinkio/shlink/issues/1909) Update docker image to PHP 8.3.
* [#1786](https://github.com/shlinkio/shlink/issues/1786) Run API tests with RoadRunner by default.
* [#2008](https://github.com/shlinkio/shlink/issues/2008) Update to Doctrine ORM 3.0.
* [#2010](https://github.com/shlinkio/shlink/issues/2010) Update to Symfony 7.0 components.
* [#2016](https://github.com/shlinkio/shlink/issues/2016) Simplify and improve how code coverage is generated in API and CLI tests.
* [#1674](https://github.com/shlinkio/shlink/issues/1674) Database columns persisting long URLs have now `TEXT` type, which allows for much longer values.
### Deprecated
* *Nothing*
### Removed
* [#1908](https://github.com/shlinkio/shlink/issues/1908) Remove support for openswoole (and swoole).
### Fixed
* [#2000](https://github.com/shlinkio/shlink/issues/2000) Fix short URL creation/edition getting stuck when trying to resolve the title of a long URL which never returns a response.
## [3.7.3] - 2024-01-04
### Added
* *Nothing*
### Changed
* [#1968](https://github.com/shlinkio/shlink/issues/1968) Move migrations from `data` to `module/Core`.
* *Nothing*
### Deprecated
* *Nothing*

View File

@@ -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 `8800` through RoadRunner, `8080` through openswoole and `8000` through nginx+php-fpm.
Once you finish this, you will have the project exposed in ports `8800` through RoadRunner and `8000` through nginx+php-fpm.
> Note: The `indocker` shell script is a helper tool used to run commands inside the main docker container.
@@ -80,7 +80,7 @@ The purposes of every folder are:
* `data`: Common git-ignored assets, like logs, caches, lock files, GeoLite DB files, etc. It's the only location where Shlink may need to write at runtime.
* `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 RoadRunner or 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.
## Project tests
@@ -96,7 +96,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 RoadRunner or 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, 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.
@@ -124,7 +124,6 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed,
* Run `./indocker composer test:api` to run API E2E tests. For these, the Postgres database engine is used.
* Run `./indocker composer test:cli` to run CLI E2E tests. For these, the Maria DB database engine is used.
* 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

View File

@@ -1,14 +1,12 @@
FROM php:8.2-alpine3.17 as base
FROM php:8.3-alpine3.19 as base
ARG SHLINK_VERSION=latest
ENV SHLINK_VERSION ${SHLINK_VERSION}
ARG SHLINK_RUNTIME=rr
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
ARG SHLINK_USER_ID='root'
ENV SHLINK_USER_ID ${SHLINK_USER_ID}
ENV OPENSWOOLE_VERSION 22.1.0
ENV PDO_SQLSRV_VERSION 5.11.1
ENV USER_ID '1001'
ENV PDO_SQLSRV_VERSION 5.12.0
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
ENV LC_ALL 'C'
@@ -26,13 +24,8 @@ RUN \
apk del .dev-deps && \
apk add --no-cache postgresql icu libzip libpng
# Install openswoole and sqlsrv driver for x86_64 builds
# Install 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; \
if [ $(uname -m) == "x86_64" ]; then \
wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
@@ -47,14 +40,7 @@ FROM base as builder
COPY . .
COPY --from=composer:2 /usr/bin/composer ./composer.phar
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 ; \
fi; \
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction && \
php composer.phar clear-cache && \
rm -r docker composer.* && \
sed -i "s/%SHLINK_VERSION%/${SHLINK_VERSION}/g" config/autoload/app_options.global.php
@@ -64,7 +50,7 @@ RUN apk add --no-cache git && \
FROM base
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
COPY --from=builder --chown=${SHLINK_USER_ID} /etc/shlink .
COPY --from=builder --chown=${USER_ID} /etc/shlink .
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink && \
if [ "$SHLINK_RUNTIME" == 'rr' ]; then \
php ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; \
@@ -78,6 +64,6 @@ COPY docker/docker-entrypoint.sh docker-entrypoint.sh
COPY docker/config/shlink_in_docker.local.php config/autoload/shlink_in_docker.local.php
COPY docker/config/php.ini ${PHP_INI_DIR}/conf.d/
USER ${SHLINK_USER_ID}
USER ${USER_ID}
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]

View File

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

View File

@@ -2,12 +2,13 @@
[![Build Status](https://img.shields.io/github/actions/workflow/status/shlinkio/shlink/ci.yml?branch=develop&logo=github&style=flat-square)](https://github.com/shlinkio/shlink/actions/workflows/ci.yml?query=workflow%3A%22Continuous+integration%22)
[![Code Coverage](https://img.shields.io/codecov/c/gh/shlinkio/shlink/develop?style=flat-square)](https://app.codecov.io/gh/shlinkio/shlink)
[![Infection MSI](https://img.shields.io/endpoint?style=flat-square&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fshlinkio%2Fshlink%2Fdevelop)](https://dashboard.stryker-mutator.io/reports/github.com/shlinkio/shlink/develop)
[![Latest Stable Version](https://img.shields.io/github/release/shlinkio/shlink.svg?style=flat-square)](https://packagist.org/packages/shlinkio/shlink)
[![Docker pulls](https://img.shields.io/docker/pulls/shlinkio/shlink.svg?logo=docker&style=flat-square)](https://hub.docker.com/r/shlinkio/shlink/)
[![License](https://img.shields.io/github/license/shlinkio/shlink.svg?style=flat-square)](https://github.com/shlinkio/shlink/blob/main/LICENSE)
[![Twitter](https://img.shields.io/badge/follow-shlinkio-blue.svg?style=flat-square&logo=x&color=black)](https://twitter.com/shlinkio)
[![Mastodon](https://img.shields.io/mastodon/follow/109329425426175098?color=%236364ff&domain=https%3A%2F%2Ffosstodon.org&label=follow&logo=mastodon&logoColor=white&style=flat-square)](https://fosstodon.org/@shlinkio)
[![Bluesky](https://img.shields.io/badge/follow-shlinkio-0285FF.svg?style=flat-square&logo=bluesky&logoColor=white)](https://bsky.app/profile/shlinkio.bsky.social)
[![Twitter](https://img.shields.io/badge/follow-shlinkio-blue.svg?style=flat-square&logo=x&color=black)](https://twitter.com/shlinkio)
[![Paypal donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=aaaaaa)](https://slnk.to/donate)
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own domain.
@@ -38,12 +39,11 @@ First, make sure the host where you are going to run shlink fulfills these requi
* 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.
* apcu extension is recommended if you don't plan to use RoadRunner.
* xml extension is required if you want to generate QR codes in svg format.
* sockets and bcmath extensions are required if you want to integrate with a RabbitMQ instance.
* MySQL, MariaDB, PostgreSQL, MicrosoftSQL or SQLite.
* You will also need the corresponding pdo variation for the database you are planning to use: `pdo_mysql`, `pdo_pgsql`, `pdo_sqlsrv` or `pdo_sqlite`.
* The [openswoole](https://openswoole.com/) PHP extension (if you plan to serve Shlink with openswoole) or the web server of your choice with PHP integration (like Apache or Nginx).
### Download
@@ -53,7 +53,7 @@ In order to run Shlink, you will need a built version of the project. There are
The easiest way to install shlink is by using one of the pre-bundled distributable packages.
Go to the [latest version](https://github.com/shlinkio/shlink/releases/latest) and download the `shlink*_dist.zip` file that suits your needs. You will find one for every supported PHP version and with/without openswoole integration.
Go to the [latest version](https://github.com/shlinkio/shlink/releases/latest) and download the `shlink*_dist.zip` file that suits your needs. You will find one for every supported PHP version.
Finally, decompress the file in the location of your choice.

View File

@@ -1,5 +1,55 @@
# Upgrading
## From v3.x to v4.x
### General
* Swoole and Openswoole are no longer officially supported runtimes. The recommended alternative is RoadRunner.
* Dist files for swoole/openswoole are no longer published.
* Webhooks are no longer supported. Migrate to one of the other [real-time updates](https://shlink.io/documentation/advanced/real-time-updates/) mechanisms.
* When using RoadRunner, the amount of web workers, task workers and the port number can no longer be provided via config options. Use `WEB_WORKER_NUM`, `TASK_WORKER_NUM` and `PORT` env vars instead.
### Changes in URL shortener
* The short URLs `loosely` mode is no longer supported, as it was a typo. Use `loose` mode instead.
* QR codes URLs now work by default, even for short URLs that cannot be visited due to max visits or date range limitations.
If you want to keep previous behavior, pass `QR_CODE_FOR_DISABLED_SHORT_URLS=false` or the equivalent configuration option.
* Long URL title resolution is now enabled by default. You can still disable it by passing `AUTO_RESOLVE_TITLES=false` or the equivalent configuration option.
* Shlink no longer allows to opt-in for long URL verification. Long URLs are unconditionally considered correct during short URL creation/edition.
* Device long URLs have been migrated to the new Dynamic rule-based redirects system and will continue to work as expected, but the API surface has changed.
If you use shlink-web-client and rely on this feature when creating/updating short URLs, **DO NOT UPDATE YET**. Support for dynamic rule-based redirects will be added to shlink-web-client soon, in v4.1.0
### Changes in REST API
* REST API v1/v2 now behave like v3. This only affects error codes, which are now proper URIs.
* `INVALID_ARGUMENT` -> `https://shlink.io/api/error/invalid-data`
* `INVALID_SHORT_URL_DELETION` -> `https://shlink.io/api/error/invalid-short-url-deletion`
* `DOMAIN_NOT_FOUND` -> `https://shlink.io/api/error/domain-not-found`
* `FORBIDDEN_OPERATION` -> `https://shlink.io/api/error/forbidden-tag-operation`
* `INVALID_SLUG` -> `https://shlink.io/api/error/non-unique-slug`
* `INVALID_SHORTCODE` -> `https://shlink.io/api/error/short-url-not-found`
* `TAG_CONFLICT` -> `https://shlink.io/api/error/tag-conflict`
* `TAG_NOT_FOUND` -> `https://shlink.io/api/error/tag-not-found`
* `MERCURE_NOT_CONFIGURED` -> `https://shlink.io/api/error/mercure-not-configured`
* `INVALID_AUTHORIZATION` -> `https://shlink.io/api/error/missing-authentication`
* `INVALID_API_KEY` -> `https://shlink.io/api/error/invalid-api-key`
* Endpoints previously returning props like `"visitsCount": {number}` no longer do it. There should be an alternative `"visitsSummary": {}` object with the amount nested on it.
* It is no longer possible to order the short URLs list with `orderBy=visitsCount-ASC`/`orderBy=visitsCount-DESC`. Use `orderBy=visits-ASC`/`orderBy=visits-DESC` instead.
* It is no longer possible to get tags with stats using `GET /tags?withStats=true`. Use `GET /tags/stats` endpoint instead.
* The `deviceLongUrls` are ignored when calling `POST /short-urls` or `PATCH /short-urls/{shortCode}`. These should now be configured as dynamic rule-based redirects via `POST /short-urls/{shortCode}/redirect-rules`.
### Changes in Docker image
* Since openswoole is no longer supported, there are no longer image tags suffixed with `openswoole`. You should migrate to the default or `roadrunner` ones.
* The `non-root` docker tag is no longer published, as all docker images are now running without super-user permissions.
* Due to previous point, it is no longer possible to pass `ENABLE_PERIODIC_VISIT_LOCATE=true` in order to configure a cron job that locates visits periodically.
This was not really needed in the docker image, as visits are located on the fly.
### Changes in integrations
* Credentials in redis URLs should now be URL-encoded, as they are unconditionally url-decoded before being used. Previously, it was possible to customize this behavior via `REDIS_DECODE_CREDENTIALS=true|false`.
* Providing redis URIs in the form of `tcp://password@6.6.6.6:6379` is no longer supported. If you want to provide password with no username, do `tcp://:password@6.6.6.6:6379` instead.
## From v2.x to v3.x
### Changes in REST API

View File

@@ -2,7 +2,7 @@
export APP_ENV=test
export TEST_ENV=api
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}" # Openswoole is deprecated. Remove in v4.0.0
export TEST_RUNTIME="${TEST_RUNTIME:-"rr"}" # rr is the only runtime currently supported
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"
@@ -13,26 +13,19 @@ mkdir data/log/api-tests
touch $OUTPUT_LOGS
# Try to stop server just in case it hanged in last execution
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -f
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -f -w .
echo 'Starting server...'
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:start -d
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr serve -p -c=config/roadrunner/.rr.dev.yml \
-o=http.address=0.0.0.0:9999 \
-o=logs.encoding=json \
-o=logs.channels.http.encoding=json \
-o=logs.channels.server.encoding=json \
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr serve -p -w . -c=config/roadrunner/.rr.test.yml \
-o=logs.output="${PWD}/${OUTPUT_LOGS}" \
-o=logs.channels.http.output="${PWD}/${OUTPUT_LOGS}" \
-o=logs.channels.server.output="${PWD}/${OUTPUT_LOGS}" &
sleep 2 # Let's give the server a couple of seconds to start
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $*
testsExitCode=$?
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always $*
TESTS_EXIT_CODE=$?
[ "$TEST_RUNTIME" = 'openswoole' ] && vendor/bin/laminas mezzio:swoole:stop
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -c config/roadrunner/.rr.dev.yml -o=http.address=0.0.0.0:9999
[ "$TEST_RUNTIME" = 'rr' ] && bin/rr stop -w .
# Exit this script with the same code as the tests. If tests failed, this script has to fail
exit $testsExitCode
exit $TESTS_EXIT_CODE

View File

@@ -1,18 +1,15 @@
#!/usr/bin/env bash
set -e
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ] || ([ "$#" == 2 ] && [ "$2" != "--no-swoole" ]); then
if [ "$#" -lt 1 ]; then
echo "Usage:" >&2
echo " $0 {version} [--no-swoole]" >&2
echo " $0 {version}" >&2
exit 1
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"
distId="shlink${version}_php${phpVersion}_dist"
builtContent="./build/${distId}"
projectdir=$(pwd)
[[ -f ./composer.phar ]] && composerBin='./composer.phar' || composerBin='composer'
@@ -31,19 +28,8 @@ cd "${builtContent}"
# Install dependencies
echo "Installing dependencies with $composerBin..."
# 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
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
${composerBin} install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction
# Delete development files
echo 'Deleting dev files...'

View File

@@ -19,13 +19,13 @@
"ext-pdo": "*",
"akrabat/ip-address-middleware": "^2.1",
"cakephp/chronos": "^3.0.2",
"doctrine/dbal": "^4.0",
"doctrine/migrations": "^3.6",
"doctrine/orm": "^2.16",
"endroid/qr-code": "^4.8",
"doctrine/orm": "^3.0",
"endroid/qr-code": "^5.0",
"friendsofphp/proxy-manager-lts": "^1.0",
"geoip2/geoip2": "^3.0",
"guzzlehttp/guzzle": "^7.5",
"happyr/doctrine-specification": "^2.0",
"jaybizzle/crawler-detect": "^1.2.116",
"laminas/laminas-config": "^3.8",
"laminas/laminas-config-aggregator": "^1.13",
@@ -33,50 +33,47 @@
"laminas/laminas-inputfilter": "^2.27",
"laminas/laminas-servicemanager": "^3.21",
"laminas/laminas-stdlib": "^3.17",
"league/uri": "^6.8",
"matomo/matomo-php-tracker": "^3.2",
"mezzio/mezzio": "^3.17",
"mezzio/mezzio-fastroute": "^3.10",
"mezzio/mezzio-fastroute": "^3.11",
"mezzio/mezzio-problem-details": "^1.13",
"mezzio/mezzio-swoole": "^4.7",
"mlocati/ip-lib": "^1.18",
"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.7.1",
"shlinkio/shlink-config": "^2.5",
"shlinkio/shlink-event-dispatcher": "^3.1",
"shlinkio/shlink-importer": "^5.2.1",
"shlinkio/shlink-installer": "^8.7",
"shlinkio/shlink-ip-geolocation": "^3.4",
"shlinkio/doctrine-specification": "^2.1.1",
"shlinkio/shlink-common": "^6.0",
"shlinkio/shlink-config": "^3.0",
"shlinkio/shlink-event-dispatcher": "^4.0",
"shlinkio/shlink-importer": "^5.3",
"shlinkio/shlink-installer": "^9.0",
"shlinkio/shlink-ip-geolocation": "^4.0",
"shlinkio/shlink-json": "^1.1",
"spiral/roadrunner": "^2023.2",
"spiral/roadrunner-cli": "^2.5",
"spiral/roadrunner-http": "^3.1",
"spiral/roadrunner-jobs": "^4.0",
"symfony/console": "^6.3",
"symfony/filesystem": "^6.3",
"symfony/lock": "^6.3",
"symfony/process": "^6.3",
"symfony/string": "^6.3"
"spiral/roadrunner": "^2023.3",
"spiral/roadrunner-cli": "^2.6",
"spiral/roadrunner-http": "^3.3",
"spiral/roadrunner-jobs": "^4.3",
"symfony/console": "^7.0",
"symfony/filesystem": "^7.0",
"symfony/lock": "^7.0",
"symfony/process": "^7.0",
"symfony/string": "^7.0"
},
"require-dev": {
"devizzent/cebe-php-openapi": "^1.0.1",
"devster/ubench": "^2.1",
"infection/infection": "^0.27",
"openswoole/ide-helper": "~22.0.0",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-doctrine": "^1.3",
"phpstan/phpstan-phpunit": "^1.3",
"phpstan/phpstan-symfony": "^1.3",
"phpunit/php-code-coverage": "^10.1",
"phpunit/phpcov": "^9.0",
"phpunit/phpunit": "^10.4",
"roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.3.0",
"shlinkio/shlink-test-utils": "^3.8.1",
"symfony/var-dumper": "^6.3",
"shlinkio/shlink-test-utils": "^4.1",
"symfony/var-dumper": "^7.0",
"veewee/composer-run-parallel": "^1.3"
},
"conflict": {
@@ -111,8 +108,8 @@
},
"scripts": {
"ci": [
"@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
"@parallel infect:test:api infect:test:cli infect:ci:unit infect:ci:db"
"@parallel cs stan swagger:validate test:unit:ci test:db:sqlite:ci test:db:postgres test:db:mysql test:db:maria test:db:ms",
"@parallel test:api:ci test:cli:ci"
],
"cs": "phpcs -s",
"cs:fix": "phpcbf",
@@ -122,54 +119,27 @@
"@parallel test:api test:cli"
],
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --testdox",
"test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
"test:unit:ci": "@test:unit --coverage-php=build/coverage-unit.cov",
"test:unit:pretty": "@test:unit --coverage-html build/coverage-unit/coverage-html",
"test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml",
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov",
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite",
"test:db:maria": "DB_DRIVER=maria composer test:db:sqlite",
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
"test:api": "bin/test/run-api-tests.sh",
"test:api:rr": "TEST_RUNTIME=rr bin/test/run-api-tests.sh",
"test:api:ci": "GENERATE_COVERAGE=yes composer test:api",
"test:api:pretty": "GENERATE_COVERAGE=pretty composer test:api",
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml --log-junit=build/coverage-cli/junit.xml",
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli",
"test:cli:pretty": "GENERATE_COVERAGE=pretty composer test:cli",
"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=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": [
"@parallel test:unit:ci test:db:sqlite:ci test:api:ci",
"@infect:ci"
],
"infect:test:unit": [
"@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"
],
"infect:test:cli": [
"@test:cli:ci",
"@infect:ci:cli"
],
"test:api:ci": "GENERATE_COVERAGE=yes composer test:api && vendor/bin/phpcov merge build/coverage-api --php build/coverage-api.cov && rm build/coverage-api/*.cov",
"test:api:pretty": "GENERATE_COVERAGE=yes composer test:api && vendor/bin/phpcov merge build/coverage-api --html build/coverage-api/coverage-html && rm build/coverage-api/*.cov",
"test:cli": "APP_ENV=test DB_DRIVER=maria TEST_ENV=cli php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-cli.xml",
"test:cli:ci": "GENERATE_COVERAGE=yes composer test:cli && vendor/bin/phpcov merge build/coverage-cli --php build/coverage-cli.cov && rm build/coverage-cli/*.cov",
"test:cli:pretty": "GENERATE_COVERAGE=yes composer test:cli && vendor/bin/phpcov merge build/coverage-cli --html build/coverage-cli/coverage-html && rm build/coverage-cli/*.cov",
"swagger:validate": "php-openapi validate docs/swagger/swagger.json",
"swagger:inline": "php-openapi inline docs/swagger/swagger.json docs/swagger/swagger-inlined.json",
"clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php"
},
"scripts-descriptions": {
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"swagger:validate\", \"test:ci\" and \"infect:ci\"</>",
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"swagger:validate\" and \"test:ci\"</>",
"cs": "<fg=blue;options=bold>Checks coding styles</>",
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
"stan": "<fg=blue;options=bold>Inspects code with phpstan</>",
@@ -190,10 +160,6 @@
"test:cli": "<fg=blue;options=bold>Runs CLI test suites</>",
"test:cli:ci": "<fg=blue;options=bold>Runs CLI test suites, and generates code coverage for CI</>",
"test:cli:pretty": "<fg=blue;options=bold>Runs CLI test suites, and generates code coverage in HTML format</>",
"infect:ci": "<fg=blue;options=bold>Checks unit and db tests quality applying mutation testing with existing reports and logs</>",
"infect:ci:unit": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing with existing reports and logs</>",
"infect:ci:db": "<fg=blue;options=bold>Checks db tests quality applying mutation testing with existing reports and logs</>",
"infect:test": "<fg=blue;options=bold>Runs unit and db tests, then checks tests quality applying mutation testing</>",
"swagger:validate": "<fg=blue;options=bold>Validates the swagger docs, making sure they fulfil the spec</>",
"swagger:inline": "<fg=blue;options=bold>Inlines swagger docs in a single file</>",
"clean:dev": "<fg=blue;options=bold>Deletes artifacts which are gitignored and could affect dev env</>"
@@ -204,7 +170,6 @@
"allow-plugins": {
"composer/package-versions-deprecated": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"infection/extension-installer": true,
"veewee/composer-run-parallel": true
}
}

View File

@@ -11,7 +11,6 @@ return (static function (): array {
'redis' => [
'servers' => $redisServers,
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
'decode_credentials' => (bool) EnvVars::REDIS_DECODE_CREDENTIALS->loadFromEnv(false),
],
];

View File

@@ -8,7 +8,7 @@ return [
'debug' => false,
// Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
// commands don't generate a cache file that's then used by php-fpm web executions
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
use GuzzleHttp\Client;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Mezzio\Application;
use Mezzio\Container;
use Psr\Http\Client\ClientInterface;
@@ -12,12 +13,14 @@ use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileFactoryInterface;
use Spiral\RoadRunner\Http\PSR7Worker;
use Spiral\RoadRunner\WorkerInterface;
use Symfony\Component\Filesystem\Filesystem;
return [
'dependencies' => [
'factories' => [
PSR7Worker::class => ConfigAbstractFactory::class,
Filesystem::class => InvokableFactory::class,
],
'delegators' => [

View File

@@ -16,6 +16,10 @@ return (static function (): array {
'mssql' => 'pdo_sqlsrv',
default => 'pdo_mysql',
};
$readCredentialAsString = static function (EnvVars $envVar): string|null {
$value = $envVar->loadFromEnv();
return $value === null ? null : (string) $value;
};
$resolveDefaultPort = static fn () => match ($driver) {
'postgres' => '5432',
'mssql' => '1433',
@@ -28,6 +32,7 @@ return (static function (): array {
'postgres' => 'utf8',
default => null,
};
$resolveConnection = static fn () => match ($driver) {
null, 'sqlite' => [
'driver' => 'pdo_sqlite',
@@ -36,8 +41,8 @@ return (static function (): array {
default => [
'driver' => $resolveDriver(),
'dbname' => EnvVars::DB_NAME->loadFromEnv('shlink'),
'user' => EnvVars::DB_USER->loadFromEnv(),
'password' => EnvVars::DB_PASSWORD->loadFromEnv(),
'user' => $readCredentialAsString(EnvVars::DB_USER),
'password' => $readCredentialAsString(EnvVars::DB_PASSWORD),
'host' => EnvVars::DB_HOST->loadFromEnv(EnvVars::DB_UNIX_SOCKET->loadFromEnv()),
'port' => EnvVars::DB_PORT->loadFromEnv($resolveDefaultPort()),
'unix_socket' => $isMysqlCompatible ? EnvVars::DB_UNIX_SOCKET->loadFromEnv() : null,

View File

@@ -21,8 +21,6 @@ return [
Option\Database\DatabaseUnixSocketConfigOption::class,
Option\UrlShortener\ShortDomainHostConfigOption::class,
Option\UrlShortener\ShortDomainSchemaConfigOption::class,
Option\Visit\VisitsWebhooksConfigOption::class,
Option\Visit\OrphanVisitsWebhooksConfigOption::class,
Option\Redirect\BaseUrlRedirectConfigOption::class,
Option\Redirect\InvalidShortUrlRedirectConfigOption::class,
Option\Redirect\Regular404RedirectConfigOption::class,
@@ -30,10 +28,7 @@ return [
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,
@@ -62,6 +57,9 @@ return [
Option\QrCode\DefaultFormatConfigOption::class,
Option\QrCode\DefaultErrorCorrectionConfigOption::class,
Option\QrCode\DefaultRoundBlockSizeConfigOption::class,
Option\QrCode\DefaultColorConfigOption::class,
Option\QrCode\DefaultBgColorConfigOption::class,
Option\QrCode\DefaultLogoUrlConfigOption::class,
Option\QrCode\EnabledForDisabledShortUrlsConfigOption::class,
Option\RabbitMq\RabbitMqEnabledConfigOption::class,
Option\RabbitMq\RabbitMqHostConfigOption::class,

View File

@@ -7,20 +7,21 @@ namespace Shlinkio\Shlink;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Monolog\Level;
use Monolog\Logger;
use PhpMiddleware\RequestId;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Shlinkio\Shlink\Common\Logger\LoggerFactory;
use Shlinkio\Shlink\Common\Logger\LoggerType;
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
use function Shlinkio\Shlink\Config\runningInRoadRunner;
return (static function (): array {
$common = [
'level' => Level::Info->value,
'processors' => [RequestId\MonologProcessor::class],
'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%',
'processors' => [RequestIdMiddleware::class],
'line_format' =>
'[%datetime%] [%extra.' . RequestIdMiddleware::ATTRIBUTE . '%] %channel%.%level_name% - %message%',
];
return [
@@ -52,16 +53,5 @@ return (static function (): array {
],
],
// Deprecated. Remove in Shlink 4.0.0
'mezzio-swoole' => [
'swoole-http-server' => [
'logger' => [
// Let's disable mezio-swoole access logging, so that we can provide our own implementation,
// consistent for roadrunner and openswoole
'logger-name' => NullLogger::class,
],
],
],
];
})();

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
return [
'mercure' => [
'public_hub_url' => 'http://localhost:8001',
'public_hub_url' => 'http://localhost:8002',
'internal_hub_url' => 'http://shlink_mercure_proxy',
'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error',
],

View File

@@ -7,10 +7,10 @@ namespace Shlinkio\Shlink;
use Laminas\Stratigility\Middleware\ErrorHandler;
use Mezzio\ProblemDetails;
use Mezzio\Router;
use PhpMiddleware\RequestId\RequestIdMiddleware;
use RKA\Middleware\IpAddress;
use Shlinkio\Shlink\Common\Middleware\AccessLogMiddleware;
use Shlinkio\Shlink\Common\Middleware\ContentLengthMiddleware;
use Shlinkio\Shlink\Common\Middleware\RequestIdMiddleware;
return [
@@ -47,7 +47,6 @@ return [
'rest' => [
'path' => '/rest',
'middleware' => [
Rest\Middleware\ErrorHandler\BackwardsCompatibleProblemDetailsHandler::class,
Router\Middleware\ImplicitOptionsMiddleware::class,
Rest\Middleware\BodyParserMiddleware::class,
Rest\Middleware\AuthenticationMiddleware::class,

View File

@@ -4,6 +4,8 @@ declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
use const Shlinkio\Shlink\DEFAULT_QR_CODE_BG_COLOR;
use const Shlinkio\Shlink\DEFAULT_QR_CODE_COLOR;
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS;
use const Shlinkio\Shlink\DEFAULT_QR_CODE_ERROR_CORRECTION;
use const Shlinkio\Shlink\DEFAULT_QR_CODE_FORMAT;
@@ -26,6 +28,9 @@ return [
'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(
DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS,
),
'color' => EnvVars::DEFAULT_QR_CODE_COLOR->loadFromEnv(DEFAULT_QR_CODE_COLOR),
'bg_color' => EnvVars::DEFAULT_QR_CODE_BG_COLOR->loadFromEnv(DEFAULT_QR_CODE_BG_COLOR),
'logo_url' => EnvVars::DEFAULT_QR_CODE_LOGO_URL->loadFromEnv(),
],
];

View File

@@ -14,9 +14,6 @@ return [
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),
'vhost' => EnvVars::RABBITMQ_VHOST->loadFromEnv('/'),
// Deprecated
'legacy_visits_publishing' => (bool) EnvVars::RABBITMQ_LEGACY_VISITS_PUBLISHING->loadFromEnv(false),
],
];

View File

@@ -7,6 +7,7 @@ return [
'rabbitmq' => [
'enabled' => true,
'host' => 'shlink_rabbitmq',
'port' => '5673',
'user' => 'rabbit',
'password' => 'rabbit',
],

View File

@@ -1,44 +0,0 @@
<?php
declare(strict_types=1);
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Laminas\ServiceManager\Factory\InvokableFactory;
use PhpMiddleware\RequestId;
use Shlinkio\Shlink\Common\Logger\Processor\BackwardsCompatibleMonologProcessorDelegator;
return [
'request_id' => [
'allow_override' => true,
'header_name' => 'X-Request-Id',
],
'dependencies' => [
'factories' => [
RequestId\Generator\RamseyUuid4StaticGenerator::class => InvokableFactory::class,
RequestId\RequestIdProviderFactory::class => ConfigAbstractFactory::class,
RequestId\RequestIdMiddleware::class => ConfigAbstractFactory::class,
RequestId\MonologProcessor::class => ConfigAbstractFactory::class,
],
'delegators' => [
RequestId\MonologProcessor::class => [
BackwardsCompatibleMonologProcessorDelegator::class,
],
],
],
ConfigAbstractFactory::class => [
RequestId\RequestIdProviderFactory::class => [
RequestId\Generator\RamseyUuid4StaticGenerator::class,
'config.request_id.allow_override',
'config.request_id.header_name',
],
RequestId\RequestIdMiddleware::class => [
RequestId\RequestIdProviderFactory::class,
'config.request_id.header_name',
],
RequestId\MonologProcessor::class => [RequestId\RequestIdMiddleware::class],
],
];

View File

@@ -11,7 +11,7 @@ return [
'base_path' => EnvVars::BASE_PATH->loadFromEnv(''),
'fastroute' => [
// Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console
// Disabling config cache for cli, ensures it's never used for RoadRunner, and also that console
// commands don't generate a cache file that's then used by php-fpm web executions
FastRouteRouter::CONFIG_CACHE_ENABLED => PHP_SAPI !== 'cli',
FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php',

View File

@@ -17,7 +17,6 @@ use Shlinkio\Shlink\Rest\Middleware\Mercure\NotConfiguredMercureErrorHandler;
use function sprintf;
return (static function (): array {
$contentNegotiationMiddleware = Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class;
$dropDomainMiddleware = Middleware\ShortUrl\DropDefaultDomainFromRequestMiddleware::class;
$overrideDomainMiddleware = Middleware\ShortUrl\OverrideDomainMiddleware::class;
@@ -32,9 +31,10 @@ return (static function (): array {
...ConfigProvider::applyRoutesPrefix([
Action\HealthAction::getRouteDef(),
// Visits and rules 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
// 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(),
@@ -44,15 +44,18 @@ return (static function (): array {
Action\Visit\DeleteOrphanVisitsAction::getRouteDef(),
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
//Redirect rules
Action\RedirectRule\ListRedirectRulesAction::getRouteDef([$dropDomainMiddleware]),
Action\RedirectRule\SetRedirectRulesAction::getRouteDef([$dropDomainMiddleware]),
// Short URLs
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
$contentNegotiationMiddleware,
$dropDomainMiddleware,
$overrideDomainMiddleware,
Middleware\ShortUrl\DefaultShortCodesLengthMiddleware::class,
]),
Action\ShortUrl\SingleStepCreateShortUrlAction::getRouteDef([
$contentNegotiationMiddleware,
Middleware\ShortUrl\CreateShortUrlContentNegotiationMiddleware::class,
$overrideDomainMiddleware,
]),
Action\ShortUrl\EditShortUrlAction::getRouteDef([$dropDomainMiddleware]),

View File

@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
use function Shlinkio\Shlink\Config\getOpenswooleConfigFromEnv;
use const Shlinkio\Shlink\MIN_TASK_WORKERS;
return (static function (): array {
$taskWorkers = (int) EnvVars::TASK_WORKER_NUM->loadFromEnv(16);
return [
'mezzio-swoole' => [
// Setting this to true can have unexpected behaviors when running several concurrent slow DB queries
'enable_coroutine' => false,
'swoole-http-server' => [
'host' => '0.0.0.0',
'port' => (int) EnvVars::PORT->loadFromEnv(8080),
'process-name' => 'shlink',
'options' => [
...getOpenswooleConfigFromEnv(),
'worker_num' => (int) EnvVars::WEB_WORKER_NUM->loadFromEnv(16),
'task_worker_num' => max($taskWorkers, MIN_TASK_WORKERS),
],
],
],
];
})();

View File

@@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
return [
'mezzio-swoole' => [
'hot-code-reload' => [
'enable' => true,
],
],
];

View File

@@ -14,7 +14,7 @@ return (static function (): array {
MIN_SHORT_CODES_LENGTH,
);
$modeFromEnv = EnvVars::SHORT_URL_MODE->loadFromEnv(ShortUrlMode::STRICT->value);
$mode = ShortUrlMode::tryDeprecated($modeFromEnv) ?? ShortUrlMode::STRICT;
$mode = ShortUrlMode::tryFrom($modeFromEnv) ?? ShortUrlMode::STRICT;
return [
@@ -24,7 +24,7 @@ return (static function (): array {
'hostname' => EnvVars::DEFAULT_DOMAIN->loadFromEnv(''),
],
'default_short_codes_length' => $shortCodesLength,
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(false),
'auto_resolve_titles' => (bool) EnvVars::AUTO_RESOLVE_TITLES->loadFromEnv(true),
'append_extra_path' => (bool) EnvVars::REDIRECT_APPEND_EXTRA_PATH->loadFromEnv(false),
'multi_segment_slugs_enabled' => (bool) EnvVars::MULTI_SEGMENT_SLUGS_ENABLED->loadFromEnv(false),
'trailing_slash_enabled' => (bool) EnvVars::SHORT_URL_TRAILING_SLASH->loadFromEnv(false),

View File

@@ -2,7 +2,6 @@
declare(strict_types=1);
use function Shlinkio\Shlink\Config\runningInOpenswoole;
use function Shlinkio\Shlink\Config\runningInRoadRunner;
return [
@@ -12,11 +11,9 @@ return [
'schema' => 'http',
'hostname' => sprintf('localhost:%s', match (true) {
runningInRoadRunner() => '8800',
runningInOpenswoole() => '8080',
default => '8000',
}),
],
'auto_resolve_titles' => true,
// 'multi_segment_slugs_enabled' => true,
// 'trailing_slash_enabled' => true,
],

View File

@@ -1,20 +0,0 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
// Deprecated. Webhooks are no longer supported. To be removed in Shlink 4.0.0
return (static function (): array {
$webhooks = EnvVars::VISITS_WEBHOOKS->loadFromEnv();
return [
'visits_webhooks' => [
'webhooks' => $webhooks === null ? [] : explode(',', $webhooks),
'notify_orphan_visits_to_webhooks' =>
(bool) EnvVars::NOTIFY_ORPHAN_VISITS_TO_WEBHOOKS->loadFromEnv(false),
],
];
})();

View File

@@ -8,19 +8,12 @@ use Laminas\ConfigAggregator;
use Laminas\Diactoros;
use Mezzio;
use Mezzio\ProblemDetails;
use Mezzio\Swoole;
use Shlinkio\Shlink\Config\ConfigAggregator\EnvVarLoaderProvider;
use function class_exists;
use function Shlinkio\Shlink\Config\env;
use function Shlinkio\Shlink\Config\openswooleIsInstalled;
use function Shlinkio\Shlink\Config\runningInRoadRunner;
use function Shlinkio\Shlink\Core\enumValues;
use const PHP_SAPI;
$isTestEnv = env('APP_ENV') === 'test';
$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner();
return (new ConfigAggregator\ConfigAggregator(
providers: [
@@ -30,9 +23,6 @@ return (new ConfigAggregator\ConfigAggregator(
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,

View File

@@ -9,7 +9,7 @@ use Shlinkio\Shlink\Core\Util\RedirectStatus;
const DEFAULT_DELETE_SHORT_URL_THRESHOLD = 15;
const DEFAULT_SHORT_CODES_LENGTH = 5;
const MIN_SHORT_CODES_LENGTH = 4;
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302; // Deprecated. Default to 307 for Shlink v4
const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302;
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
@@ -19,6 +19,6 @@ const DEFAULT_QR_CODE_MARGIN = 0;
const DEFAULT_QR_CODE_FORMAT = 'png';
const DEFAULT_QR_CODE_ERROR_CORRECTION = 'l';
const DEFAULT_QR_CODE_ROUND_BLOCK_SIZE = true;
// Deprecated. Shlink 4.0.0 should change default value to `true`
const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = false;
const MIN_TASK_WORKERS = 4;
const DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS = true;
const DEFAULT_QR_CODE_COLOR = '#000000'; // Black
const DEFAULT_QR_CODE_BG_COLOR = '#ffffff'; // White

View File

@@ -12,17 +12,6 @@ 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
{
OpenSwoole\Util::setProcessName($name);
}
// phpcs:enable
}
// This is one of the first files loaded. Configure the timezone here
date_default_timezone_set(EnvVars::TIMEZONE->loadFromEnv(date_default_timezone_get()));

View File

@@ -0,0 +1,49 @@
version: '3'
############################################################################################
# Routes here need to be relative to the project root, as API tests are run with `-w .` #
# See https://github.com/orgs/roadrunner-server/discussions/1440#discussioncomment-8486186 #
############################################################################################
rpc:
listen: tcp://127.0.0.1:6001
server:
command: 'php ./bin/roadrunner-worker.php'
http:
address: '0.0.0.0:9999'
middleware: ['static']
static:
dir: './public'
forbid: ['.php', '.htaccess']
pool:
num_workers: 1
debug: false
jobs:
pool:
num_workers: 1
debug: false
timeout: 300
consume: ['shlink']
pipelines:
shlink:
driver: memory
config:
priority: 10
prefetch: 10
logs:
encoding: json
mode: development
channels:
http:
mode: 'off' # Disable logging as Shlink handles it internally
server:
encoding: json
level: info
metrics:
level: panic
jobs:
level: panic

View File

@@ -7,12 +7,6 @@ namespace Shlinkio\Shlink\TestUtils;
use Doctrine\ORM\EntityManager;
use Psr\Container\ContainerInterface;
use function register_shutdown_function;
use function sprintf;
use const ShlinkioTest\Shlink\API_TESTS_HOST;
use const ShlinkioTest\Shlink\API_TESTS_PORT;
/** @var ContainerInterface $container */
$container = require __DIR__ . '/../container.php';
$testHelper = $container->get(Helper\TestHelper::class);
@@ -20,14 +14,6 @@ $config = $container->get('config');
$em = $container->get(EntityManager::class);
$httpClient = $container->get('shlink_test_api_client');
// Dump code coverage when process shuts down
register_shutdown_function(function () use ($httpClient): void {
$httpClient->request(
'GET',
sprintf('http://%s:%s/api-tests/stop-coverage', API_TESTS_HOST, API_TESTS_PORT),
);
});
$testHelper->createTestDb(
createDbCommand: ['bin/cli', 'db:create'],
migrateDbCommand: ['bin/cli', 'db:migrate'],

View File

@@ -6,75 +6,38 @@ namespace Shlinkio\Shlink;
use GuzzleHttp\Client;
use Laminas\ConfigAggregator\ConfigAggregator;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\Diactoros\Response\HtmlResponse;
use Laminas\ServiceManager\Factory\InvokableFactory;
use League\Event\EventDispatcher;
use Mezzio\Router\FastRouteRouter;
use Monolog\Level;
use PHPUnit\Runner\Version;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\Selector;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\Report\Html\Facade as Html;
use SebastianBergmann\CodeCoverage\Report\PHP;
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
use Shlinkio\Shlink\Common\Logger\LoggerType;
use Shlinkio\Shlink\TestUtils\ApiTest\CoverageMiddleware;
use Shlinkio\Shlink\TestUtils\CliTest\CliCoverageDelegator;
use Shlinkio\Shlink\TestUtils\Helper\CoverageHelper;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use function file_exists;
use function Laminas\Stratigility\middleware;
use function Shlinkio\Shlink\Config\env;
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
use function sleep;
use function sprintf;
use function sys_get_temp_dir;
use const ShlinkioTest\Shlink\API_TESTS_HOST;
use const ShlinkioTest\Shlink\API_TESTS_PORT;
$isApiTest = env('TEST_ENV') === 'api';
$isCliTest = env('TEST_ENV') === 'cli';
$testEnv = env('TEST_ENV');
$isApiTest = $testEnv === 'api';
$isCliTest = $testEnv === 'cli';
$isE2eTest = $isApiTest || $isCliTest;
$coverageType = env('GENERATE_COVERAGE');
$generateCoverage = contains($coverageType, ['yes', 'pretty']);
$coverage = null;
if ($isE2eTest && $generateCoverage) {
$filter = new Filter();
$filter->includeDirectory(__DIR__ . '/../../module/Core/src');
$filter->includeDirectory(__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src');
$coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter);
}
/**
* @param 'api'|'cli' $type
*/
$exportCoverage = static function (string $type = 'api') use (&$coverage, $coverageType): void {
if ($coverage === null) {
return;
}
$basePath = __DIR__ . '/../../build/coverage-' . $type;
$covPath = $basePath . '.cov';
// Every CLI test runs on its own process and dumps the coverage afterwards.
// Try to load it and merge it, so that we end up with the whole coverage at the end.
if ($type === 'cli' && file_exists($covPath)) {
$coverage->merge(require $covPath);
}
if ($coverageType === 'pretty') {
(new Html())->process($coverage, $basePath . '/coverage-html');
} else {
(new PHP())->process($coverage, $covPath);
(new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml');
}
};
$generateCoverage = $coverageType === 'yes';
$coverage = $isE2eTest && $generateCoverage ? CoverageHelper::createCoverageForDirectories(
[
__DIR__ . '/../../module/Core/src',
__DIR__ . '/../../module/' . ($isApiTest ? 'Rest' : 'CLI') . '/src',
],
__DIR__ . '/../../build/coverage-' . $testEnv,
) : null;
$buildDbConnection = static function (): array {
$driver = env('DB_DRIVER', 'sqlite');
@@ -89,7 +52,7 @@ $buildDbConnection = static function (): array {
'postgres' => [
'driver' => 'pdo_pgsql',
'host' => $isCi ? '127.0.0.1' : 'shlink_db_postgres',
'port' => $isCi ? '5433' : '5432',
'port' => $isCi ? '5434' : '5432',
'user' => 'postgres',
'password' => 'root',
'dbname' => 'shlink_test',
@@ -128,6 +91,7 @@ return [
'debug' => true,
ConfigAggregator::ENABLE_CACHE => false,
FastRouteRouter::CONFIG_CACHE_ENABLED => false,
'url_shortener' => [
'domain' => [
@@ -136,52 +100,27 @@ return [
],
],
'mezzio-swoole' => [
'enable_coroutine' => false,
'swoole-http-server' => [
'host' => API_TESTS_HOST,
'port' => API_TESTS_PORT,
'process-name' => 'shlink_test',
'options' => [
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
'log_file' => __DIR__ . '/../../data/log/api-tests/output.log',
'enable_coroutine' => false,
],
],
],
'routes' => !$isApiTest ? [] : [
'routes' => [
// This route is used to test that title resolution is skipped if the long URL times out
[
'name' => 'dump_coverage',
'path' => '/api-tests/stop-coverage',
'middleware' => middleware(static function () use ($exportCoverage) {
// TODO I have tried moving this block to a listener so that it's invoked automatically,
// but then the coverage is generated empty ¯\_(ツ)_/¯
$exportCoverage();
return new EmptyResponse();
}),
'name' => 'long_url_with_timeout',
'path' => '/api-tests/long-url-with-timeout',
'allowed_methods' => ['GET'],
'middleware' => middleware(static function () {
sleep(5); // Title resolution times out at 3 seconds
return new HtmlResponse('<title>The title</title>');
}),
],
],
'middleware_pipeline' => !$isApiTest ? [] : [
'capture_code_coverage' => [
'middleware' => middleware(static function (
ServerRequestInterface $req,
RequestHandlerInterface $handler,
) use (&$coverage): ResponseInterface {
$coverage?->start($req->getHeaderLine('x-coverage-id'));
try {
return $handler->handle($req);
} finally {
$coverage?->stop();
}
}),
'middleware' => new CoverageMiddleware($coverage),
'priority' => 9999,
],
],
// Disable mercure integration during E2E tests
'mercure' => [
'public_hub_url' => null,
'internal_hub_url' => null,
@@ -200,58 +139,7 @@ return [
],
'delegators' => $isCliTest ? [
Application::class => [
static function (
ContainerInterface $c,
string $serviceName,
callable $callback,
) use (
&$coverage,
$exportCoverage,
) {
/** @var Application $app */
$app = $callback();
$wrappedEventDispatcher = new EventDispatcher();
// When the command starts, start collecting coverage
$wrappedEventDispatcher->subscribeTo(
ConsoleCommandEvent::class,
static function () use (&$coverage): void {
$id = env('COVERAGE_ID');
if ($id === null) {
return;
}
$coverage?->start($id);
},
);
// When the command ends, stop collecting coverage
$wrappedEventDispatcher->subscribeTo(
ConsoleTerminateEvent::class,
static function () use (&$coverage, $exportCoverage): void {
$id = env('COVERAGE_ID');
if ($id === null) {
return;
}
$coverage?->stop();
$exportCoverage('cli');
},
);
$app->setDispatcher(new class ($wrappedEventDispatcher) implements EventDispatcherInterface {
public function __construct(private EventDispatcher $wrappedDispatcher)
{
}
public function dispatch(object $event, ?string $eventName = null): object
{
$this->wrappedDispatcher->dispatch($event);
return $event;
}
});
return $app;
},
new CliCoverageDelegator($coverage),
],
] : [],
],
@@ -262,7 +150,7 @@ return [
'data_fixtures' => [
'paths' => [
// TODO These are used for CLI tests too, so maybe should be somewhere else
// TODO These are used for other module's tests, so maybe should be somewhere else
__DIR__ . '/../../module/Rest/test-api/Fixtures',
],
],

View File

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

View File

@@ -1,13 +0,0 @@
/var/log/shlink/shlink_openswoole.log {
su root root
daily
missingok
rotate 120
compress
delaycompress
notifempty
create 0640 root root
postrotate
/etc/init.d/shlink_openswoole restart
endscript
}

View File

@@ -1,54 +0,0 @@
#!/bin/bash
### BEGIN INIT INFO
# Provides: shlink_openswoole
# Required-Start: $local_fs $network $named $time $syslog
# Required-Stop: $local_fs $network $named $time $syslog
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: Shlink non-blocking server with openswoole
### END INIT INFO
SCRIPT=/path/to/shlink/vendor/bin/laminas\ mezzio:swoole:start
RUNAS=root
PIDFILE=/var/run/shlink_openswoole.pid
LOGDIR=/var/log/shlink
LOGFILE=${LOGDIR}/shlink_openswoole.log
start() {
if [[ -f "$PIDFILE" ]] && kill -0 $(cat "$PIDFILE"); then
echo 'Shlink with openswoole already running' >&2
return 1
fi
echo 'Starting shlink with openswoole' >&2
mkdir -p "$LOGDIR"
touch "$LOGFILE"
local CMD="$SCRIPT &> \"$LOGFILE\" & echo \$!"
su -c "$CMD" $RUNAS > "$PIDFILE"
echo 'Shlink started' >&2
}
stop() {
if [[ ! -f "$PIDFILE" ]] || ! kill -0 $(cat "$PIDFILE"); then
echo 'Shlink with openswoole not running' >&2
return 1
fi
echo 'Stopping shlink with openswoole' >&2
kill -15 $(cat "$PIDFILE") && rm -f "$PIDFILE"
echo 'Shlink stopped' >&2
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
echo "Usage: $0 {start|stop|restart}"
esac

View File

@@ -1,8 +1,8 @@
FROM php:8.2-fpm-alpine3.17
FROM php:8.3-fpm-alpine3.19
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.21
ENV PDO_SQLSRV_VERSION 5.11.1
ENV APCU_VERSION 5.1.23
ENV PDO_SQLSRV_VERSION 5.12.0
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1

View File

@@ -1,8 +1,8 @@
FROM php:8.2-alpine3.17
FROM php:8.3-alpine3.19
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.21
ENV PDO_SQLSRV_VERSION 5.11.1
ENV APCU_VERSION 5.1.23
ENV PDO_SQLSRV_VERSION 5.12.0
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1

View File

@@ -1,85 +0,0 @@
FROM php:8.2-alpine3.17
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.21
ENV INOTIFY_VERSION 3.0.0
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
RUN apk update
# Install common php extensions
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install calendar
RUN apk add --no-cache oniguruma-dev
RUN docker-php-ext-install mbstring
RUN apk add --no-cache sqlite-libs
RUN apk add --no-cache sqlite-dev
RUN docker-php-ext-install pdo_sqlite
RUN apk add --no-cache icu-dev
RUN docker-php-ext-install intl
RUN apk add --no-cache libzip-dev zlib-dev
RUN docker-php-ext-install zip
RUN apk add --no-cache libpng-dev
RUN docker-php-ext-install gd
RUN apk add --no-cache postgresql-dev
RUN docker-php-ext-install pdo_pgsql
RUN apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS linux-headers && \
docker-php-ext-install sockets && \
apk del .phpize-deps
RUN docker-php-ext-install bcmath
# Install APCu extension
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu \
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1 \
&& docker-php-ext-configure apcu \
&& docker-php-ext-install apcu \
&& rm /tmp/apcu.tar.gz \
&& rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini \
&& echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
# Install inotify extension
ADD https://pecl.php.net/get/inotify-$INOTIFY_VERSION.tgz /tmp/inotify.tar.gz
RUN mkdir -p /usr/src/php/ext/inotify \
&& tar xf /tmp/inotify.tar.gz -C /usr/src/php/ext/inotify --strip-components=1 \
&& docker-php-ext-configure inotify \
&& docker-php-ext-install inotify \
&& rm /tmp/inotify.tar.gz
# Install openswoole, pcov and mssql driver
RUN wget https://download.microsoft.com/download/${MS_ODBC_DOWNLOAD}/msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --allow-untrusted msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk && \
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
pecl install openswoole-${OPENSWOOLE_VERSION} pdo_sqlsrv-${PDO_SQLSRV_VERSION} pcov && \
docker-php-ext-enable openswoole pdo_sqlsrv pcov && \
apk del .phpize-deps && \
rm msodbcsql${MS_ODBC_SQL_VERSION}-1_amd64.apk
# Install composer
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
# Make home directory writable by anyone
RUN chmod 777 /home
VOLUME /home/shlink
WORKDIR /home/shlink
# Expose openswoole port
EXPOSE 8080
CMD \
# Install dependencies if the vendor dir does not exist
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
# 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

View File

@@ -1,14 +0,0 @@
server {
listen 80 default_server;
error_log /home/shlink/www/data/infra/nginx/swoole_proxy.error.log;
location / {
proxy_http_version 1.1;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://shlink_swoole:8080;
proxy_read_timeout 90s;
}
}

View File

@@ -7,12 +7,6 @@ services:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
shlink_swoole:
user: 1000:1000
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro
shlink_roadrunner:
user: 1000:1000
volumes:

View File

@@ -39,44 +39,6 @@ services:
extra_hosts:
- 'host.docker.internal:host-gateway'
shlink_swoole_proxy:
container_name: shlink_swoole_proxy
image: nginx:1.25-alpine
ports:
- "8002:80"
volumes:
- ./:/home/shlink/www
- ./data/infra/swoole_proxy_vhost.conf:/etc/nginx/conf.d/default.conf
links:
- shlink_swoole
shlink_swoole:
container_name: shlink_swoole
build:
context: .
dockerfile: ./data/infra/swoole.Dockerfile
ports:
- "8080:8080"
- "9001:9001"
volumes:
- ./:/home/shlink
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
links:
- shlink_db_mysql
- shlink_db_postgres
- shlink_db_maria
- shlink_db_ms
- shlink_redis
- shlink_redis_acl
- shlink_mercure
- shlink_mercure_proxy
- shlink_rabbitmq
- shlink_matomo
environment:
LC_ALL: C
extra_hosts:
- 'host.docker.internal:host-gateway'
shlink_roadrunner:
container_name: shlink_roadrunner
build:
@@ -119,7 +81,7 @@ services:
container_name: shlink_db_postgres
image: postgres:12.2-alpine
ports:
- "5433:5432"
- "5434:5432"
volumes:
- ./:/home/shlink/www
- ./data/infra/database_pg:/var/lib/postgresql/data
@@ -169,7 +131,7 @@ services:
container_name: shlink_mercure_proxy
image: nginx:1.25-alpine
ports:
- "8001:80"
- "8002:80"
volumes:
- ./:/home/shlink/www
- ./data/infra/mercure_proxy_vhost.conf:/etc/nginx/conf.d/default.conf
@@ -191,15 +153,15 @@ services:
container_name: shlink_rabbitmq
image: rabbitmq:3.11-management-alpine
ports:
- "15672:15672"
- "5672:5672"
- "15673:15672"
- "5673:5672"
environment:
RABBITMQ_DEFAULT_USER: "rabbit"
RABBITMQ_DEFAULT_PASS: "rabbit"
shlink_swagger_ui:
container_name: shlink_swagger_ui
image: swaggerapi/swagger-ui:v5.10.3
image: swaggerapi/swagger-ui:v5.11.3
ports:
- "8005:8080"
volumes:

View File

@@ -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 [RoadRunner](https://roadrunner.dev) or [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), which can be linked to external databases to persist data.
## Usage

View File

@@ -20,19 +20,6 @@ fi
php vendor/bin/shlink-installer init ${flags}
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided and running as root
# FIXME: ENABLE_PERIODIC_VISIT_LOCATE is deprecated. Remove cron support in Shlink 4.0.0
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ] && [ "${SHLINK_USER_ID}" = "root" ]; then
echo "Configuring periodic visit location..."
echo "0 * * * * php /etc/shlink/bin/cli visit:locate -q" > /etc/crontabs/root
/usr/sbin/crond &
fi
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
if [ "$SHLINK_RUNTIME" = 'rr' ]; then
./bin/rr serve -c config/roadrunner/.rr.yml
fi

View File

@@ -111,9 +111,6 @@
"type": "string",
"description": "The original long URL."
},
"deviceLongUrls": {
"$ref": "#/components/schemas/DeviceLongUrls"
},
"dateCreated": {
"type": "string",
"format": "date-time",
@@ -122,11 +119,6 @@
"visitsSummary": {
"$ref": "#/components/schemas/VisitsSummary"
},
"visitsCount": {
"deprecated": true,
"type": "integer",
"description": "The number of visits that this short URL has received."
},
"tags": {
"type": "array",
"items": {
@@ -155,11 +147,6 @@
"shortCode": "12C18",
"shortUrl": "https://s.test/12C18",
"longUrl": "https://store.steampowered.com",
"deviceLongUrls": {
"android": "https://store.steampowered.com/android",
"ios": "https://store.steampowered.com/ios",
"desktop": null
},
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsSummary": {
"total": 328,
@@ -223,24 +210,6 @@
}
}
},
"DeviceLongUrls": {
"type": "object",
"required": ["android", "ios", "desktop"],
"properties": {
"android": {
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
"type": "string"
},
"ios": {
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
"type": "string"
},
"desktop": {
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
"type": "string"
}
}
},
"Visit": {
"type": "object",
"properties": {

View File

@@ -1,17 +0,0 @@
{
"type": "object",
"properties": {
"android": {
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
"type": ["string"]
},
"ios": {
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
"type": ["string"]
},
"desktop": {
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
"type": ["string"]
}
}
}

View File

@@ -1,17 +0,0 @@
{
"type": "object",
"allOf": [{
"$ref": "./DeviceLongUrls.json"
}],
"properties": {
"android": {
"type": ["null"]
},
"ios": {
"type": ["null"]
},
"desktop": {
"type": ["null"]
}
}
}

View File

@@ -1,7 +0,0 @@
{
"type": "object",
"required": ["android", "ios", "desktop"],
"allOf": [{
"$ref": "./DeviceLongUrlsEdit.json"
}]
}

View File

@@ -0,0 +1,31 @@
{
"type": "object",
"required": ["longUrl", "conditions"],
"properties": {
"longUrl": {
"description": "Long URL to redirect to when this condition matches",
"type": "string"
},
"conditions": {
"description": "List of conditions that need to match in order to consider this rule matches",
"type": "array",
"items": {
"type": "object",
"required": ["type", "matchKey", "matchValue"],
"properties": {
"type": {
"type": "string",
"enum": ["device", "language", "query-param"],
"description": "The type of the condition, which will condition the logic used to match it"
},
"matchKey": {
"type": ["string", "null"]
},
"matchValue": {
"type": "string"
}
}
}
}
}
}

View File

@@ -4,9 +4,7 @@
"shortCode",
"shortUrl",
"longUrl",
"deviceLongUrls",
"dateCreated",
"visitsCount",
"visitsSummary",
"tags",
"meta",
@@ -28,19 +26,11 @@
"type": "string",
"description": "The original long URL."
},
"deviceLongUrls": {
"$ref": "./DeviceLongUrlsResp.json"
},
"dateCreated": {
"type": "string",
"format": "date-time",
"description": "The date in which the short URL was created in ISO format."
},
"visitsCount": {
"deprecated": true,
"type": "integer",
"description": "**[DEPRECATED]** Use `visitsSummary.total` instead."
},
"visitsSummary": {
"$ref": "./VisitsSummary.json"
},

View File

@@ -5,9 +5,6 @@
"description": "The long URL this short URL will redirect to",
"type": "string"
},
"deviceLongUrls": {
"$ref": "./DeviceLongUrlsEdit.json"
},
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": ["string", "null"]
@@ -20,11 +17,6 @@
"description": "The maximum number of allowed visits for this short code",
"type": ["number", "null"]
},
"validateUrl": {
"deprecated": true,
"description": "**[DEPRECATED]** Tells if the long URL should or should not be validated as a reachable URL. Defaults to `false`",
"type": "boolean"
},
"tags": {
"type": "array",
"items": {

View File

@@ -0,0 +1,13 @@
{
"type": "object",
"required": ["priority"],
"properties": {
"priority": {
"description": "Order in which attempting to match the rule. Lower goes first",
"type": "number"
}
},
"allOf": [{
"$ref": "./SetShortUrlRedirectRule.json"
}]
}

View File

@@ -1,6 +1,6 @@
{
"type": "object",
"required": ["tag", "shortUrlsCount", "visitsSummary", "visitsCount"],
"required": ["tag", "shortUrlsCount", "visitsSummary"],
"properties": {
"tag": {
"type": "string",
@@ -12,11 +12,6 @@
},
"visitsSummary": {
"$ref": "./VisitsSummary.json"
},
"visitsCount": {
"deprecated": true,
"type": "number",
"description": "**[DEPRECATED]** Use visitsSummary.total instead"
}
}
}

View File

@@ -1,22 +1,12 @@
{
"type": "object",
"required": ["nonOrphanVisits", "orphanVisits", "visitsCount", "orphanVisitsCount"],
"required": ["nonOrphanVisits", "orphanVisits"],
"properties": {
"nonOrphanVisits": {
"$ref": "./VisitsSummary.json"
},
"orphanVisits": {
"$ref": "./VisitsSummary.json"
},
"visitsCount": {
"deprecated": true,
"type": "number",
"description": "**[DEPRECATED]** Use nonOrphanVisits.total instead"
},
"orphanVisitsCount": {
"deprecated": true,
"type": "number",
"description": "**[DEPRECATED]** Use orphanVisits.total instead"
}
}
}

View File

@@ -1,9 +0,0 @@
{
"value": {
"title": "Invalid data",
"type": "INVALID_ARGUMENT",
"detail": "Provided data is not valid",
"status": 400,
"invalidElements": ["maxVisits", "validSince"]
}
}

View File

@@ -1,9 +0,0 @@
{
"value": {
"detail": "No URL found with short code \"abc123\"",
"title": "Short URL not found",
"type": "INVALID_SHORTCODE",
"status": 404,
"shortCode": "abc123"
}
}

View File

@@ -1,9 +0,0 @@
{
"value": {
"detail": "Tag with name \"foo\" could not be found",
"title": "Tag not found",
"type": "TAG_NOT_FOUND",
"status": 404,
"tag": "foo"
}
}

View File

@@ -163,11 +163,6 @@
"shortCode": "12C18",
"shortUrl": "https://s.test/12C18",
"longUrl": "https://store.steampowered.com",
"deviceLongUrls": {
"android": null,
"ios": null,
"desktop": null
},
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsSummary": {
"total": 328,
@@ -191,11 +186,6 @@
"shortCode": "12Kb3",
"shortUrl": "https://s.test/12Kb3",
"longUrl": "https://shlink.io",
"deviceLongUrls": {
"android": null,
"ios": "https://shlink.io/ios",
"desktop": null
},
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsSummary": {
"total": 1029,
@@ -218,11 +208,6 @@
"shortCode": "123bA",
"shortUrl": "https://example.com/123bA",
"longUrl": "https://www.google.com",
"deviceLongUrls": {
"android": null,
"ios": null,
"desktop": null
},
"dateCreated": "2015-10-01T20:34:16+02:00",
"visitsSummary": {
"total": 25,
@@ -296,13 +281,14 @@
"type": "object",
"required": ["longUrl"],
"properties": {
"deviceLongUrls": {
"$ref": "../definitions/DeviceLongUrls.json"
},
"customSlug": {
"description": "A unique custom slug to be used instead of the generated short code",
"type": "string"
},
"pathPrefix": {
"description": "A prefix that will be prepended to provided custom slug or auto-generated short code",
"type": "string"
},
"findIfExists": {
"description": "Will force existing matching URL to be returned if found, instead of creating a new one",
"type": "boolean"
@@ -334,11 +320,6 @@
"shortCode": "12C18",
"shortUrl": "https://s.test/12C18",
"longUrl": "https://store.steampowered.com",
"deviceLongUrls": {
"android": null,
"ios": null,
"desktop": null
},
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsSummary": {
"total": 0,
@@ -382,16 +363,13 @@
"validSince",
"validUntil",
"customSlug",
"pathPrefix",
"maxVisits",
"findIfExists",
"domain"
]
}
},
"url": {
"type": "string",
"description": "A URL that could not be verified, if the error type is https://shlink.io/api/error/invalid-url"
},
"customSlug": {
"type": "string",
"description": "Provided custom slug when the error type is https://shlink.io/api/error/non-unique-slug"
@@ -405,19 +383,10 @@
]
},
"examples": {
"Invalid arguments with API v3 and newer": {
"Invalid arguments": {
"$ref": "../examples/short-url-invalid-args-v3.json"
},
"Invalid long URL with API v3 and newer": {
"value": {
"title": "Invalid URL",
"type": "https://shlink.io/api/error/invalid-url",
"detail": "Provided URL foo is invalid. Try with a different one.",
"status": 400,
"url": "https://invalid-url.com"
}
},
"Non-unique slug with API v3 and newer": {
"Non-unique slug": {
"value": {
"title": "Invalid custom slug",
"type": "https://shlink.io/api/error/non-unique-slug",
@@ -425,27 +394,6 @@
"status": 400,
"customSlug": "my-slug"
}
},
"Invalid arguments previous to API v3": {
"$ref": "../examples/short-url-invalid-args-v2.json"
},
"Invalid long URL previous to API v3": {
"value": {
"title": "Invalid URL",
"type": "INVALID_URL",
"detail": "Provided URL foo is invalid. Try with a different one.",
"status": 400,
"url": "https://invalid-url.com"
}
},
"Non-unique slug previous to API v3": {
"value": {
"title": "Invalid custom slug",
"type": "INVALID_SLUG",
"detail": "Provided slug \"my-slug\" is already in use.",
"status": 400,
"customSlug": "my-slug"
}
}
}
}

View File

@@ -53,11 +53,6 @@
},
"example": {
"longUrl": "https://github.com/shlinkio/shlink",
"deviceLongUrls": {
"android": null,
"ios": null,
"desktop": null
},
"shortUrl": "https://s.test/abc123",
"shortCode": "abc123",
"dateCreated": "2016-08-21T20:34:16+02:00",
@@ -88,49 +83,6 @@
}
}
},
"400": {
"description": "The long URL was not provided or is invalid.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
},
"examples": {
"API v3 and newer": {
"value": {
"title": "Invalid URL",
"type": "https://shlink.io/api/error/invalid-url",
"detail": "Provided URL foo is invalid. Try with a different one.",
"status": 400,
"url": "https://invalid-url.com"
}
},
"Previous to API v3": {
"value": {
"title": "Invalid URL",
"type": "INVALID_URL",
"detail": "Provided URL foo is invalid. Try with a different one.",
"status": 400,
"url": "https://invalid-url.com"
}
}
}
},
"text/plain": {
"schema": {
"type": "string"
},
"examples": {
"API v3 and newer": {
"value": "https://shlink.io/api/error/invalid-url"
},
"Previous to API v3": {
"value": "INVALID_URL"
}
}
}
}
},
"default": {
"description": "Unexpected error.",
"content": {

View File

@@ -34,11 +34,6 @@
"shortCode": "12Kb3",
"shortUrl": "https://s.test/12Kb3",
"longUrl": "https://shlink.io",
"deviceLongUrls": {
"android": null,
"ios": null,
"desktop": null
},
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsSummary": {
"total": 1029,
@@ -86,11 +81,8 @@
]
},
"examples": {
"API v3 and newer": {
"Short URL not found": {
"$ref": "../examples/short-url-not-found-v3.json"
},
"Previous to API v3": {
"$ref": "../examples/short-url-not-found-v2.json"
}
}
}
@@ -155,11 +147,6 @@
"shortCode": "12Kb3",
"shortUrl": "https://s.test/12Kb3",
"longUrl": "https://shlink.io",
"deviceLongUrls": {
"android": "https://shlink.io/android",
"ios": null,
"desktop": null
},
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsSummary": {
"total": 1029,
@@ -212,11 +199,8 @@
]
},
"examples": {
"API v3 and newer": {
"Invalid arguments": {
"$ref": "../examples/short-url-invalid-args-v3.json"
},
"Previous to API v3": {
"$ref": "../examples/short-url-invalid-args-v2.json"
}
}
}
@@ -248,11 +232,8 @@
]
},
"examples": {
"API v3 and newer": {
"Short URL not found": {
"$ref": "../examples/short-url-not-found-v3.json"
},
"Previous to API v3": {
"$ref": "../examples/short-url-not-found-v2.json"
}
}
}
@@ -378,11 +359,8 @@
]
},
"examples": {
"API v3 and newer": {
"Short URL not found": {
"$ref": "../examples/short-url-not-found-v3.json"
},
"Previous to API v3": {
"$ref": "../examples/short-url-not-found-v2.json"
}
}
}

View File

@@ -145,11 +145,8 @@
"$ref": "../definitions/Error.json"
},
"examples": {
"Short URL not found with API v3 and newer": {
"Short URL not found": {
"$ref": "../examples/short-url-not-found-v3.json"
},
"Short URL not found previous to API v3": {
"$ref": "../examples/short-url-not-found-v2.json"
}
}
}
@@ -219,11 +216,8 @@
"$ref": "../definitions/Error.json"
},
"examples": {
"Short URL not found with API v3 and newer": {
"Short URL not found": {
"$ref": "../examples/short-url-not-found-v3.json"
},
"Short URL not found previous to API v3": {
"$ref": "../examples/short-url-not-found-v2.json"
}
}
}

View File

@@ -15,20 +15,6 @@
{
"$ref": "../parameters/version.json"
},
{
"name": "withStats",
"deprecated": true,
"description": "**[Deprecated]** Use [GET /tags/stats](#/Tags/tagsWithStats) endpoint to get tags with their stats.",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"true",
"false"
]
}
},
{
"name": "page",
"in": "query",
@@ -88,13 +74,6 @@
"type": "string"
}
},
"stats": {
"description": "The tag stats will be returned only if the withStats param was provided with value 'true'",
"type": "array",
"items": {
"$ref": "../definitions/TagInfo.json"
}
},
"pagination": {
"$ref": "../definitions/Pagination.json"
}
@@ -249,9 +228,6 @@
"examples": {
"API v3 and newer": {
"$ref": "../examples/tag-not-found-v3.json"
},
"Previous to API v3": {
"$ref": "../examples/tag-not-found-v2.json"
}
}
}

View File

@@ -148,12 +148,8 @@
"$ref": "../definitions/Error.json"
},
"examples": {
"API v3 and newer": {
"Tag not found": {
"$ref": "../examples/tag-not-found-v3.json"
},
"Previous to API v3": {
"$ref": "../examples/tag-not-found-v2.json"
}
}
}

View File

@@ -55,6 +55,16 @@
"type": "string",
"enum": ["true"]
}
},
{
"name": "type",
"in": "query",
"description": "The type of visits to return. All visits are returned when not provided.",
"required": false,
"schema": {
"type": "string",
"enum": ["invalid_short_url", "base_url", "regular_404"]
}
}
],
"security": [
@@ -137,6 +147,54 @@
}
}
},
"400": {
"description": "Provided query arguments are invalid.",
"content": {
"application/problem+json": {
"schema": {
"type": "object",
"allOf": [
{
"$ref": "../definitions/Error.json"
},
{
"type": "object",
"required": ["invalidElements"],
"properties": {
"invalidElements": {
"type": "array",
"items": {
"type": "string",
"enum": ["type"]
}
}
}
}
]
},
"examples": {
"API v3 and newer": {
"value": {
"title": "Invalid data",
"type": "https://shlink.io/api/error/invalid-data",
"detail": "Provided data is not valid",
"status": 400,
"invalidElements": ["type"]
}
},
"Previous to API v3": {
"value": {
"title": "Invalid data",
"type": "INVALID_ARGUMENT",
"detail": "Provided data is not valid",
"status": 400,
"invalidElements": ["type"]
}
}
}
}
}
},
"default": {
"description": "Unexpected error.",
"content": {

View File

@@ -0,0 +1,344 @@
{
"get": {
"operationId": "listShortUrlRedirectRules",
"tags": [
"Redirect rules"
],
"summary": "List short URL redirect rules",
"description": "Returns the list of redirect rules for a short URL.",
"parameters": [
{
"$ref": "../parameters/version.json"
},
{
"$ref": "../parameters/shortCode.json"
},
{
"$ref": "../parameters/domain.json"
}
],
"security": [
{
"ApiKey": []
}
],
"responses": {
"200": {
"description": "The list of rules",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["defaultLongUrl", "redirectRules"],
"properties": {
"defaultLongUrl": {
"type": "string"
},
"redirectRules": {
"type": "array",
"items": {
"$ref": "../definitions/ShortUrlRedirectRule.json"
}
}
}
},
"example": {
"defaultLongUrl": "https://example.com",
"redirectRules": [
{
"longUrl": "https://example.com/android-en-us",
"priority": 1,
"conditions": [
{
"type": "device",
"matchValue": "android",
"matchKey": null
},
{
"type": "language",
"matchValue": "en-US",
"matchKey": null
}
]
},
{
"longUrl": "https://example.com/fr",
"priority": 2,
"conditions": [
{
"type": "language",
"matchValue": "fr",
"matchKey": null
}
]
},
{
"longUrl": "https://example.com/query-foo-bar-hello-world",
"priority": 3,
"conditions": [
{
"type": "query",
"matchKey": "foo",
"matchValue": "bar"
},
{
"type": "query",
"matchKey": "hello",
"matchValue": "world"
}
]
}
]
}
}
}
},
"404": {
"description": "No URL was found for provided short code.",
"content": {
"application/problem+json": {
"schema": {
"allOf": [
{
"$ref": "../definitions/Error.json"
},
{
"type": "object",
"required": ["shortCode"],
"properties": {
"shortCode": {
"type": "string",
"description": "The short code with which we tried to find the short URL"
},
"domain": {
"type": "string",
"description": "The domain with which we tried to find the short URL"
}
}
}
]
},
"examples": {
"Short URL not found": {
"$ref": "../examples/short-url-not-found-v3.json"
}
}
}
}
},
"default": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
},
"post": {
"operationId": "setShortUrlRedirectRules",
"tags": [
"Redirect rules"
],
"summary": "Set short URL redirect rules",
"description": "Sets redirect rules for a short URL, with priorities matching the order in which they are provided.",
"parameters": [
{
"$ref": "../parameters/version.json"
},
{
"$ref": "../parameters/shortCode.json"
},
{
"$ref": "../parameters/domain.json"
}
],
"security": [
{
"ApiKey": []
}
],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"redirectRules": {
"type": "array",
"items": {
"$ref": "../definitions/SetShortUrlRedirectRule.json"
}
}
}
},
"example": {
"redirectRules": [
{
"longUrl": "https://example.com/android-en-us",
"conditions": [
{
"type": "device",
"matchValue": "android",
"matchKey": null
},
{
"type": "language",
"matchValue": "en-US",
"matchKey": null
}
]
},
{
"longUrl": "https://example.com/fr",
"conditions": [
{
"type": "language",
"matchValue": "fr",
"matchKey": null
}
]
},
{
"longUrl": "https://example.com/query-foo-bar-hello-world",
"conditions": [
{
"type": "query",
"matchKey": "foo",
"matchValue": "bar"
},
{
"type": "query",
"matchKey": "hello",
"matchValue": "world"
}
]
}
]
}
}
}
},
"responses": {
"200": {
"description": "The list of rules",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["defaultLongUrl", "redirectRules"],
"properties": {
"defaultLongUrl": {
"type": "string"
},
"redirectRules": {
"type": "array",
"items": {
"$ref": "../definitions/ShortUrlRedirectRule.json"
}
}
}
},
"example": {
"defaultLongUrl": "https://example.com",
"redirectRules": [
{
"longUrl": "https://example.com/android-en-us",
"priority": 1,
"conditions": [
{
"type": "device",
"matchValue": "android",
"matchKey": null
},
{
"type": "language",
"matchValue": "en-US",
"matchKey": null
}
]
},
{
"longUrl": "https://example.com/fr",
"priority": 2,
"conditions": [
{
"type": "language",
"matchValue": "fr",
"matchKey": null
}
]
},
{
"longUrl": "https://example.com/query-foo-bar-hello-world",
"priority": 3,
"conditions": [
{
"type": "query",
"matchKey": "foo",
"matchValue": "bar"
},
{
"type": "query",
"matchKey": "hello",
"matchValue": "world"
}
]
}
]
}
}
}
},
"404": {
"description": "No URL was found for provided short code.",
"content": {
"application/problem+json": {
"schema": {
"allOf": [
{
"$ref": "../definitions/Error.json"
},
{
"type": "object",
"required": ["shortCode"],
"properties": {
"shortCode": {
"type": "string",
"description": "The short code with which we tried to find the short URL"
},
"domain": {
"type": "string",
"description": "The domain with which we tried to find the short URL"
}
}
}
]
},
"examples": {
"Short URL not found": {
"$ref": "../examples/short-url-not-found-v3.json"
}
}
}
}
},
"default": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -65,6 +65,26 @@
"enum": ["true", "false"],
"default": "false"
}
},
{
"name": "color",
"in": "query",
"description": "The QR code foreground color. It should be an hex representation of a color, in 3 or 6 characters, optionally preceded by the \"#\" character.",
"required": false,
"schema": {
"type": "string",
"default": "#000000"
}
},
{
"name": "bgColor",
"in": "query",
"description": "The QR code background color. It should be an hex representation of a color, in 3 or 6 characters, optionally preceded by the \"#\" character.",
"required": false,
"schema": {
"type": "string",
"default": "#ffffff"
}
}
],
"responses": {

View File

@@ -42,6 +42,10 @@
"name": "Short URLs",
"description": "Operations that can be performed on short URLs"
},
{
"name": "Redirect rules",
"description": "Handle dynamic rule-based redirects"
},
{
"name": "Tags",
"description": "Let you handle the list of available tags"
@@ -79,6 +83,10 @@
"$ref": "paths/v1_short-urls_{shortCode}.json"
},
"/rest/v{version}/short-urls/{shortCode}/redirect-rules": {
"$ref": "paths/v3_short-urls_{shortCode}_redirect-rules.json"
},
"/rest/v{version}/tags": {
"$ref": "paths/v1_tags.json"
},

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env bash
# Run docker containers if they are not up yet
if ! [[ $(docker ps | grep shlink_swoole) ]]; then
if ! [[ $(docker ps | grep shlink_roadrunner) ]]; then
docker compose up -d
fi
docker exec -it shlink_swoole /bin/sh -c "$*"
docker exec -it shlink_roadrunner /bin/sh -c "$*"

View File

@@ -1,24 +0,0 @@
{
source: {
directories: [
'module/*/src'
]
},
timeout: 5,
logs: {
text: 'build/infection-api/infection-log.txt',
html: 'build/infection-api/infection-log.html',
summary: 'build/infection-api/summary-log.txt',
debug: 'build/infection-api/debug-log.txt'
},
tmpDir: 'build/infection-api/temp',
phpUnit: {
configDir: '.'
},
testFrameworkOptions: '--configuration=phpunit-api.xml',
mutators: {
'@default': true,
IdenticalEqual: false,
NotIdenticalNotEqual: false
}
}

View File

@@ -1,24 +0,0 @@
{
source: {
directories: [
'module/*/src'
]
},
timeout: 5,
logs: {
text: 'build/infection-cli/infection-log.txt',
html: 'build/infection-cli/infection-log.html',
summary: 'build/infection-cli/summary-log.txt',
debug: 'build/infection-cli/debug-log.txt'
},
tmpDir: 'build/infection-cli/temp',
phpUnit: {
configDir: '.'
},
testFrameworkOptions: '--configuration=phpunit-cli.xml',
mutators: {
'@default': true,
IdenticalEqual: false,
NotIdenticalNotEqual: false
}
}

View File

@@ -1,24 +0,0 @@
{
source: {
directories: [
'module/*/src'
]
},
timeout: 5,
logs: {
text: 'build/infection-db/infection-log.txt',
html: 'build/infection-db/infection-log.html',
summary: 'build/infection-db/summary-log.txt',
debug: 'build/infection-db/debug-log.txt'
},
tmpDir: 'build/infection-db/temp',
phpUnit: {
configDir: '.'
},
testFrameworkOptions: '--configuration=phpunit-db.xml',
mutators: {
'@default': true,
IdenticalEqual: false,
NotIdenticalNotEqual: false
}
}

View File

@@ -1,26 +0,0 @@
{
source: {
directories: [
'module/*/src'
]
},
timeout: 5,
logs: {
text: 'build/infection-unit/infection-log.txt',
html: 'build/infection-unit/infection-log.html',
summary: 'build/infection-unit/summary-log.txt',
debug: 'build/infection-unit/debug-log.txt',
stryker: {
report: 'develop'
}
},
tmpDir: 'build/infection-unit/temp',
phpUnit: {
configDir: '.'
},
mutators: {
'@default': true,
IdenticalEqual: false,
NotIdenticalNotEqual: false
}
}

View File

@@ -37,6 +37,9 @@ return [
Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class,
Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class,
Command\RedirectRule\ManageRedirectRulesCommand::NAME =>
Command\RedirectRule\ManageRedirectRulesCommand::class,
],
],

View File

@@ -4,19 +4,20 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;
use GeoIp2\Database\Reader;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
use Shlinkio\Shlink\Core\Domain\DomainService;
use Shlinkio\Shlink\Core\Options\TrackingOptions;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleService;
use Shlinkio\Shlink\Core\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifier;
use Shlinkio\Shlink\Core\Tag\TagService;
use Shlinkio\Shlink\Core\Visit;
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\GeoLite2ReaderFactory;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console as SymfonyCli;
use Symfony\Component\Lock\LockFactory;
@@ -33,6 +34,7 @@ return [
PhpExecutableFinder::class => InvokableFactory::class,
GeoLite\GeolocationDbUpdater::class => ConfigAbstractFactory::class,
RedirectRule\RedirectRuleHandler::class => InvokableFactory::class,
Util\ProcessRunner::class => ConfigAbstractFactory::class,
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,
@@ -66,13 +68,15 @@ return [
Command\Domain\ListDomainsCommand::class => ConfigAbstractFactory::class,
Command\Domain\DomainRedirectsCommand::class => ConfigAbstractFactory::class,
Command\Domain\GetDomainVisitsCommand::class => ConfigAbstractFactory::class,
Command\RedirectRule\ManageRedirectRulesCommand::class => ConfigAbstractFactory::class,
],
],
ConfigAbstractFactory::class => [
GeoLite\GeolocationDbUpdater::class => [
DbUpdater::class,
Reader::class,
GeoLite2ReaderFactory::class,
LOCAL_LOCK_FACTORY,
TrackingOptions::class,
],
@@ -117,6 +121,12 @@ return [
Command\Domain\DomainRedirectsCommand::class => [DomainService::class],
Command\Domain\GetDomainVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
Command\RedirectRule\ManageRedirectRulesCommand::class => [
ShortUrl\ShortUrlResolver::class,
ShortUrlRedirectRuleService::class,
RedirectRule\RedirectRuleHandler::class,
],
Command\Db\CreateDatabaseCommand::class => [
LockFactory::class,
Util\ProcessRunner::class,

View File

@@ -31,7 +31,7 @@ class DisableKeyCommand extends Command
->addArgument('apiKey', InputArgument::REQUIRED, 'The API key to disable');
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
protected function execute(InputInterface $input, OutputInterface $output): int
{
$apiKey = $input->getArgument('apiKey');
$io = new SymfonyStyle($input, $output);

View File

@@ -98,7 +98,7 @@ class GenerateKeyCommand extends Command
->setHelp($help);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
protected function execute(InputInterface $input, OutputInterface $output): int
{
$expirationDate = $input->getOption('expiration-date');

View File

@@ -29,7 +29,7 @@ class InitialApiKeyCommand extends Command
->addArgument('apiKey', InputArgument::REQUIRED, 'The initial API to create');
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
protected function execute(InputInterface $input, OutputInterface $output): int
{
$key = $input->getArgument('apiKey');
$result = $this->apiKeyService->createInitial($key);

View File

@@ -45,7 +45,7 @@ class ListKeysCommand extends Command
);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
protected function execute(InputInterface $input, OutputInterface $output): int
{
$enabledOnly = $input->getOption('enabled-only');

View File

@@ -68,7 +68,7 @@ class DomainRedirectsCommand extends Command
$input->setArgument('domain', str_contains($selectedOption, 'New domain') ? $askNewDomain() : $selectedOption);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$domainAuthority = $input->getArgument('domain');

View File

@@ -38,7 +38,7 @@ class ListDomainsCommand extends Command
);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
protected function execute(InputInterface $input, OutputInterface $output): int
{
$domains = $this->domainService->listDomains();
$showRedirects = $input->getOption('show-redirects');

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\RedirectRule;
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
use Shlinkio\Shlink\CLI\RedirectRule\RedirectRuleHandlerInterface;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
class ManageRedirectRulesCommand extends Command
{
public const NAME = 'short-url:manage-rules';
private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput;
public function __construct(
protected readonly ShortUrlResolverInterface $shortUrlResolver,
protected readonly ShortUrlRedirectRuleServiceInterface $ruleService,
protected readonly RedirectRuleHandlerInterface $ruleHandler,
) {
parent::__construct();
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
$this,
shortCodeDesc: 'The short code which rules we want to set.',
domainDesc: 'The domain for the short code.',
);
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('Set redirect rules for a short URL');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
try {
$shortUrl = $this->shortUrlResolver->resolveShortUrl($identifier);
} catch (ShortUrlNotFoundException) {
$io->error(sprintf('Short URL for %s not found', $identifier->__toString()));
return ExitCode::EXIT_FAILURE;
}
$rulesToSave = $this->ruleHandler->manageRules($io, $shortUrl, $this->ruleService->rulesForShortUrl($shortUrl));
if ($rulesToSave !== null) {
$this->ruleService->saveRulesForShortUrl($shortUrl, $rulesToSave);
$io->success('Rules properly saved');
}
return ExitCode::EXIT_SUCCESS;
}
}

View File

@@ -5,7 +5,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
@@ -71,6 +70,12 @@ class CreateShortUrlCommand extends Command
InputOption::VALUE_REQUIRED,
'If provided, this slug will be used instead of generating a short code',
)
->addOption(
'path-prefix',
'p',
InputOption::VALUE_REQUIRED,
'Prefix to prepend before the generated short code or provided custom slug',
)
->addOption(
'max-visits',
'm',
@@ -95,12 +100,6 @@ class CreateShortUrlCommand extends Command
InputOption::VALUE_REQUIRED,
'The length for generated short code (it will be ignored if --custom-slug was provided).',
)
->addOption(
'validate-url',
null,
InputOption::VALUE_NONE,
'[DEPRECATED] Makes the URL to be validated as publicly accessible.',
)
->addOption(
'crawlable',
'r',
@@ -134,7 +133,7 @@ class CreateShortUrlCommand extends Command
}
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = $this->getIO($input, $output);
$longUrl = $input->getArgument('longUrl');
@@ -145,22 +144,20 @@ class CreateShortUrlCommand extends Command
$explodeWithComma = static fn (string $tag) => explode(',', $tag);
$tags = array_unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
$customSlug = $input->getOption('custom-slug');
$maxVisits = $input->getOption('max-visits');
$shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength;
$doValidateUrl = $input->getOption('validate-url');
try {
$result = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([
ShortUrlInputFilter::LONG_URL => $longUrl,
ShortUrlInputFilter::VALID_SINCE => $input->getOption('valid-since'),
ShortUrlInputFilter::VALID_UNTIL => $input->getOption('valid-until'),
ShortUrlInputFilter::CUSTOM_SLUG => $customSlug,
ShortUrlInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null,
ShortUrlInputFilter::CUSTOM_SLUG => $input->getOption('custom-slug'),
ShortUrlInputFilter::PATH_PREFIX => $input->getOption('path-prefix'),
ShortUrlInputFilter::FIND_IF_EXISTS => $input->getOption('find-if-exists'),
ShortUrlInputFilter::DOMAIN => $input->getOption('domain'),
ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
ShortUrlInputFilter::VALIDATE_URL => $doValidateUrl,
ShortUrlInputFilter::TAGS => $tags,
ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'),
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
@@ -176,7 +173,7 @@ class CreateShortUrlCommand extends Command
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)),
]);
return ExitCode::EXIT_SUCCESS;
} catch (InvalidUrlException | NonUniqueSlugException $e) {
} catch (NonUniqueSlugException $e) {
$io->error($e->getMessage());
return ExitCode::EXIT_FAILURE;
}

View File

@@ -4,12 +4,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@@ -21,9 +21,16 @@ class DeleteShortUrlCommand extends Command
{
public const NAME = 'short-url:delete';
private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput;
public function __construct(private readonly DeleteShortUrlServiceInterface $deleteShortUrlService)
{
parent::__construct();
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
$this,
shortCodeDesc: 'The short code for the short URL to be deleted',
domainDesc: 'The domain if the short code does not belong to the default one',
);
}
protected function configure(): void
@@ -31,26 +38,19 @@ class DeleteShortUrlCommand extends Command
$this
->setName(self::NAME)
->setDescription('Deletes a short URL')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code for the short URL to be deleted')
->addOption(
'ignore-threshold',
'i',
InputOption::VALUE_NONE,
'Ignores the safety visits threshold check, which could make short URLs with many visits to be '
. 'accidentally deleted',
)
->addOption(
'domain',
'd',
InputOption::VALUE_REQUIRED,
'The domain if the short code does not belong to the default one',
);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$identifier = ShortUrlIdentifier::fromCli($input);
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
$ignoreThreshold = $input->getOption('ignore-threshold');
try {

View File

@@ -5,13 +5,11 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Command\Visit\AbstractDeleteVisitsCommand;
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlVisitsDeleterInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
@@ -20,32 +18,28 @@ class DeleteShortUrlVisitsCommand extends AbstractDeleteVisitsCommand
{
public const NAME = 'short-url:visits-delete';
private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput;
public function __construct(private readonly ShortUrlVisitsDeleterInterface $deleter)
{
parent::__construct();
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
$this,
shortCodeDesc: 'The short code for the short URL which visits will be deleted',
domainDesc: 'The domain if the short code does not belong to the default one',
);
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('Deletes visits from a short URL')
->addArgument(
'shortCode',
InputArgument::REQUIRED,
'The short code for the short URL which visits will be deleted',
)
->addOption(
'domain',
'd',
InputOption::VALUE_REQUIRED,
'The domain if the short code does not belong to the default one',
);
->setDescription('Deletes visits from a short URL');
}
protected function doExecute(InputInterface $input, SymfonyStyle $io): ?int
protected function doExecute(InputInterface $input, SymfonyStyle $io): int
{
$identifier = ShortUrlIdentifier::fromCli($input);
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
try {
$result = $this->deleter->deleteShortUrlVisits($identifier);
$io->success(sprintf('Successfully deleted %s visits', $result->affectedItems));

View File

@@ -5,14 +5,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Command\Visit\AbstractVisitsListCommand;
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\Model\VisitsParams;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -20,18 +18,23 @@ class GetShortUrlVisitsCommand extends AbstractVisitsListCommand
{
public const NAME = 'short-url:visits';
private ShortUrlIdentifierInput $shortUrlIdentifierInput;
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('Returns the detailed visits information for provided short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code which visits we want to get.')
->addOption('domain', 'd', InputOption::VALUE_REQUIRED, 'The domain for the short code.');
->setDescription('Returns the detailed visits information for provided short code');
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
$this,
shortCodeDesc: 'The short code which visits we want to get.',
domainDesc: 'The domain for the short code.',
);
}
protected function interact(InputInterface $input, OutputInterface $output): void
{
$shortCode = $input->getArgument('shortCode');
$shortCode = $this->shortUrlIdentifierInput->shortCode($input);
if (! empty($shortCode)) {
return;
}
@@ -45,7 +48,7 @@ class GetShortUrlVisitsCommand extends AbstractVisitsListCommand
protected function getVisitsPaginator(InputInterface $input, DateRange $dateRange): Paginator
{
$identifier = ShortUrlIdentifier::fromCli($input);
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
return $this->visitsHelper->visitsForShortUrl($identifier, new VisitsParams($dateRange));
}

View File

@@ -129,7 +129,7 @@ class ListShortUrlsCommand extends Command
);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
@@ -218,7 +218,7 @@ class ListShortUrlsCommand extends Command
'Short URL' => $pickProp('shortUrl'),
'Long URL' => $pickProp('longUrl'),
'Date created' => $pickProp('dateCreated'),
'Visits count' => $pickProp('visitsCount'),
'Visits count' => static fn (array $shortUrl) => $shortUrl['visitsSummary']->total,
];
if ($input->getOption('show-tags')) {
$columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']);

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