Compare commits

..

272 Commits

Author SHA1 Message Date
Alejandro Celaya
361d987f47 Merge pull request #1970 from shlinkio/develop
Release 3.7.3
2024-01-04 14:07:53 +01:00
Alejandro Celaya
6017db260a Add v3.7.3 to changelog 2024-01-04 14:02:00 +01:00
Alejandro Celaya
f9c9b3d981 Merge pull request #1969 from acelaya-forks/feature/mountable-data-dir
Feature/mountable data dir
2024-01-04 08:42:41 +01:00
Alejandro Celaya
e7b876f4e6 Update changelog 2024-01-03 19:42:33 +01:00
Alejandro Celaya
554b948775 Create data directories in docker entry point if they don't exist 2024-01-03 19:22:33 +01:00
Alejandro Celaya
9bdbb59401 Update shlinkio/shlink-testing-utils 2024-01-03 10:08:03 +01:00
Alejandro Celaya
377861c5f1 Move migrations to module/Core 2024-01-02 17:55:23 +01:00
Alejandro Celaya
26c2aaf567 Merge pull request #1963 from shlinkio/develop
Release 3.7.2
2023-12-26 16:23:49 +01:00
Alejandro Celaya
62b54ceaaf Add v3.7.2 to changelog 2023-12-26 16:16:10 +01:00
Alejandro Celaya
625eba76c7 Merge pull request #1962 from acelaya-forks/feature/disabled-qr-codes
Allow QR codes to be generated for disabled short URLs
2023-12-24 16:55:52 +01:00
Alejandro Celaya
e12bda3f42 Add API test to verify QR codes return a 404 for disabled short URLs 2023-12-24 10:37:09 +01:00
Alejandro Celaya
0f0301ae5c Update changelog 2023-12-24 10:27:25 +01:00
Alejandro Celaya
8d1776af98 Test error when short URLs cannot be resolved 2023-12-24 10:25:58 +01:00
Alejandro Celaya
c597738915 Test how URLs are resolved in QrCodeAction 2023-12-24 10:13:19 +01:00
Alejandro Celaya
639329dbe4 Update installer 2023-12-24 09:48:44 +01:00
Alejandro Celaya
92b0525b6e Update Twitter badge 2023-12-23 11:14:12 +01:00
Alejandro Celaya
06306aabd5 Allow QR codes to be generated for disabled short URLs 2023-12-22 13:29:22 +01:00
Alejandro Celaya
225905fcdb update changelog 2023-12-19 11:22:40 +01:00
Alejandro Celaya
8ca2b3c641 Merge pull request #1955 from acelaya-forks/feature/artifact-actions
Update artifact GitHub actions
2023-12-19 11:19:34 +01:00
Alejandro Celaya
ac1737492b Update artifact GitHub actions 2023-12-19 11:13:13 +01:00
Alejandro Celaya
a63075eb4c Merge pull request #1953 from shlinkio/develop
Release 3.7.1
2023-12-17 20:06:25 +01:00
Alejandro Celaya
97e9dfad67 Merge pull request #1952 from shlinkio/feature/rr-logs-improvement
Feature/rr logs improvement
2023-12-17 19:59:21 +01:00
Alejandro Celaya
17c4f13568 Set fixed versions for Shlink dependencies 2023-12-17 19:49:50 +01:00
Alejandro Celaya
3b5243689b Fine-tune RoadRunner logs to avoid too many useless info 2023-12-17 19:26:28 +01:00
Alejandro Celaya
4d28adf4a7 Merge pull request #1948 from acelaya-forks/feature/fix-postgres-import
Fix error when importing short URLs while using Postgres
2023-12-16 20:38:46 +01:00
Alejandro Celaya
1b14bb07b1 Fix error when importing short URLs while using Postgres 2023-12-16 20:22:39 +01:00
Alejandro Celaya
3a43aa4d41 Merge pull request #1942 from acelaya-forks/feature/geoip-3
Update to geolite2 v3
2023-12-07 07:52:58 +01:00
Alejandro Celaya
2340b4f601 Update to geolite2 v3 2023-12-06 21:48:54 +01:00
Alejandro Celaya
664886eddf Support laminas-diactoros 3 2023-11-30 22:10:41 +01:00
Alejandro Celaya
d3570dac0b Merge pull request #1937 from acelaya-forks/feature/remove-functional
Feature/remove functional
2023-11-30 18:53:21 +01:00
Alejandro Celaya
1854cc2f19 Remove last references to functional-php 2023-11-30 18:39:27 +01:00
Alejandro Celaya
bff4bd12ae Removed more functional-php usages 2023-11-30 14:34:44 +01:00
Alejandro Celaya
549c6605f0 Replaced usage of Functional\contians 2023-11-30 09:13:29 +01:00
Alejandro Celaya
f50263d2d9 Remove usage of Functional\map function 2023-11-29 12:34:13 +01:00
Alejandro Celaya
c80ec54508 Merge pull request #1933 from shlinkio/develop
Release 3.7.0
2023-11-25 20:22:38 +01:00
Alejandro Celaya
a91a560651 Fix typo in version contraint 2023-11-25 20:12:41 +01:00
Alejandro Celaya
a931c60230 Point to actual versions on shlink deps 2023-11-25 20:08:29 +01:00
Alejandro Celaya
479a331008 Merge pull request #1932 from acelaya-forks/feature/ssl-connections
Feature/ssl connections
2023-11-25 18:11:30 +01:00
Alejandro Celaya
5d99b1aef0 Update changelog 2023-11-25 13:08:15 +01:00
Alejandro Celaya
17e0c9176e Add support for SSL on Redis and RabbitMQ connections 2023-11-25 13:04:30 +01:00
Alejandro Celaya
48d7388bdc Merge pull request #1931 from acelaya-forks/feature/update-installer
Update to installer with runtime question
2023-11-25 10:01:19 +01:00
Alejandro Celaya
aa01c034db Update to installer with runtime question 2023-11-25 09:55:01 +01:00
Alejandro Celaya
9035161b65 Merge pull request #1928 from acelaya-forks/feature/redis-urlencoded
Allow redis credentials be URL-decoded before passing them to connection
2023-11-23 11:29:46 +01:00
Alejandro Celaya
df57ca5edb Allow redis credentials be URL-decoded before passing them to connection 2023-11-23 11:22:23 +01:00
Alejandro Celaya
0511c73cc8 Merge pull request #1926 from acelaya-forks/feature/geolite-download-warn
Print a warning when manually running visit:download-db with no license
2023-11-23 09:42:31 +01:00
Alejandro Celaya
a3554eaf74 Print a warning when manually running visit:download-db with no license 2023-11-23 09:31:02 +01:00
Alejandro Celaya
cb0bac55d2 Merge pull request #1920 from acelaya-forks/feature/matomo-integration
Feature/matomo integration
2023-11-22 18:59:45 +01:00
Alejandro Celaya
bd5d3f6897 Update changelog 2023-11-22 18:51:47 +01:00
Alejandro Celaya
5e6e386c5a Add matomo dev config 2023-11-22 18:30:03 +01:00
Alejandro Celaya
e783bdc456 Set referrer when sending visits to Matomo 2023-11-21 10:01:27 +01:00
Alejandro Celaya
316b88cea6 Add 10 second timeout to matomo requests 2023-11-21 08:34:37 +01:00
Alejandro Celaya
c03eea789c Fix LocateVisitTest 2023-11-21 08:25:58 +01:00
Alejandro Celaya
bd5d3cb6fa Create SendVisitToMatomoTest 2023-11-20 10:11:15 +01:00
Alejandro Celaya
e1f2dcc136 Create MatomoTrackerBuilderTest 2023-11-17 23:31:23 +01:00
Alejandro Celaya
5e6ebfa5a9 Update shlink-event-dispatcher 2023-11-17 09:32:07 +01:00
Alejandro Celaya
a7ed14a1c9 Enhance EnableListenerCheckerTest with support for matomo listener 2023-11-16 09:24:52 +01:00
Alejandro Celaya
f88d57b2b6 Do not dispatch async job for matomo if disabled 2023-11-15 20:02:35 +01:00
Alejandro Celaya
9dbd15bc0c Add logic to send visits to a matomo instance 2023-11-15 19:57:58 +01:00
Alejandro Celaya
0edb3e5c2c Update to installer with support for matomo 2023-11-11 20:12:39 +01:00
Alejandro Celaya
7501eca71e Update matomo container 2023-11-09 09:04:41 +01:00
Alejandro Celaya
b145d106b0 Add matomo env vars and config 2023-11-09 08:59:34 +01:00
Alejandro Celaya
b4386a3508 Add matomo container 2023-11-09 08:58:58 +01:00
Alejandro Celaya
36e2a9387d Merge pull request #1917 from acelaya-forks/feature/php-8.3-deps
Update native deps for PHP 8.3 preparation
2023-11-08 19:13:29 +01:00
Alejandro Celaya
14c68b4bbe Update native deps for PHP 8.3 preparation 2023-11-08 18:51:03 +01:00
Alejandro Celaya
d6fedaf926 Merge pull request #1913 from acelaya-forks/feature/fix-delete-multi-segment-visits
Fix short URL visits deletion when multi-segment slugs are enabled
2023-11-08 09:20:06 +01:00
Alejandro Celaya
8d35c1dde2 Fix short URL visits deletion when multi-segment slugs are enabled 2023-11-08 09:06:12 +01:00
Alejandro Celaya
85b5f760e5 Update dev swagger UI 2023-11-05 10:58:41 +01:00
Alejandro Celaya
1a4a107952 Merge pull request #1911 from acelaya-forks/feature/slug-url-chars
Feature/slug url chars
2023-11-05 10:52:46 +01:00
Alejandro Celaya
e431395a12 Update changelog 2023-11-05 10:31:51 +01:00
Alejandro Celaya
cfc3d54122 Do not allow URL reserved characters in custom slugs 2023-11-05 10:30:40 +01:00
Alejandro Celaya
d9d6d5bd9c Merge pull request #1907 from acelaya-forks/feature/php-8.3
Add support for PHP 8.3
2023-11-04 13:30:31 +01:00
Alejandro Celaya
32f465f7a6 Add PHP 8.3 to building pipeline 2023-11-04 13:15:15 +01:00
Alejandro Celaya
4cddb573a0 Ignore all platform reqs on PHP 8.3, as openswoole cannot be installed there 2023-11-04 13:03:10 +01:00
Alejandro Celaya
2cb8486bb3 Add support for PHP 8.3 2023-11-04 12:42:31 +01:00
Alejandro Celaya
2a782ab60b Merge pull request #1897 from acelaya-forks/feature/disable-health-endpoint-logs
Do not log requests to the health endpoint
2023-10-20 20:44:49 +02:00
Alejandro Celaya
5bde273d59 Fix Rest's ConfigProvider test 2023-10-20 09:42:48 +02:00
Alejandro Celaya
41e322fd47 Update changelog 2023-10-20 09:34:20 +02:00
Alejandro Celaya
55885b0f25 Do not log requests to the health endpoint 2023-10-20 09:33:29 +02:00
Alejandro Celaya
d419b9d62d Merge pull request #1891 from acelaya-forks/feature/customizable-cache-namespace
Feature/customizable cache namespace
2023-10-07 11:33:40 +02:00
Alejandro Celaya
3bdc05fbc4 Fix CliTestUtils for PHPUnit 10.4 2023-10-07 10:56:04 +02:00
Alejandro Celaya
57053d66a4 Update changelog 2023-10-06 09:21:53 +02:00
Alejandro Celaya
9d8ea0a4f6 Allow cache namespace to be customizable via env var 2023-10-06 09:19:55 +02:00
Alejandro Celaya
46354baae9 Merge pull request #1886 from acelaya-forks/feature/chronos-3
Update to chronos 3
2023-09-30 21:19:01 +02:00
Alejandro Celaya
27c48414da Update to chronos 3 2023-09-30 21:03:17 +02:00
Alejandro Celaya
25b1138000 Fix merge conflicts 2023-09-23 09:06:38 +02:00
Alejandro Celaya
4cf3bc08f9 Merge pull request #1883 from shlinkio/release/v3.6.4
Release 3.6.4
2023-09-23 08:57:10 +02:00
Alejandro Celaya
7e093a3fd8 Fix date in changelog 2023-09-23 08:41:57 +02:00
Alejandro Celaya
abecf3be02 Merge pull request #1882 from acelaya-forks/feature/create-api-key
Feature/create api key
2023-09-23 08:40:53 +02:00
Alejandro Celaya
3d9b48c5fd Create InitialApiKeyCommand cli test 2023-09-23 08:28:57 +02:00
Alejandro Celaya
ba4a66f772 Add InitialApiKeyCommand unit test 2023-09-23 08:16:22 +02:00
Alejandro Celaya
ec839183e8 Add unit test for ApiKeyService::createInitial 2023-09-23 08:01:10 +02:00
Alejandro Celaya
b0ec0601c1 Update to latest shlink-installer 2023-09-22 10:00:19 +02:00
Alejandro Celaya
637d8334f4 New CLI command to create the initial API key idempotently 2023-09-21 09:47:21 +02:00
Alejandro Celaya
6db46b50e9 Roll back change to allow creating API keys with custom value 2023-09-21 08:58:05 +02:00
Alejandro Celaya
f6b1cc7556 Test API key creation with custom key 2023-09-19 10:14:04 +02:00
Alejandro Celaya
65a0a90a51 Allow custom API keys to be created 2023-09-19 09:10:17 +02:00
Alejandro Celaya
38a7872fbf Merge pull request #1878 from acelaya-forks/feature/add-swagger-ui-dev
Add a swagger ui container for dev env
2023-09-17 12:01:05 +02:00
Alejandro Celaya
5839cc5926 Add a swagger ui container for dev env 2023-09-17 11:59:23 +02:00
Alejandro Celaya
49bd230474 Merge pull request #1874 from acelaya-forks/feature/redis-lock-namespace
Make sure locks include the same cache namespace when sent to Redis
2023-09-12 21:44:45 +02:00
Alejandro Celaya
074f2135f6 Make sure locks include the same cache namespace when sent to Redis 2023-09-12 21:20:38 +02:00
Alejandro Celaya
ef073d59ca Merge pull request #1872 from acelaya-forks/bugfix/db-commands-timeout
Fix incorrect timeout in init commands
2023-09-12 08:33:13 +02:00
Alejandro Celaya
a3b2f94339 Make sure local config is not loaded in tests 2023-09-12 08:21:34 +02:00
Alejandro Celaya
b17c576a30 Fix incorrect timeout in init commands 2023-09-11 09:07:18 +02:00
Alejandro Celaya
bc4156ca3c Merge pull request #1858 from acelaya-forks/feature/update-deps
Update dependencies
2023-08-19 12:51:02 +02:00
Alejandro Celaya
b747b8448e Update dependencies 2023-08-19 12:03:29 +02:00
Alejandro Celaya
aa4b9fc27e Replace references to docker-compose with docker compose 2023-08-03 09:10:05 +02:00
Alejandro Celaya
3f3c2c3d1e Add form config for Feature Request issues 2023-08-03 09:08:35 +02:00
Alejandro Celaya
4b49f8fb7f Use issue form for bugs 2023-07-25 08:45:24 +02:00
Alejandro Celaya
550f3b28ea Use textarea instead of markdown for main field in help-wanted discussion 2023-07-15 11:09:36 +02:00
Alejandro Celaya
6d4c232345 Merge pull request #1844 from acelaya-forks/feature/help-discussion-template
Add discussion template for 'Help wanted'
2023-07-15 10:59:51 +02:00
Alejandro Celaya
2d085ad6f4 Add discussion template for 'Help wanted' 2023-07-15 10:59:39 +02:00
Alejandro Celaya
3ea83f5cc3 Merge pull request #1836 from acelaya-forks/feature/oas-3.1
Feature/oas 3.1
2023-07-12 19:42:42 +02:00
Alejandro Celaya
b47bd0fc7a Use stable version of devizzent/cebe-php-openapi 2023-07-12 11:33:58 +02:00
Alejandro Celaya
27e90c4c26 Update changelog 2023-07-12 11:30:12 +02:00
Alejandro Celaya
ad1a846d8e Remove references to nullable in OAS 2023-07-12 11:29:44 +02:00
Alejandro Celaya
78f75a06df Updated swagger docs to v3.1, and fixed some 'required' definitions 2023-07-12 11:29:44 +02:00
Alejandro Celaya
262d714751 Add ADR for latest docker image publishing change 2023-07-09 11:31:13 +02:00
Alejandro Celaya
f71c3bba5c Merge pull request #1842 from acelaya-forks/feature/docker-build-on-tag
Build docker image only on tags
2023-07-09 09:59:25 +02:00
Alejandro Celaya
8b495064b2 Build docker image only on tags 2023-07-09 09:45:46 +02:00
Alejandro Celaya
57a36204db Merge pull request #1840 from acelaya-forks/feature/docker-no-interactive-init
Improve verbosity hint when an error occurs during docker init
2023-07-06 09:11:04 +02:00
Alejandro Celaya
7cc1722858 Improve verbosity hint when an error occurs during docker init 2023-07-05 09:58:51 +02:00
Alejandro Celaya
af50887361 Fix typo 2023-07-01 16:33:52 +02:00
Alejandro Celaya
99c8c6c8d4 Merge pull request #1832 from acelaya-forks/feature/migrations-config
Refactor cli-config file as it's currently used by doctrine migrations only
2023-06-23 22:05:24 +02:00
Alejandro Celaya
1d7c9fd553 Refactor cli-config file as it's currently used by doctrine migrations only 2023-06-23 09:16:33 +02:00
Alejandro Celaya
274c454fa4 Merge pull request #1827 from acelaya-forks/feature/question-discussion
Make sure people asking questions opens a discussion instead of an issue
2023-06-21 08:55:07 +02:00
Alejandro Celaya
453fcc4675 Make sure people asking questions opens a discussion instead of an issue 2023-06-21 08:54:03 +02:00
Alejandro Celaya
42427bfd74 Merge pull request #1824 from acelaya-forks/feature/api-coverage-fix
Update shlink-test-utils to fix coverage ID on API tests
2023-06-18 19:24:03 +02:00
Alejandro Celaya
33eedd2270 Update shlink-test-utils to fix coverage ID on API tests 2023-06-18 18:59:15 +02:00
Alejandro Celaya
edaf9e34f4 Merge pull request #1823 from acelaya-forks/feature/external-data-providers
Feature/external data providers
2023-06-18 11:06:24 +02:00
Alejandro Celaya
965325aa7c Replace traits with static classes in CLI unit tests 2023-06-18 10:51:59 +02:00
Alejandro Celaya
bdf2bbd0f1 Replace traits with external data providers in Core unit tests 2023-06-18 10:41:24 +02:00
Alejandro Celaya
dc4aab2cab Replace traits with external data providers in API tests 2023-06-18 10:36:45 +02:00
Alejandro Celaya
3b1f6c69de Merge pull request #1822 from acelaya-forks/feature/fix-installer-command-timeouts
Fix incorrect timeout in init commands
2023-06-15 19:07:32 +02:00
Alejandro Celaya
cdf5082cff Fix incorrect timeout in init commands 2023-06-15 18:53:42 +02:00
Alejandro Celaya
61686ed6ea Fix JamesIves/github-pages-deploy-action version 2023-06-14 18:27:03 +02:00
Alejandro Celaya
f63b96fd05 Fix merge conflicts 2023-06-14 18:25:09 +02:00
Alejandro Celaya
228bd83b75 Merge pull request #1818 from acelaya-forks/feature/fix-sqlite-db-creation
Fix Shlink trying to create SQLite database tables even if they already exist
2023-06-14 18:22:39 +02:00
Alejandro Celaya
a21dcb852a Fix Shlink trying to create SQLite database tables even if they already exist 2023-06-14 18:08:39 +02:00
Alejandro Celaya
6558c37b9a Fix merge conflicts 2023-06-11 20:10:29 +02:00
Alejandro Celaya
e6720cce12 Merge pull request #1814 from acelaya-forks/feature/update-test-utils
Update to latest test utils lib
2023-06-11 20:05:45 +02:00
Alejandro Celaya
22d039c550 Update to latest test utils lib 2023-06-11 19:54:33 +02:00
Alejandro Celaya
a21fcd72ce Merge pull request #1807 from acelaya-forks/feature/mercure-options
Use MercureOptions instead of raw config, where possible
2023-06-08 19:00:43 +02:00
Alejandro Celaya
058391cf06 Merge pull request #1809 from acelaya-forks/feature/fix-rr-download
Update to a shlink-installer version that fixes rr download
2023-06-08 18:59:28 +02:00
Alejandro Celaya
24e6acc6e8 Update to a shlink-installer version that fixes rr download 2023-06-08 18:47:55 +02:00
Alejandro Celaya
8e3508f28d Use MercureOptions instead of raw config, where possible 2023-06-06 20:25:14 +02:00
Alejandro Celaya
e72b424968 Fix merge conflicts 2023-06-04 09:33:16 +02:00
Alejandro Celaya
56d299a7dc Merge pull request #1804 from acelaya-forks/feature/release-3.6.1
Feature/release 3.6.1
2023-06-04 09:30:19 +02:00
Alejandro Celaya
575e6bf707 Downgrade PHPUnit to avoid infection error 2023-06-04 09:13:37 +02:00
Alejandro Celaya
e50c21440f Define default values for env vars used in rr prod config 2023-06-04 09:07:41 +02:00
Alejandro Celaya
7cff11080d Update changelog 2023-06-04 08:57:07 +02:00
Alejandro Celaya
72381f9844 Change order to create initial database to avoid permission errors 2023-06-04 08:54:08 +02:00
Alejandro Celaya
7c649e7497 Merge pull request #1801 from acelaya-forks/feature/no-run-disabled-tasks
Feature/no run disabled tasks
2023-06-03 19:19:08 +02:00
Alejandro Celaya
eff308cd43 Update changelog 2023-06-03 17:58:26 +02:00
Alejandro Celaya
bd3745118e Add logic to prevent roadrunner/openswoole jobs for tasks that will do nothing 2023-06-03 17:56:52 +02:00
Alejandro Celaya
602ebef02a Merge pull request #1800 from acelaya-forks/feature/deprecate-openswoole
Deprecate support for openswoole
2023-06-03 10:06:03 +02:00
Alejandro Celaya
9040937376 Stick with PHPUnit 10.1 until API tests coverage is fixed 2023-06-03 09:24:43 +02:00
Alejandro Celaya
a11be5b2ff Deprecate support for openswoole 2023-06-03 09:08:07 +02:00
Alejandro Celaya
6351d0b87d Merge pull request #1797 from acelaya-forks/feature/improve-new-db-check
Feature/improve new db check
2023-06-01 20:01:39 +02:00
Alejandro Celaya
fae3434393 Update changelog 2023-06-01 19:28:15 +02:00
Alejandro Celaya
4013ae87dd Change order to create initial database to avoid permission errors 2023-06-01 19:27:04 +02:00
Alejandro Celaya
cb4ba58b08 Merge pull request #1795 from acelaya-forks/feature/non-orphan-role
Feature/non orphan role
2023-05-31 09:43:18 +02:00
Alejandro Celaya
8c94452348 Fix CLI tests 2023-05-31 09:33:05 +02:00
Alejandro Celaya
ea96a00b12 Update changelog 2023-05-31 09:24:23 +02:00
Alejandro Celaya
be26dd58c3 Add API tests to cover usage of orphan visits restricted keys 2023-05-31 09:22:40 +02:00
Alejandro Celaya
eaba5edf7f Restrict interaction with orphan visits when API key has that role 2023-05-31 09:11:20 +02:00
Alejandro Celaya
12da04ef37 Add ApiKey check to tell if it has any role that is short-url restrictive 2023-05-30 09:32:44 +02:00
Alejandro Celaya
8b03532ddb Add ORPHAN_VISITS_EXCLUDED API key role 2023-05-30 09:15:35 +02:00
Alejandro Celaya
112b54ec7d Merge pull request #1793 from acelaya-forks/feature/drop-php-8.1
Drop support for PHP 8.1
2023-05-29 21:53:30 +02:00
Alejandro Celaya
ee6a8ede0a Drop support for PHP 8.1 2023-05-29 09:43:12 +02:00
Alejandro Celaya
07ce5f05a2 Add missing entry to v3.6.0 changelog 2023-05-29 09:02:59 +02:00
Alejandro Celaya
7b04016ca2 Fix version number on JamesIves/github-pages-deploy-action GitHub action 2023-05-24 08:59:21 +02:00
Alejandro Celaya
b6792d3fb8 Merge pull request #1792 from shlinkio/develop
Release 3.6.0
2023-05-24 08:46:25 +02:00
Alejandro Celaya
2f0d658432 Merge pull request #1791 from acelaya-forks/feature/fix-cpu-100
Update changelog
2023-05-23 23:25:39 +02:00
Alejandro Celaya
8c1865c3ec Update changelog 2023-05-23 23:15:59 +02:00
Alejandro Celaya
096d2098d6 Update installer 2023-05-23 18:42:50 +02:00
Alejandro Celaya
882d64ae11 Add deprecation note for ENABLE_PERIODIC_VISIT_LOCATE env var 2023-05-23 10:55:49 +02:00
Alejandro Celaya
3352bcd186 Merge pull request #1789 from acelaya-forks/feature/improved-dependency-locks
Feature/improved dependency locks
2023-05-21 18:44:10 +02:00
Alejandro Celaya
9743c1624d Update changelog 2023-05-21 18:10:08 +02:00
Alejandro Celaya
e85d59c5a4 Add locks when creating short URL dependencies, to avoid race condition 2023-05-21 18:08:17 +02:00
Alejandro Celaya
ac0ff8fb94 Merge pull request #1787 from acelaya-forks/feature/shlink-init-command
Feature/shlink init command
2023-05-21 14:44:08 +02:00
Alejandro Celaya
90f93ee4ec Update changelog 2023-05-21 14:32:00 +02:00
Alejandro Celaya
794d926e3a Update docker entry point to use new shlink-installer init command 2023-05-21 14:30:20 +02:00
Alejandro Celaya
bd41ebef9f Merge pull request #1785 from acelaya-forks/feature/non-root-support
Allow running docker container as non-root
2023-05-19 20:29:40 +02:00
Alejandro Celaya
725370704f Update changelog 2023-05-19 19:50:05 +02:00
Alejandro Celaya
f03b7689ce Allow running docker container as non-root 2023-05-19 19:48:20 +02:00
Alejandro Celaya
fb31e2a5e4 Merge pull request #1782 from acelaya-forks/feature/clear-orphan-visits
Feature/clear orphan visits
2023-05-18 09:49:31 +02:00
Alejandro Celaya
d688c6da7e Update changelog 2023-05-18 09:36:50 +02:00
Alejandro Celaya
618784dc3b Create command to delete all orphan visits 2023-05-18 09:35:42 +02:00
Alejandro Celaya
9d64d4ed1d Create abstract base class for commands deleting visits 2023-05-18 09:33:15 +02:00
Alejandro Celaya
7f02243c6c Rename short-url:delete-visits to short-url:visits-delete for consistency with other commands 2023-05-18 09:19:01 +02:00
Alejandro Celaya
3916c68126 Add DeleteOrphanVisitsTest API test 2023-05-18 09:09:44 +02:00
Alejandro Celaya
a6f0c66331 Document endpoint to delete orphan visits 2023-05-18 09:06:52 +02:00
Alejandro Celaya
bdfb220126 Create REST action to delete orphan visits 2023-05-18 09:04:28 +02:00
Alejandro Celaya
abcf2f86be Create service to delete orphan visits 2023-05-18 09:01:57 +02:00
Alejandro Celaya
a4d8ebdfc9 Create DB logic to delete orphan visits 2023-05-18 08:58:07 +02:00
Alejandro Celaya
b51c149c30 Merge pull request #1779 from acelaya-forks/feature/clear-short-url-visits
Feature/clear short url visits
2023-05-17 09:20:28 +02:00
Alejandro Celaya
39095a3098 Fix coding styles 2023-05-17 08:57:36 +02:00
Alejandro Celaya
765199727e Update changelog 2023-05-16 09:29:22 +02:00
Alejandro Celaya
c7043af853 Create DeleteShortUrlVisitsCommandTest 2023-05-16 09:26:29 +02:00
Alejandro Celaya
02a8ef7dd9 Create DeleteShortUrlVisitsCommand 2023-05-15 09:48:24 +02:00
Alejandro Celaya
6bb8c1b2f5 Rename CLI Option namespace to Input 2023-05-15 09:02:23 +02:00
Alejandro Celaya
3cf253fd0f Document short URLs visits deletion endpoint 2023-05-14 18:25:27 +02:00
Alejandro Celaya
0365728337 Create DeleteShortUrlVisitsTest 2023-05-14 13:35:15 +02:00
Alejandro Celaya
b8143a5bb4 Create VisitDeleterRepositoryTest 2023-05-14 13:04:45 +02:00
Alejandro Celaya
531a19dde9 Refactor short URL visits deletion layers 2023-05-14 13:04:17 +02:00
Alejandro Celaya
69ff7de481 Create ShortUrlVisitsDeleterTest 2023-05-14 12:32:54 +02:00
Alejandro Celaya
ffc0555c7c Create DeleteShortUrlVisitsActionTest 2023-05-14 12:15:35 +02:00
Alejandro Celaya
84a7981dfa Create REST action to delete short URL visits 2023-05-14 12:00:08 +02:00
Alejandro Celaya
2573c2bf98 Update roadrunner config 2023-05-14 11:56:49 +02:00
Alejandro Celaya
3b4c1501f3 Set platforms to be used for openswoole docker image 2023-05-07 17:13:26 +02:00
Alejandro Celaya
e836bedecc Merge pull request #1775 from acelaya-forks/feature/default-roadrunner
Feature/default roadrunner
2023-05-07 13:34:53 +02:00
Alejandro Celaya
a797b74a70 Standardize logger for all Shlink execution contexts 2023-05-07 13:18:19 +02:00
Alejandro Celaya
ab497403ca Merge pull request #1773 from acelaya-forks/feature/rr-friendly-installer
Update shlink-installer
2023-05-06 18:07:57 +02:00
Alejandro Celaya
d4dea9a1d2 Update shlink-installer 2023-05-06 10:12:42 +02:00
Alejandro Celaya
28d93ea5e0 Update changelog 2023-05-03 08:59:47 +02:00
Alejandro Celaya
e6a31b16ed Switch to roadrunner as default docker runtime 2023-05-03 08:59:09 +02:00
Alejandro Celaya
9553192281 Merge pull request #1766 from acelaya-forks/feature/rr-cli-2.5
Update to rr-cli 2.5, and do not generate config
2023-05-02 20:01:51 +02:00
Alejandro Celaya
74069f2d24 Skip API tests fetching Twitter during CI 2023-05-02 19:51:37 +02:00
Alejandro Celaya
b4b00a57c1 Update chrome user agent used for anti-bots 2023-05-02 19:40:23 +02:00
Alejandro Celaya
a516ef691d Update to rr-cli 2.5, and do not generate config 2023-05-02 08:43:14 +02:00
Alejandro Celaya
e80b7448f5 Merge pull request #1761 from acelaya-forks/feature/null-default-domain
Feature/null default domain
2023-04-23 15:57:02 +02:00
Alejandro Celaya
f129544f83 Update changelog 2023-04-23 15:22:40 +02:00
Alejandro Celaya
9fa291a32f Update shlink-common 2023-04-23 15:20:33 +02:00
Alejandro Celaya
d06e92ffc2 Created CLI test for short URL importing 2023-04-23 13:26:59 +02:00
Alejandro Celaya
1b83344995 Create CLI test checking default domain is ignored even if explicitly provided 2023-04-23 11:20:54 +02:00
Alejandro Celaya
cf49393ef2 Add --show-domain flag to list short URLs command 2023-04-23 11:19:05 +02:00
Alejandro Celaya
f2ecbceae9 Update changelog 2023-04-22 19:46:28 +02:00
Alejandro Celaya
c582eba753 Make sure short URL domain is resolved as null when default one is provided 2023-04-22 19:44:04 +02:00
Alejandro Celaya
de86b62cdd Merge pull request #1759 from acelaya-forks/feature/fix-docker-build
Fix docker image build
2023-04-20 09:00:39 +02:00
Alejandro Celaya
73150471e9 Fix docker image build 2023-04-19 18:57:35 +02:00
Alejandro Celaya
ec751f4ac2 Merge pull request #1758 from acelaya-forks/feature/roadrunner-2023
Feature/roadrunner 2023
2023-04-19 08:11:18 +02:00
Alejandro Celaya
e652166289 Update changelog 2023-04-18 23:24:21 +02:00
Alejandro Celaya
a671d555cb Update to roadrunner 2023 2023-04-18 23:22:48 +02:00
Alejandro Celaya
6240554f4c Merge pull request #1757 from acelaya-forks/feature/shlink-json
Migrate to shlinkio/shlink-json
2023-04-18 23:14:11 +02:00
Alejandro Celaya
4ee9c9bbe3 Migrate to shlinkio/shlink-json 2023-04-18 23:04:58 +02:00
Alejandro Celaya
c830439085 Merge pull request #1752 from acelaya-forks/feature/phpunit-10.1
Update phpunit configs to fulfil v10.1
2023-04-14 09:55:38 +02:00
Alejandro Celaya
f2196583c8 Update phpunit configs to fulfil v10.1 2023-04-14 09:44:01 +02:00
Alejandro Celaya
3dbca2115c Merge pull request #1751 from acelaya-forks/feature/openswoole-22
Add support for openswoole 22
2023-04-14 09:16:47 +02:00
Alejandro Celaya
b45d8de27d Ignore openswoole dep on roadrunner tests CI 2023-04-14 09:02:17 +02:00
Alejandro Celaya
3ba46bbbfa Add support for openswoole 22 2023-04-14 08:58:54 +02:00
Alejandro Celaya
06f3f0c86c Merge pull request #1750 from acelaya-forks/feature/update-delete-artifacts
Update to geekyeggo/delete-artifact@2
2023-04-13 08:58:24 +02:00
Alejandro Celaya
06f07e3e40 Update to geekyeggo/delete-artifact@2 2023-04-12 19:13:35 +02:00
Alejandro Celaya
740740b8c6 Update to latest JamesIves/github-pages-deploy-action 2023-04-12 19:11:06 +02:00
Alejandro Celaya
b6ed39b18b Merge pull request #1749 from shlinkio/develop
Release 3.5.4
2023-04-12 19:03:25 +02:00
Alejandro Celaya
958c4704f8 Merge pull request #1748 from acelaya-forks/feature/create-error
Feature/create error
2023-04-12 18:52:18 +02:00
Alejandro Celaya
ef075fb0ce Fix test when CLI output viewport is too narrow 2023-04-12 18:36:28 +02:00
Alejandro Celaya
556520583a Update changelog 2023-04-12 18:31:57 +02:00
Alejandro Celaya
399c56a097 Print warning when trying to create short URL from CLI on openswoole in verbose mode 2023-04-12 18:30:02 +02:00
Alejandro Celaya
f078d95588 Capture error on real-time update when creating short URL 2023-04-12 09:25:01 +02:00
Alejandro Celaya
33911afcd6 Merge pull request #1744 from acelaya-forks/feature/regression-fix
Feature/regression fix
2023-04-11 19:13:08 +02:00
Alejandro Celaya
ae8d31e83f Add test case for deeplink long URLs 2023-04-11 17:24:38 +02:00
Alejandro Celaya
72c4052012 Be less restrictive when validating long URLs 2023-04-10 18:05:57 +02:00
Alejandro Celaya
f713a1fa7e Merge pull request #1737 from shlinkio/develop
Release 3.5.3
2023-03-31 22:07:51 +02:00
Alejandro Celaya
62488ac4e5 Merge pull request #1739 from acelaya-forks/feature/import-memory-leak
Feature/import memory leak
2023-03-31 10:00:36 +02:00
Alejandro Celaya
ab4c6e5fca Update changelog 2023-03-31 09:48:08 +02:00
Alejandro Celaya
26f4a969c9 Fix memory leak when importing big amounts of visits 2023-03-31 09:46:05 +02:00
Alejandro Celaya
703965915d Merge pull request #1736 from acelaya-forks/feature/lcobucci-jwt-5
Update to latest shlink-common
2023-03-30 18:45:39 +02:00
Alejandro Celaya
24e38a3cf9 Update to latest shlink-common 2023-03-30 18:33:53 +02:00
Alejandro Celaya
b12cfaedf3 Merge pull request #1730 from acelaya-forks/feature/validate-uris
Feature/validate uris
2023-03-25 13:29:36 +01:00
Alejandro Celaya
71807e698c Update changelog 2023-03-25 11:23:01 +01:00
Alejandro Celaya
1d155298c1 Fix API tests 2023-03-25 11:23:01 +01:00
Alejandro Celaya
4dfc5ae681 Fix DB tests 2023-03-25 11:23:01 +01:00
Alejandro Celaya
26f237069c Fixed unit tests 2023-03-25 11:23:01 +01:00
Alejandro Celaya
b6e1c65c4c Enforce a schema to be provided when short URLs are created 2023-03-25 11:23:00 +01:00
Alejandro Celaya
11f94b8306 Merge pull request #1723 from acelaya-forks/feature/tags-list-performance-join-tags
Feature/tags list performance join tags
2023-03-16 21:57:43 +01:00
Alejandro Celaya
01bcedef7a Simplify how ordering field is resolved in tags list 2023-03-04 11:54:30 +01:00
Alejandro Celaya
e51384fcc0 Reduce duplicated logic when checking if an API key is admin 2023-03-04 10:22:46 +01:00
Alejandro Celaya
83c53c8b2e Add correct index on visits potential_bot column 2023-03-04 09:51:14 +01:00
Alejandro Celaya
1afe08caed Simplify how limits are applied to tags query 2023-03-04 09:50:38 +01:00
Alejandro Celaya
7289833928 Move join on short URLs to tags sub-query 2023-03-03 12:10:41 +01:00
Alejandro Celaya
f4d10df0f3 Delete no longer used spec file 2023-02-27 09:28:27 +01:00
Alejandro Celaya
652b0df054 Use native query builders for all queries/sub-queries in tags list 2023-02-27 09:21:11 +01:00
347 changed files with 5191 additions and 1979 deletions

View File

@@ -0,0 +1,51 @@
title: 'Help wanted'
body:
- type: input
validations:
required: true
attributes:
label: Shlink version
placeholder: x.y.z
- type: input
validations:
required: true
attributes:
label: PHP version
placeholder: x.y.z
- type: dropdown
validations:
required: true
attributes:
label: How do you serve Shlink
options:
- Self-hosted Apache
- Self-hosted nginx
- Self-hosted openswoole
- Self-hosted RoadRunner
- Openswoole Docker image
- RoadRunner Docker image
- Other (explain in summary)
- type: dropdown
validations:
required: true
attributes:
label: Database engine
options:
- MySQL
- MariaDB
- PostgreSQL
- MicrosoftSQL
- SQLite
- type: input
validations:
required: true
attributes:
label: Database version
placeholder: x.y.z
- type: textarea
validations:
required: true
attributes:
label: Summary
value: '<!-- Describe your issue, question or request here. -->'

View File

@@ -1,38 +0,0 @@
---
name: Bug report
about: Something on shlink is broken or not working as documented?
labels: bug
---
<!--
Before opening an issue, just take into account that this is a completely free of charge and open source project.
I'm always happy to help and provide support, but some understanding will be expected.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
You may also be asked to provide tests or ways to reproduce reported bugs.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
-->
#### How Shlink is set up
* Shlink Version: x.y.z
* PHP Version: x.y.z
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Self-hosted RoadRunner|Openswoole Docker image|RoadRunner Docker image
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
#### Summary
<!-- Provide a summary describing the problem you are experiencing. -->
#### Current behavior
<!-- How is it actually behaving (and it shouldn't)? -->
#### Expected behavior
<!-- How did you expect it to behave? -->
#### How to reproduce
<!-- Provide steps to reproduce the bug. -->

64
.github/ISSUE_TEMPLATE/Bug.yml vendored Normal file
View File

@@ -0,0 +1,64 @@
name: Bug report
description: Something on Shlink is broken or not working as documented?
labels: ['bug']
body:
- type: input
validations:
required: true
attributes:
label: Shlink version
placeholder: x.y.z
- type: input
validations:
required: true
attributes:
label: PHP version
placeholder: x.y.z
- type: dropdown
validations:
required: true
attributes:
label: How do you serve Shlink
options:
- Self-hosted Apache
- Self-hosted nginx
- Self-hosted openswoole
- Self-hosted RoadRunner
- Openswoole Docker image
- RoadRunner Docker image
- Other (explain in summary)
- type: dropdown
validations:
required: true
attributes:
label: Database engine
options:
- MySQL
- MariaDB
- PostgreSQL
- MicrosoftSQL
- SQLite
- type: input
validations:
required: true
attributes:
label: Database version
placeholder: x.y.z
- type: textarea
validations:
required: true
attributes:
label: Current behavior
value: '<!-- How is it actually behaving (and it should not)? -->'
- type: textarea
validations:
required: true
attributes:
label: Expected behavior
value: '<!-- How did you expect it to behave? -->'
- type: textarea
validations:
required: true
attributes:
label: How to reproduce
value: '<!-- Provide steps to reproduce the bug. -->'

View File

@@ -1,19 +0,0 @@
---
name: Feature request
about: Do you find shlink is missing some important feature that would make it more useful?
labels: feature
---
<!--
Before opening an issue, just take into account that this is a completely free of charge and open source project.
I'm always happy to help and provide support, but some understanding will be expected.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
You may also be asked to provide tests or ways to reproduce reported bugs.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
-->
#### Summary
<!-- Describe the new feature you would like to request. -->

View File

@@ -0,0 +1,16 @@
name: Feature request
description: Do you find Shlink is missing some important feature that would make it more useful?
labels: ['feature']
body:
- type: textarea
validations:
required: true
attributes:
label: Summary
value: '<!-- Describe the new feature you would like to request. -->'
- type: textarea
validations:
required: true
attributes:
label: Use case
value: '<!-- Explain why do you think this feature would be useful, and what problems would it help to solve. -->'

View File

@@ -1,26 +0,0 @@
---
name: Question - Support
about: Do you have a problem setting up or using shlink?
labels: question
---
<!--
Before opening an issue, just take into account that this is a completely free of charge and open source project.
I'm always happy to help and provide support, but some understanding will be expected.
I do this in my own free time, so expect some delays when implementing new features and fixing bugs, and don't take it personally if an issue gets eventually closed.
You may also be asked to provide tests or ways to reproduce reported bugs.
Try to be polite, and understand it is impossible for an OSS project to cover all use cases.
With that said, please fill in the information requested next. More information might be requested next (like logs or system configs).
-->
#### How Shlink is set up
* Shlink Version: x.y.z
* PHP Version: x.y.z
* How do you serve Shlink: Self-hosted Apache|Self-hosted nginx|Self-hosted openswoole|Self-hosted RoadRunner|Openswoole Docker image|RoadRunner Docker image
* Database engine used: MySQL|MariaDB|PostgreSQL|MicrosoftSQL|SQLite (x.y.z)
#### Summary
<!-- Describe the issue you are facing here. -->

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Question - Support
about: Do you need help setting up or using Shlink?
url: https://github.com/shlinkio/shlink/discussions/new?category=help-wanted

View File

@@ -43,5 +43,5 @@ runs:
ini-values: pcov.directory=module
- name: Install dependencies
if: ${{ inputs.install-deps == 'yes' }}
run: composer install --no-interaction --prefer-dist
run: composer install --no-interaction --prefer-dist ${{ inputs.php-version == '8.3' && '--ignore-platform-reqs' || '' }}
shell: bash

View File

@@ -13,11 +13,12 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1', '8.2']
php-version: ['8.2', '8.3']
continue-on-error: ${{ matrix.php-version == '8.3' }}
env:
LC_ALL: C
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Install MSSQL ODBC
if: ${{ inputs.platform == 'ms' }}
run: sudo ./data/infra/ci/install-ms-odbc.sh
@@ -27,7 +28,7 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.12.1, pdo_sqlsrv-5.10.1
php-extensions: openswoole-22.1.0, pdo_sqlsrv-5.11.1
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
- name: Create test database
if: ${{ inputs.platform == 'ms' }}
@@ -35,8 +36,8 @@ jobs:
- name: Run tests
run: composer test:db:${{ inputs.platform }}
- name: Upload code coverage
uses: actions/upload-artifact@v3
if: ${{ matrix.php-version == '8.1' && inputs.platform == 'sqlite:ci' }}
uses: actions/upload-artifact@v4
if: ${{ matrix.php-version == '8.2' && inputs.platform == 'sqlite:ci' }}
with:
name: coverage-db
path: |

View File

@@ -10,5 +10,5 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- run: docker build -t shlink-docker-image:temp .

View File

@@ -13,15 +13,16 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1', '8.2']
php-version: ['8.2', '8.3']
continue-on-error: ${{ matrix.php-version == '8.3' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.12.1
php-extensions: openswoole-22.1.0
extensions-cache-key: mutation-tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: coverage-${{ inputs.test-group }}
path: build

View File

@@ -13,9 +13,10 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1', '8.2']
php-version: ['8.2', '8.3']
continue-on-error: ${{ matrix.php-version == '8.3' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Start postgres database server
if: ${{ inputs.test-group == 'api' }}
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
@@ -25,11 +26,11 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.12.1
php-extensions: openswoole-22.1.0
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ inputs.test-group }}
- run: composer test:${{ inputs.test-group }}:ci
- uses: actions/upload-artifact@v3
if: ${{ matrix.php-version == '8.1' }}
- uses: actions/upload-artifact@v4
if: ${{ matrix.php-version == '8.2' }}
with:
name: coverage-${{ inputs.test-group }}
path: |

View File

@@ -29,14 +29,14 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1']
php-version: ['8.2']
command: ['cs', 'stan', 'swagger:validate']
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.12.1
php-extensions: openswoole-22.1.0
extensions-cache-key: tests-extensions-${{ matrix.php-version }}-${{ matrix.command }}
- run: composer ${{ matrix.command }}
@@ -59,18 +59,19 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1', '8.2']
php-version: ['8.2', '8.3']
continue-on-error: ${{ matrix.php-version == '8.3' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # rr get-binary picks this env automatically
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
- run: composer install --no-interaction --prefer-dist
- run: ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr
- 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:
@@ -135,17 +136,17 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1']
php-version: ['8.2']
steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: pcov
ini-values: pcov.directory=module
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
path: build
- run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov
@@ -168,10 +169,7 @@ jobs:
- upload-coverage
runs-on: ubuntu-22.04
steps:
- uses: geekyeggo/delete-artifact@v1
- uses: geekyeggo/delete-artifact@v2
with:
name: |
coverage-unit
coverage-db
coverage-api
coverage-cli
coverage-*

View File

@@ -2,34 +2,33 @@ name: Build and publish docker image
on:
push:
branches:
- develop
paths-ignore:
- 'LICENSE'
- '.*'
- '*.md'
- '*.xml'
- '*.yml*'
- '*.json5'
- '*.neon'
tags:
- 'v*'
jobs:
build-openswoole:
build-image:
strategy:
matrix:
include:
- runtime: 'rr'
platforms: 'linux/arm64/v8,linux/amd64'
- 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:
image-name: shlinkio/shlink
version-arg-name: SHLINK_VERSION
build-roadrunner:
uses: shlinkio/github-actions/.github/workflows/docker-build-and-publish.yml@main
secrets: inherit
with:
image-name: shlinkio/shlink
version-arg-name: SHLINK_VERSION
platforms: 'linux/arm64/v8,linux/amd64'
tags-suffix: roadrunner
platforms: ${{ matrix.platforms }}
tags-suffix: ${{ matrix.tag-suffix }}
extra-build-args: |
SHLINK_RUNTIME=rr
SHLINK_RUNTIME=${{ matrix.runtime }}
SHLINK_USER_ID=${{ matrix.user-id && matrix.user-id || 'root' }}

View File

@@ -10,21 +10,21 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1', '8.2']
php-version: ['8.2', '8.3']
swoole: ['yes', 'no']
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.12.1
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
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
path: build
@@ -33,8 +33,8 @@ jobs:
needs: ['build']
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
path: build
- name: Publish release with assets
@@ -49,11 +49,7 @@ jobs:
delete-artifacts:
needs: ['publish']
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1', '8.2']
swoole: ['yes', 'no']
steps:
- uses: geekyeggo/delete-artifact@v1
- uses: geekyeggo/delete-artifact@v2
with:
name: dist-files-${{ matrix.php-version }}-${{ matrix.swoole }}
name: dist-files-*

View File

@@ -10,9 +10,9 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php-version: ['8.1']
php-version: ['8.2']
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Determine version
id: determine_version
run: echo "version=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
@@ -20,13 +20,13 @@ jobs:
- uses: './.github/actions/ci-setup'
with:
php-version: ${{ matrix.php-version }}
php-extensions: openswoole-4.12.1
php-extensions: openswoole-22.1.0
extensions-cache-key: publish-swagger-spec-extensions-${{ matrix.php-version }}
- run: composer swagger:inline
- run: mkdir ${{ steps.determine_version.outputs.version }}
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json
- name: Publish spec
uses: JamesIves/github-pages-deploy-action@4.1.7
uses: JamesIves/github-pages-deploy-action@v4
with:
token: ${{ secrets.OAS_PUBLISH_TOKEN }}
repository-name: 'shlinkio/shlink-open-api-specs'

2
.gitignore vendored
View File

@@ -1,5 +1,4 @@
.idea
bin/.rr.*
bin/rr
config/roadrunner/.pid
build
@@ -10,6 +9,7 @@ vendor/
data/database.sqlite
data/shlink-tests.db
data/GeoLite2-City.*
data/infra/matomo
docs/swagger-ui*
docs/mercure.html
docker-compose.override.yml

View File

@@ -4,6 +4,270 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
## [3.7.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*
### Removed
* *Nothing*
### Fixed
* [#1967](https://github.com/shlinkio/shlink/issues/1967) Allow an empty dir to be mounted in `data` when using the docker image.
## [3.7.2] - 2023-12-26
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1960](https://github.com/shlinkio/shlink/issues/1960) Allow QR codes to be optionally resolved even when corresponding short URL is not enabled.
## [3.7.1] - 2023-12-17
### Added
* *Nothing*
### Changed
* Remove dependency on functional-php library
* [#1939](https://github.com/shlinkio/shlink/issues/1939) Fine-tune RoadRunner logs to avoid too many useless info.
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1947](https://github.com/shlinkio/shlink/issues/1947) Fix error when importing short URLs while using Postgres.
## [3.7.0] - 2023-11-25
### Added
* [#1798](https://github.com/shlinkio/shlink/issues/1798) Experimental support to send visits to an external Matomo instance.
* [#1780](https://github.com/shlinkio/shlink/issues/1780) Add new `NO_ORPHAN_VISITS` API key role.
Keys with this role will always get `0` when fetching orphan visits.
When trying to delete orphan visits the result will also be `0` and no visits will actually get deleted.
* [#1879](https://github.com/shlinkio/shlink/issues/1879) Cache namespace can now be customized via config option or `CACHE_NAMESPACE` env var.
This is important if you are running multiple Shlink instance on the same server, or they share the same Redis instance (even more so if they are on different versions).
* [#1905](https://github.com/shlinkio/shlink/issues/1905) Add support for PHP 8.3.
* [#1927](https://github.com/shlinkio/shlink/issues/1927) Allow redis credentials be URL-decoded before passing them to connection.
* [#1834](https://github.com/shlinkio/shlink/issues/1834) Add support for redis encrypted connections using SSL/TLS.
Encryption should work out of the box if servers schema is set tp `tls` or `rediss`, including support for self-signed certificates.
This has been tested with AWS ElasticCache using in-transit encryption, and with Digital Ocean Redis database.
* [#1906](https://github.com/shlinkio/shlink/issues/1906) Add support for RabbitMQ encrypted connections using SSL/TLS.
In order to enable SLL, you need to pass `RABBITMQ_USE_SSL=true` or the corresponding config option.
Connections using self-signed certificates should work out of the box.
This has been tested with AWS RabbitMQ using in-transit encryption, and with CloudAMQP.
### Changed
* [#1799](https://github.com/shlinkio/shlink/issues/1799) RoadRunner/openswoole jobs are not run anymore for tasks that are actually disabled.
For example, if you did not enable RabbitMQ real-time updates, instead of triggering a job that ends immediately, the job will not even be enqueued.
* [#1835](https://github.com/shlinkio/shlink/issues/1835) Docker image is now built only when a release is tagged, and new tags are included, for minor and major versions.
* [#1055](https://github.com/shlinkio/shlink/issues/1055) Update OAS definition to v3.1.
* [#1885](https://github.com/shlinkio/shlink/issues/1885) Update to chronos 3.0.
* [#1896](https://github.com/shlinkio/shlink/issues/1896) Requests to health endpoint are no longer logged.
* [#1877](https://github.com/shlinkio/shlink/issues/1877) Print a warning when manually running `visit:download-db` command and a GeoLite2 license was not provided.
### Deprecated
* [#1783](https://github.com/shlinkio/shlink/issues/1783) Deprecated support for openswoole. RoadRunner is the best replacement, with the same capabilities, but much easier and convenient to install and manage.
### Removed
* [#1790](https://github.com/shlinkio/shlink/issues/1790) Drop support for PHP 8.1.
### Fixed
* [#1819](https://github.com/shlinkio/shlink/issues/1819) Fix incorrect timeout when running DB commands during Shlink start-up.
* [#1901](https://github.com/shlinkio/shlink/issues/1901) Do not allow short URLs with custom slugs containing URL-reserved characters, as they will not work at all afterward.
* [#1900](https://github.com/shlinkio/shlink/issues/1900) Fix short URL visits deletion when multi-segment slugs are enabled.
## [3.6.4] - 2023-09-23
### Added
* *Nothing*
### Changed
* [#1866](https://github.com/shlinkio/shlink/issues/1866) The `INITIAL_API_KEY` env var is now only relevant for the official docker image.
Going forward, new non-docker Shlink installations provisioned with env vars that also wish to provide an initial API key, should do it by using the `vendor/bin/shlink-installer init --initial-api-key=%SOME_KEY%` command, instead of using `INITIAL_API_KEY`.
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1819](https://github.com/shlinkio/shlink/issues/1819) Fix incorrect timeout when running DB commands during Shlink start-up.
* [#1870](https://github.com/shlinkio/shlink/issues/1870) Make sure shared locks include the cache prefix when using Redis.
* [#1866](https://github.com/shlinkio/shlink/issues/1866) Fix error when starting docker image with `INITIAL_API_KEY` env var.
## [3.6.3] - 2023-06-14
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1817](https://github.com/shlinkio/shlink/issues/1817) Fix Shlink trying to create SQLite database tables even if they already exist.
## [3.6.2] - 2023-06-08
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1808](https://github.com/shlinkio/shlink/issues/1808) Fix `rr` binary downloading during Shlink update.
## [3.6.1] - 2023-06-04
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1413](https://github.com/shlinkio/shlink/issues/1413) Fix error when creating initial DB in Postgres in a cluster where a default `postgres` db does not exist or the credentials do not grant permissions to connect.
* [#1803](https://github.com/shlinkio/shlink/issues/1803) Fix default RoadRunner port when not using docker image.
## [3.6.0] - 2023-05-24
### Added
* [#1148](https://github.com/shlinkio/shlink/issues/1148) Add support to delete short URL visits.
This can be done via `DELETE /short-urls/{shortCode}/visits` REST endpoint or via `short-url:visits-delete` console command.
The CLI command includes a warning and requires the user to confirm before proceeding.
* [#1681](https://github.com/shlinkio/shlink/issues/1681) Add support to delete orphan visits.
This can be done via `DELETE /visits/orphan` REST endpoint or via `visit:orphan-delete` console command.
The CLI command includes a warning and requires the user to confirm before proceeding.
* [#1753](https://github.com/shlinkio/shlink/issues/1753) Add a new `vendor/bin/shlink-installer init` command that can be used to automate Shlink installations.
This command can create the initial database, update it, create proxies, clean cache, download initial GeoLite db files, etc
The official docker image also uses it on its entry point script.
* [#1656](https://github.com/shlinkio/shlink/issues/1656) Add support for openswoole 22
* [#1784](https://github.com/shlinkio/shlink/issues/1784) Add new docker tag where the container runs as a non-root user.
* [#953](https://github.com/shlinkio/shlink/issues/953) Add locks that prevent errors on duplicated keys when creating short URLs in parallel that depend on the same new tag or domain.
### Changed
* [#1755](https://github.com/shlinkio/shlink/issues/1755) Update to roadrunner 2023
* [#1745](https://github.com/shlinkio/shlink/issues/1745) Roadrunner is now the default docker runtime.
There are now three different docker images published:
* Versions without suffix (like `3.6.0`) will contain the default runtime, whichever it is.
* Versions with `-roadrunner` suffix (like `3.6.0-roadrunner`) will always use roadrunner as the runtime, even if default one changes in the future.
* Versions with `-openswoole` suffix (like `3.6.0-openswoole`) will always use openswoole as the runtime, even if default one changes in the future.
### Deprecated
* Deprecated `ENABLE_PERIODIC_VISIT_LOCATE` env var. Use an external mechanism to automate visit locations.
### Removed
* *Nothing*
### Fixed
* [#1760](https://github.com/shlinkio/shlink/issues/1760) Fix domain not being set to null when importing short URLs with default domain.
* [#953](https://github.com/shlinkio/shlink/issues/953) Fix duplicated key errors and short URL creation failing when creating short URLs in parallel that depend on the same new tag or domain.
* [#1741](https://github.com/shlinkio/shlink/issues/1741) Fix randomly using 100% CPU in task workers when trying to download GeoLite DB files.
* Fix Shlink trying to connect to RabbitMQ even if configuration set to not connect.
## [3.5.4] - 2023-04-12
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1742](https://github.com/shlinkio/shlink/issues/1742) Fix URLs using schemas which do not contain `//`, like `mailto:`, to no longer be considered valid.
* [#1743](https://github.com/shlinkio/shlink/issues/1743) Fix Error when trying to create short URLs from CLI on an openswoole context.
Unfortunately the reason are real-time updates do not work with openswoole when outside an openswoole request, so the feature has been disabled for that context.
## [3.5.3] - 2023-03-31
### Added
* *Nothing*
### Changed
* *Nothing*
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* [#1715](https://github.com/shlinkio/shlink/issues/1715) Fix short URL creation/edition allowing long URLs without schema. Now a validation error is thrown.
* [#1537](https://github.com/shlinkio/shlink/issues/1537) Fix incorrect list of tags being returned for some author-only API keys.
* [#1738](https://github.com/shlinkio/shlink/issues/1738) Fix memory leak when importing short URLs with many visits.
## [3.5.2] - 2023-02-16
### Added
* *Nothing*

View File

@@ -6,9 +6,9 @@ You will also see how to ensure the code fulfills the expected code checks, and
## System dependencies
The project provides all its dependencies as docker containers through a docker-compose configuration.
The project provides all its dependencies as docker containers through a `docker compose` configuration.
Because of this, the only actual dependencies are [docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/).
Because of this, the only actual dependencies are [docker](https://docs.docker.com/get-docker/) and [docker compose](https://docs.docker.com/compose/install/).
## Setting up the project
@@ -21,7 +21,7 @@ Then you will have to follow these steps:
For example the `common.local.php.dist` file should be copied as `common.local.php`.
* Copy the file `docker-compose.override.yml.dist` by also removing the `dist` extension.
* Start-up the project by running `docker-compose up`.
* Start-up the project by running `docker compose up`.
The first time this command is run, it will create several containers that are used during development, so it may take some time.
@@ -31,7 +31,7 @@ Then you will have to follow these steps:
* Run `./indocker bin/cli db:migrate` to get database migrations up to date.
* Run `./indocker bin/cli api-key:generate` to get your first API key generated.
Once you finish this, you will have the project exposed in ports `8000` through nginx+php-fpm and `8080` through openswoole.
Once you finish this, you will have the project exposed in ports `8800` through RoadRunner, `8080` through openswoole and `8000` through nginx+php-fpm.
> Note: The `indocker` shell script is a helper tool used to run commands inside the main docker container.
@@ -46,17 +46,18 @@ This is a simplified version of the project structure:
```
shlink
├── bin
── cli
── cli
│ └── [...]
├── config
│ ├── autoload
│ ├── params
│ ├── config.php
── container.php
── container.php
│ └── [...]
├── data
│ ├── cache
│ ├── locks
│ ├── log
│ ├── migrations
│ └── proxies
├── docs
│ ├── adr
@@ -67,18 +68,19 @@ shlink
│ ├── Core
│ └── Rest
├── public
│ └── [...]
├── composer.json
└── README.md
```
The purposes of every folder are:
* `bin`: It contains the CLI tools. The `cli` one is the main entry point to run shlink from the command line.
* `bin`: It contains the CLI tools. The `cli` one is the main entry point to run Shlink from the command line.
* `config`: Contains application-wide configurations, which are later merged with the ones provided by every module.
* `data`: Common runtime-generated git-ignored assets, like logs, caches, etc.
* `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 openswoole.
* `public`: Few assets (like `favicon.ico` or `robots.txt`) and the web entry point are stored here. This web entry point is not used when serving the app with RoadRunner or openswoole.
## Project tests
@@ -94,7 +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 openswoole, and test it from the outside by interacting with the REST API.
* **API tests**: These are E2E tests that spin up an instance of the app with RoadRunner or openswoole, and test it from the outside by interacting with the REST API.
These are the best tests to catch regressions, and to verify everything behaves as expected.
@@ -125,6 +127,12 @@ Depending on the kind of contribution, maybe not all kinds of tests are needed,
* Run `./indocker composer infect:test` to run both unit and database tests (over sqlite) and then apply mutations to them with [infection](https://infection.github.io/).
* Run `./indocker composer ci` to run all previous commands together, parallelizing non-conflicting tasks as much as possible.
## Testing endpoints
The project provides a Swagger UI container for dev envs, which can be accessed in http://localhost:8005.
It will automatically load the contents of `docs/swagger`, so you can make any updates and they will get reflected.
## Pull request process
**Important!**: Before starting to work on a pull request, make sure you always [open an issue](https://github.com/shlinkio/shlink/issues/new/choose) first.

View File

@@ -2,13 +2,16 @@ FROM php:8.2-alpine3.17 as base
ARG SHLINK_VERSION=latest
ENV SHLINK_VERSION ${SHLINK_VERSION}
ARG SHLINK_RUNTIME=openswoole
ARG SHLINK_RUNTIME=rr
ENV SHLINK_RUNTIME ${SHLINK_RUNTIME}
ENV OPENSWOOLE_VERSION 4.12.1
ENV PDO_SQLSRV_VERSION 5.10.1
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 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"
ENV LC_ALL 'C'
WORKDIR /etc/shlink
@@ -26,6 +29,7 @@ RUN \
# Install openswoole and sqlsrv driver for x86_64 builds
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then \
# Openswoole is deprecated. Remove in v4.0.0
pecl install openswoole-${OPENSWOOLE_VERSION} && \
docker-php-ext-enable openswoole ; \
fi; \
@@ -43,11 +47,13 @@ FROM base as builder
COPY . .
COPY --from=composer:2 /usr/bin/composer ./composer.phar
RUN apk add --no-cache git && \
php composer.phar install --no-dev --prefer-dist --optimize-autoloader --no-progress --no-interaction && \
# 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 \
php composer.phar remove spiral/roadrunner spiral/roadrunner-jobs --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interactionc ; \
elif [ $SHLINK_RUNTIME == 'rr' ]; then \
php composer.phar remove mezzio/mezzio-swoole --with-all-dependencies --update-no-dev --optimize-autoloader --no-progress --no-interaction ; \
# 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 clear-cache && \
rm -r docker composer.* && \
@@ -58,10 +64,10 @@ RUN apk add --no-cache git && \
FROM base
LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
COPY --from=builder /etc/shlink .
COPY --from=builder --chown=${SHLINK_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 --location bin/ && chmod +x bin/rr ; \
php ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; \
fi;
# Expose default port
@@ -72,14 +78,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/
# Change the ownership of /etc/shlink/data to be writable, then change the user to non-root
# FIXME Disabled for now, as it conflicts with ENABLE_PERIODIC_VISIT_LOCATE, which is used to configure a cron as root.
# Ref: https://github.com/shlinkio/shlink/issues/1132
#RUN chown 1001 /etc/shlink/data
#RUN chown 1001 /etc/shlink/data/locks
#RUN chown 1001 /etc/shlink/data/proxies
#RUN chown 1001 /etc/shlink/data/cache
#RUN chown 1001 /etc/shlink/data/log
#USER 1001
USER ${SHLINK_USER_ID}
ENTRYPOINT ["/bin/sh", "./docker-entrypoint.sh"]

View File

@@ -6,7 +6,7 @@
[![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=twitter&color=blue)](https://twitter.com/shlinkio)
[![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)
[![Paypal donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=aaaaaa)](https://slnk.to/donate)
@@ -32,11 +32,11 @@ You can learn how to use the official docker image by reading [the docs](https:/
The idea is that you can just generate a container using the image and provide the custom config via env vars.
## Self hosted
## Self-hosted
First, make sure the host where you are going to run shlink fulfills these requirements:
* PHP 8.1 or 8.2
* PHP 8.2 or 8.3
* The next PHP extensions: json, curl, pdo, intl, gd and gmp/bcmath.
* apcu extension is recommended if you don't plan to use openswoole.
* xml extension is required if you want to generate QR codes in svg format.

View File

@@ -2,7 +2,7 @@
export APP_ENV=test
export TEST_ENV=api
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}"
export TEST_RUNTIME="${TEST_RUNTIME:-"openswoole"}" # Openswoole is deprecated. Remove in v4.0.0
export DB_DRIVER="${DB_DRIVER:-"postgres"}"
export GENERATE_COVERAGE="${GENERATE_COVERAGE:-"no"}"

View File

@@ -10,6 +10,7 @@ fi
version=$1
noSwoole=$2
phpVersion=$(php -r 'echo PHP_MAJOR_VERSION . "." . PHP_MINOR_VERSION;')
# Openswoole is deprecated. Remove in v4.0.0
[[ $noSwoole ]] && swooleSuffix="" || swooleSuffix="_openswoole"
distId="shlink${version}_php${phpVersion}${swooleSuffix}_dist"
builtContent="./build/${distId}"
@@ -30,7 +31,8 @@ cd "${builtContent}"
# Install dependencies
echo "Installing dependencies with $composerBin..."
composerFlags="--optimize-autoloader --no-progress --no-interaction"
# Deprecated. Do not ignore PHP platform req for Shlink v4.0.0
composerFlags="--optimize-autoloader --no-progress --no-interaction --ignore-platform-req=php+"
${composerBin} self-update
${composerBin} install --no-dev --prefer-dist $composerFlags
@@ -38,15 +40,16 @@ 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 --with-all-dependencies --update-no-dev $composerFlags
${composerBin} remove spiral/roadrunner spiral/roadrunner-jobs spiral/roadrunner-cli spiral/roadrunner-http --with-all-dependencies --update-no-dev $composerFlags
fi
# Delete development files
echo 'Deleting dev files...'
rm composer.*
# Update shlink version in config
# Update Shlink version in config
sed -i "s/%SHLINK_VERSION%/${version}/g" config/autoload/app_options.global.php
# Compressing file

View File

@@ -12,70 +12,75 @@
}
],
"require": {
"php": "^8.1",
"php": "^8.2",
"ext-curl": "*",
"ext-gd": "*",
"ext-json": "*",
"ext-pdo": "*",
"akrabat/ip-address-middleware": "^2.1",
"cakephp/chronos": "^2.3",
"doctrine/migrations": "^3.5",
"doctrine/orm": "^2.14",
"endroid/qr-code": "^4.7",
"geoip2/geoip2": "^2.13",
"cakephp/chronos": "^3.0.2",
"doctrine/migrations": "^3.6",
"doctrine/orm": "^2.16",
"endroid/qr-code": "^4.8",
"friendsofphp/proxy-manager-lts": "^1.0",
"geoip2/geoip2": "^3.0",
"guzzlehttp/guzzle": "^7.5",
"happyr/doctrine-specification": "^2.0",
"jaybizzle/crawler-detect": "^1.2.112",
"jaybizzle/crawler-detect": "^1.2.116",
"laminas/laminas-config": "^3.8",
"laminas/laminas-config-aggregator": "^1.13",
"laminas/laminas-diactoros": "^2.24",
"laminas/laminas-inputfilter": "^2.24",
"laminas/laminas-servicemanager": "^3.20",
"laminas/laminas-stdlib": "^3.16",
"lcobucci/jwt": "^4.3",
"laminas/laminas-diactoros": "^3.3",
"laminas/laminas-inputfilter": "^2.27",
"laminas/laminas-servicemanager": "^3.21",
"laminas/laminas-stdlib": "^3.17",
"league/uri": "^6.8",
"lstrojny/functional-php": "^1.17",
"mezzio/mezzio": "^3.15",
"mezzio/mezzio-fastroute": "^3.8",
"mezzio/mezzio-problem-details": "^1.11",
"mezzio/mezzio-swoole": "^4.6",
"matomo/matomo-php-tracker": "^3.2",
"mezzio/mezzio": "^3.17",
"mezzio/mezzio-fastroute": "^3.10",
"mezzio/mezzio-problem-details": "^1.13",
"mezzio/mezzio-swoole": "^4.7",
"mlocati/ip-lib": "^1.18",
"mobiledetect/mobiledetectlib": "^3.74",
"ocramius/proxy-manager": "^2.14",
"pagerfanta/core": "^3.7",
"mobiledetect/mobiledetectlib": "^4.8",
"pagerfanta/core": "^3.8",
"php-middleware/request-id": "^4.1",
"pugx/shortid-php": "^1.1",
"ramsey/uuid": "^4.7",
"shlinkio/shlink-common": "^5.3.1",
"shlinkio/shlink-config": "^2.4",
"shlinkio/shlink-event-dispatcher": "^2.6",
"shlinkio/shlink-importer": "^5.0",
"shlinkio/shlink-installer": "^8.3",
"shlinkio/shlink-ip-geolocation": "^3.2",
"spiral/roadrunner": "^2.12",
"spiral/roadrunner-jobs": "^2.7",
"symfony/console": "^6.2",
"symfony/filesystem": "^6.2",
"symfony/lock": "^6.2",
"symfony/process": "^6.2",
"symfony/string": "^6.2"
"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/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"
},
"require-dev": {
"cebe/php-openapi": "^1.7",
"devizzent/cebe-php-openapi": "^1.0.1",
"devster/ubench": "^2.1",
"infection/infection": "^0.26.19",
"openswoole/ide-helper": "~4.11.5",
"phpstan/phpstan": "^1.9",
"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.2",
"phpunit/php-code-coverage": "^10.0",
"phpunit/phpunit": "^10.0",
"phpstan/phpstan-symfony": "^1.3",
"phpunit/php-code-coverage": "^10.1",
"phpunit/phpunit": "^10.4",
"roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.3.0",
"shlinkio/shlink-test-utils": "^3.5",
"symfony/var-dumper": "^6.2",
"veewee/composer-run-parallel": "^1.2"
"shlinkio/shlink-test-utils": "^3.8.1",
"symfony/var-dumper": "^6.3",
"veewee/composer-run-parallel": "^1.3"
},
"conflict": {
"symfony/var-exporter": ">=6.3.9,<=6.4.0"
},
"autoload": {
"psr-4": {
@@ -85,6 +90,7 @@
},
"files": [
"config/constants.php",
"module/Core/functions/array-utils.php",
"module/Core/functions/functions.php"
]
},
@@ -108,9 +114,9 @@
"@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"
],
"cs": "phpcs",
"cs": "phpcs -s",
"cs:fix": "phpcbf",
"stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/test* module/*/config config docker/config data/migrations --level=8",
"stan": "APP_ENV=test php vendor/bin/phpstan analyse module/*/src module/*/test* module/*/config module/*/migrations config docker/config --level=8",
"test": [
"@parallel test:unit test:db",
"@parallel test:api test:cli"
@@ -135,7 +141,7 @@
"infect:ci:base": "infection --threads=max --only-covered --skip-initial-tests",
"infect:ci:unit": "@infect:ci:base --coverage=build/coverage-unit --min-msi=80",
"infect:ci:db": "@infect:ci:base --coverage=build/coverage-db --min-msi=95 --configuration=infection-db.json5",
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=80 --configuration=infection-api.json5",
"infect:ci:api": "@infect:ci:base --coverage=build/coverage-api --min-msi=95 --configuration=infection-api.json5",
"infect:ci:cli": "@infect:ci:base --coverage=build/coverage-cli --min-msi=90 --configuration=infection-cli.json5",
"infect:ci": "@parallel infect:ci:unit infect:ci:db infect:ci:api infect:ci:cli",
"infect:test": [
@@ -146,6 +152,10 @@
"@test:unit:ci",
"@infect:ci:unit"
],
"infect:test:db": [
"@test:db:sqlite:ci",
"@infect:ci:db"
],
"infect:test:api": [
"@test:api:ci",
"@infect:ci:api"

View File

@@ -11,12 +11,13 @@ return (static function (): array {
'redis' => [
'servers' => $redisServers,
'sentinel_service' => EnvVars::REDIS_SENTINEL_SERVICE->loadFromEnv(),
'decode_credentials' => (bool) EnvVars::REDIS_DECODE_CREDENTIALS->loadFromEnv(false),
],
];
return [
'cache' => [
'namespace' => 'Shlink',
'namespace' => EnvVars::CACHE_NAMESPACE->loadFromEnv('Shlink'),
...$cacheRedisBlock,
],
'redis' => $redis,

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
use GuzzleHttp\Client;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Mezzio\Application;
use Mezzio\Container;
use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\ServerRequestFactoryInterface;
@@ -20,7 +21,7 @@ return [
],
'delegators' => [
Mezzio\Application::class => [
Application::class => [
Container\ApplicationConfigInjectionDelegator::class,
],
],

View File

@@ -5,11 +5,11 @@ declare(strict_types=1);
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
use Shlinkio\Shlink\Core\Config\EnvVars;
use function Functional\contains;
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
return (static function (): array {
$driver = EnvVars::DB_DRIVER->loadFromEnv();
$isMysqlCompatible = contains(['maria', 'mysql'], $driver);
$isMysqlCompatible = contains($driver, ['maria', 'mysql']);
$resolveDriver = static fn () => match ($driver) {
'postgres' => 'pdo_pgsql',

View File

@@ -15,6 +15,14 @@ return [
// 'dbname' => 'shlink_foo',
'charset' => 'utf8mb4',
// MariaDB
// 'user' => 'root',
// 'password' => 'root',
// 'driver' => 'pdo_mysql',
// 'host' => 'shlink_db_maria',
// 'dbname' => 'shlink_foo',
// 'charset' => 'utf8mb4',
// Postgres
// 'user' => 'postgres',
// 'password' => 'root',

View File

@@ -11,6 +11,7 @@ return [
'installer' => [
'enabled_options' => [
Option\Server\RuntimeConfigOption::class,
Option\Database\DatabaseDriverConfigOption::class,
Option\Database\DatabaseNameConfigOption::class,
Option\Database\DatabaseHostConfigOption::class,
@@ -28,9 +29,11 @@ return [
Option\Visit\VisitsThresholdConfigOption::class,
Option\BasePathConfigOption::class,
Option\TimezoneConfigOption::class,
Option\Cache\CacheNamespaceConfigOption::class,
Option\Worker\TaskWorkerNumConfigOption::class,
Option\Worker\WebWorkerNumConfigOption::class,
Option\Redis\RedisServersConfigOption::class,
Option\Redis\RedisDecodeCredentialsConfigOption::class,
Option\Redis\RedisSentinelServiceConfigOption::class,
Option\Redis\RedisPubSubConfigOption::class,
Option\UrlShortener\ShortCodeLengthOption::class,
@@ -59,12 +62,18 @@ return [
Option\QrCode\DefaultFormatConfigOption::class,
Option\QrCode\DefaultErrorCorrectionConfigOption::class,
Option\QrCode\DefaultRoundBlockSizeConfigOption::class,
Option\QrCode\EnabledForDisabledShortUrlsConfigOption::class,
Option\RabbitMq\RabbitMqEnabledConfigOption::class,
Option\RabbitMq\RabbitMqHostConfigOption::class,
Option\RabbitMq\RabbitMqUseSslConfigOption::class,
Option\RabbitMq\RabbitMqPortConfigOption::class,
Option\RabbitMq\RabbitMqUserConfigOption::class,
Option\RabbitMq\RabbitMqPasswordConfigOption::class,
Option\RabbitMq\RabbitMqVhostConfigOption::class,
Option\Matomo\MatomoEnabledConfigOption::class,
Option\Matomo\MatomoBaseUrlConfigOption::class,
Option\Matomo\MatomoSiteIdConfigOption::class,
Option\Matomo\MatomoApiTokenConfigOption::class,
],
'installation_commands' => [
@@ -86,6 +95,9 @@ return [
InstallationCommand::API_KEY_GENERATE->value => [
'command' => 'bin/cli ' . Command\Api\GenerateKeyCommand::NAME,
],
InstallationCommand::API_KEY_CREATE->value => [
'command' => 'bin/cli ' . Command\Api\InitialApiKeyCommand::NAME,
],
],
],

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Shlinkio\Shlink\Common\Cache\RedisFactory;
use Shlinkio\Shlink\Common\Lock\NamespacedStore;
use Shlinkio\Shlink\Common\Logger\LoggerAwareDelegatorFactory;
use Shlinkio\Shlink\Core\Config\EnvVars;
use Symfony\Component\Lock;
@@ -22,11 +23,12 @@ return [
Lock\Store\RedisStore::class => ConfigAbstractFactory::class,
Lock\LockFactory::class => ConfigAbstractFactory::class,
LOCAL_LOCK_FACTORY => ConfigAbstractFactory::class,
NamespacedStore::class => ConfigAbstractFactory::class,
],
'aliases' => [
'lock_store' => EnvVars::REDIS_SERVERS->existsInEnv() ? 'redis_lock_store' : 'local_lock_store',
'redis_lock_store' => Lock\Store\RedisStore::class,
'redis_lock_store' => NamespacedStore::class,
'local_lock_store' => Lock\Store\FlockStore::class,
],
'delegators' => [
@@ -39,6 +41,8 @@ return [
ConfigAbstractFactory::class => [
Lock\Store\FlockStore::class => ['config.locks.locks_dir'],
Lock\Store\RedisStore::class => [RedisFactory::SERVICE_NAME],
NamespacedStore::class => [Lock\Store\RedisStore::class, 'config.cache.namespace'],
Lock\LockFactory::class => ['lock_store'],
LOCAL_LOCK_FACTORY => ['local_lock_store'],
],

View File

@@ -4,51 +4,64 @@ declare(strict_types=1);
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;
$common = [
'level' => Level::Info->value,
'processors' => [RequestId\MonologProcessor::class],
'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%',
];
use function Shlinkio\Shlink\Config\runningInRoadRunner;
return [
return (static function (): array {
$common = [
'level' => Level::Info->value,
'processors' => [RequestId\MonologProcessor::class],
'line_format' => '[%datetime%] [%extra.request_id%] %channel%.%level_name% - %message%',
];
'logger' => [
'Shlink' => [
'type' => LoggerType::FILE->value,
...$common,
],
'Access' => [
'type' => LoggerType::STREAM->value,
...$common,
],
],
return [
'dependencies' => [
'factories' => [
'Logger_Shlink' => [LoggerFactory::class, 'Shlink'],
'Logger_Access' => [LoggerFactory::class, 'Access'],
],
'aliases' => [
'logger' => 'Logger_Shlink',
Logger::class => 'Logger_Shlink',
LoggerInterface::class => 'Logger_Shlink',
],
],
'mezzio-swoole' => [
'swoole-http-server' => [
'logger' => [
'logger-name' => 'Logger_Access',
'format' => '%u "%r" %>s %B',
'logger' => [
'Shlink' => [
'type' => LoggerType::FILE->value,
...$common,
],
'Access' => [
'type' => LoggerType::STREAM->value,
'destination' => 'php://stderr',
'add_new_line' => ! runningInRoadRunner(),
...$common,
],
],
],
];
'dependencies' => [
'factories' => [
'Logger_Shlink' => [LoggerFactory::class, 'Shlink'],
'Logger_Access' => [LoggerFactory::class, 'Access'],
NullLogger::class => InvokableFactory::class,
],
'aliases' => [
'logger' => 'Logger_Shlink',
Logger::class => 'Logger_Shlink',
LoggerInterface::class => 'Logger_Shlink',
AccessLogMiddleware::LOGGER_SERVICE_NAME => 'Logger_Access',
],
],
// 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,16 +5,12 @@ declare(strict_types=1);
use Monolog\Level;
use Shlinkio\Shlink\Common\Logger\LoggerType;
use function Shlinkio\Shlink\Config\runningInOpenswoole;
$logToStream = runningInOpenswoole();
return [
'logger' => [
'Shlink' => [
// For openswoole, send logs as stream
'type' => $logToStream ? LoggerType::STREAM->value : LoggerType::FILE->value,
'type' => LoggerType::STREAM->value,
'destination' => 'php://stderr',
'level' => Level::Debug->value,
],
],

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
return [
'matomo' => [
'enabled' => (bool) EnvVars::MATOMO_ENABLED->loadFromEnv(false),
'base_url' => EnvVars::MATOMO_BASE_URL->loadFromEnv(),
'site_id' => EnvVars::MATOMO_SITE_ID->loadFromEnv(),
'api_token' => EnvVars::MATOMO_API_TOKEN->loadFromEnv(),
],
];

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* Dev matomo instance needs to be manually configured once before enabling the configuration below.
*
* 1. Go to http://localhost:8003 and follow the installation instructions.
* 2. Open data/infra/matomo/config/config.ini.php and replace `trusted_hosts[] = "localhost"` with
* `trusted_hosts[] = "localhost:8003"` (see https://github.com/matomo-org/matomo/issues/9549)
* 3. Go to http://localhost:8003/index.php?module=SitesManager&action=index and paste the ID for the site you just
* created into the `site_id` field below.
* 4. Go to http://localhost:8003/index.php?module=UsersManager&action=userSecurity, scroll down, click
* "Create new token" and once generated, paste the token into the `api_token` field below.
*/
return [
'matomo' => [
// 'enabled' => true,
// 'base_url' => 'http://shlink_matomo',
// 'site_id' => '...',
// 'api_token' => '...',
],
];

View File

@@ -9,6 +9,7 @@ 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;
return [
@@ -16,6 +17,7 @@ return [
'middleware_pipeline' => [
'error-handler' => [
'middleware' => [
AccessLogMiddleware::class,
ContentLengthMiddleware::class,
RequestIdMiddleware::class,
ErrorHandler::class,

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
use Shlinkio\Shlink\Core\Config\EnvVars;
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;
use const Shlinkio\Shlink\DEFAULT_QR_CODE_MARGIN;
@@ -22,6 +23,9 @@ return [
'round_block_size' => (bool) EnvVars::DEFAULT_QR_CODE_ROUND_BLOCK_SIZE->loadFromEnv(
DEFAULT_QR_CODE_ROUND_BLOCK_SIZE,
),
'enabled_for_disabled_short_urls' => (bool) EnvVars::QR_CODE_FOR_DISABLED_SHORT_URLS->loadFromEnv(
DEFAULT_QR_CODE_ENABLED_FOR_DISABLED_SHORT_URLS,
),
],
];

View File

@@ -9,6 +9,7 @@ return [
'rabbitmq' => [
'enabled' => (bool) EnvVars::RABBITMQ_ENABLED->loadFromEnv(false),
'host' => EnvVars::RABBITMQ_HOST->loadFromEnv(),
'use_ssl' => (bool) EnvVars::RABBITMQ_USE_SSL->loadFromEnv(false),
'port' => (int) EnvVars::RABBITMQ_PORT->loadFromEnv('5672'),
'user' => EnvVars::RABBITMQ_USER->loadFromEnv(),
'password' => EnvVars::RABBITMQ_PASSWORD->loadFromEnv(),

View File

@@ -11,7 +11,9 @@ return [
'base_path' => EnvVars::BASE_PATH->loadFromEnv(''),
'fastroute' => [
FastRouteRouter::CONFIG_CACHE_ENABLED => true,
// Disabling config cache for cli, ensures it's never used for openswoole/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

@@ -32,12 +32,16 @@ return (static function (): array {
...ConfigProvider::applyRoutesPrefix([
Action\HealthAction::getRouteDef(),
// Visits
// Visits.
// These routes must go first, as they have a more specific path, otherwise, when multi-segment slugs
// are enabled, routes with a less-specific path might match first
Action\Visit\ShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
Action\ShortUrl\DeleteShortUrlVisitsAction::getRouteDef([$dropDomainMiddleware]),
Action\Visit\TagVisitsAction::getRouteDef(),
Action\Visit\DomainVisitsAction::getRouteDef(),
Action\Visit\GlobalVisitsAction::getRouteDef(),
Action\Visit\OrphanVisitsAction::getRouteDef(),
Action\Visit\DeleteOrphanVisitsAction::getRouteDef(),
Action\Visit\NonOrphanVisitsAction::getRouteDef(),
// Short URLs

View File

@@ -2,11 +2,26 @@
declare(strict_types=1);
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager;
use Doctrine\Migrations\Configuration\Migration\ConfigurationArray;
use Doctrine\Migrations\DependencyFactory;
// This file is currently used by doctrine migrations only
return (static function () {
/** @var EntityManager $em */
$migrationsConfig = [
'migrations_paths' => [
'ShlinkMigrations' => 'module/Core/migrations',
],
'table_storage' => [
'table_name' => 'migrations',
],
'custom_template' => 'data/migrations_template.txt',
];
$em = include __DIR__ . '/entity-manager.php';
return ConsoleRunner::createHelperSet($em);
return DependencyFactory::fromEntityManager(
new ConfigurationArray($migrationsConfig),
new ExistingEntityManager($em),
);
})();

View File

@@ -22,34 +22,39 @@ use const PHP_SAPI;
$isTestEnv = env('APP_ENV') === 'test';
$enableSwoole = PHP_SAPI === 'cli' && openswooleIsInstalled() && ! runningInRoadRunner();
return (new ConfigAggregator\ConfigAggregator([
! $isTestEnv
? new EnvVarLoaderProvider('config/params/generated_config.php', enumValues(Core\Config\EnvVars::class))
: new ConfigAggregator\ArrayProvider([]),
Mezzio\ConfigProvider::class,
Mezzio\Router\ConfigProvider::class,
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
$enableSwoole && class_exists(Swoole\ConfigProvider::class)
? Swoole\ConfigProvider::class
: new ConfigAggregator\ArrayProvider([]),
ProblemDetails\ConfigProvider::class,
Diactoros\ConfigProvider::class,
Common\ConfigProvider::class,
Config\ConfigProvider::class,
Importer\ConfigProvider::class,
IpGeolocation\ConfigProvider::class,
EventDispatcher\ConfigProvider::class,
Core\ConfigProvider::class,
CLI\ConfigProvider::class,
Rest\ConfigProvider::class,
new ConfigAggregator\PhpFileProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
$isTestEnv
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
: new ConfigAggregator\ArrayProvider([]),
// Routes have to be loaded last
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
], 'data/cache/app_config.php', [
Core\Config\PostProcessor\BasePathPrefixer::class,
Core\Config\PostProcessor\MultiSegmentSlugProcessor::class,
Core\Config\PostProcessor\ShortUrlMethodsProcessor::class,
]))->getMergedConfig();
return (new ConfigAggregator\ConfigAggregator(
providers: [
! $isTestEnv
? new EnvVarLoaderProvider('config/params/generated_config.php', enumValues(Core\Config\EnvVars::class))
: new ConfigAggregator\ArrayProvider([]),
Mezzio\ConfigProvider::class,
Mezzio\Router\ConfigProvider::class,
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
$enableSwoole && class_exists(Swoole\ConfigProvider::class)
? Swoole\ConfigProvider::class
: new ConfigAggregator\ArrayProvider([]),
ProblemDetails\ConfigProvider::class,
Diactoros\ConfigProvider::class,
Common\ConfigProvider::class,
Config\ConfigProvider::class,
Importer\ConfigProvider::class,
IpGeolocation\ConfigProvider::class,
EventDispatcher\ConfigProvider::class,
Core\ConfigProvider::class,
CLI\ConfigProvider::class,
Rest\ConfigProvider::class,
new ConfigAggregator\PhpFileProvider('config/autoload/{,*.}global.php'),
// Local config should not be loaded during tests, whereas test config should be loaded ONLY during tests
new ConfigAggregator\PhpFileProvider(
$isTestEnv ? 'config/test/*.global.php' : 'config/autoload/{,*.}local.php',
),
// Routes have to be loaded last
new ConfigAggregator\PhpFileProvider('config/autoload/routes.config.php'),
],
cachedConfigFile: 'data/cache/app_config.php',
postProcessors: [
Core\Config\PostProcessor\BasePathPrefixer::class,
Core\Config\PostProcessor\MultiSegmentSlugProcessor::class,
Core\Config\PostProcessor\ShortUrlMethodsProcessor::class,
],
))->getMergedConfig();

View File

@@ -13,9 +13,12 @@ const DEFAULT_REDIRECT_STATUS_CODE = RedirectStatus::STATUS_302; // Deprecated.
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
const LOOSE_URI_MATCHER = '/(.+)\:(.+)/i'; // Matches anything starting with a schema.
const DEFAULT_QR_CODE_SIZE = 300;
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;

View File

@@ -12,6 +12,17 @@ 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()));
@@ -21,7 +32,6 @@ if (! class_exists(LOCAL_LOCK_FACTORY)) {
class_alias(Lock\LockFactory::class, LOCAL_LOCK_FACTORY);
}
// Build container
return (static function (): ServiceManager {
$config = require __DIR__ . '/config.php';
$container = new ServiceManager($config['dependencies']);

View File

@@ -1,4 +1,4 @@
version: '2.7'
version: '3'
rpc:
listen: tcp://127.0.0.1:6001
@@ -14,10 +14,12 @@ http:
forbid: ['.php', '.htaccess']
pool:
num_workers: 1
debug: true
jobs:
pool:
num_workers: 1
debug: true
timeout: 300
consume: ['shlink']
pipelines:
@@ -31,19 +33,10 @@ logs:
mode: development
channels:
http:
level: debug
mode: 'off' # Disable logging as Shlink handles it internally
server:
level: debug
level: info
metrics:
level: debug
reload:
interval: 1s
patterns: ['.php']
services:
http:
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
recursive: true
jobs:
dirs: ['../../bin', '../../config', '../../data/migrations', '../../module', '../../vendor']
recursive: true
level: debug

View File

@@ -1,4 +1,4 @@
version: '2.7'
version: '3'
rpc:
listen: tcp://127.0.0.1:6001
@@ -7,18 +7,18 @@ server:
command: 'php -dopcache.enable_cli=1 -dopcache.validate_timestamps=0 ../../bin/roadrunner-worker.php'
http:
address: '0.0.0.0:${PORT}'
address: '0.0.0.0:${PORT:-8080}'
middleware: ['static']
static:
dir: '../../public'
forbid: ['.php', '.htaccess']
pool:
num_workers: ${WEB_WORKER_NUM}
num_workers: ${WEB_WORKER_NUM:-0}
jobs:
timeout: 300 # 5 minutes
pool:
num_workers: ${TASK_WORKER_NUM}
num_workers: ${TASK_WORKER_NUM:-0}
consume: ['shlink']
pipelines:
shlink:
@@ -31,6 +31,8 @@ logs:
mode: production
channels:
http:
level: info # Log all http requests, set to info to disable
mode: 'off' # Disable logging as Shlink handles it internally
server:
level: debug # Everything written to worker stderr is logged
level: info
jobs:
level: debug

View File

@@ -29,10 +29,10 @@ register_shutdown_function(function () use ($httpClient): void {
});
$testHelper->createTestDb(
['bin/cli', 'db:create'],
['bin/cli', 'db:migrate'],
['bin/doctrine', 'orm:schema-tool:drop'],
['bin/doctrine', 'dbal:run-sql'],
createDbCommand: ['bin/cli', 'db:create'],
migrateDbCommand: ['bin/cli', 'db:migrate'],
dropSchemaCommand: ['bin/doctrine', 'orm:schema-tool:drop'],
runSqlCommand: ['bin/doctrine', 'dbal:run-sql'],
);
ApiTest\ApiTestCase::setApiClient($httpClient);
ApiTest\ApiTestCase::setSeedFixturesCallback(fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []));

View File

@@ -9,9 +9,9 @@ use Psr\Container\ContainerInterface;
/** @var ContainerInterface $container */
$container = require __DIR__ . '/../container.php';
$container->get(Helper\TestHelper::class)->createTestDb(
['bin/cli', 'db:create'],
['bin/cli', 'db:migrate'],
['bin/doctrine', 'orm:schema-tool:drop'],
['bin/doctrine', 'dbal:run-sql'],
createDbCommand: ['bin/cli', 'db:create'],
migrateDbCommand: ['bin/cli', 'db:migrate'],
dropSchemaCommand: ['bin/doctrine', 'orm:schema-tool:drop'],
runSqlCommand: ['bin/doctrine', 'dbal:run-sql'],
);
DbTest\DatabaseTestCase::setEntityManager($container->get('em'));

View File

@@ -28,9 +28,9 @@ use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use function file_exists;
use function Functional\contains;
use function Laminas\Stratigility\middleware;
use function Shlinkio\Shlink\Config\env;
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
use function sprintf;
use function sys_get_temp_dir;
@@ -41,7 +41,7 @@ $isApiTest = env('TEST_ENV') === 'api';
$isCliTest = env('TEST_ENV') === 'cli';
$isE2eTest = $isApiTest || $isCliTest;
$coverageType = env('GENERATE_COVERAGE');
$generateCoverage = contains(['yes', 'pretty'], $coverageType);
$generateCoverage = contains($coverageType, ['yes', 'pretty']);
$coverage = null;
if ($isE2eTest && $generateCoverage) {
@@ -121,6 +121,7 @@ $buildTestLoggerConfig = static fn (string $filename) => [
'level' => Level::Debug->value,
'type' => LoggerType::STREAM->value,
'destination' => sprintf('data/log/api-tests/%s', $filename),
'add_new_line' => true,
];
return [

View File

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

View File

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

View File

@@ -2,7 +2,7 @@ FROM php:8.2-alpine3.17
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.21
ENV PDO_SQLSRV_VERSION 5.10.1
ENV PDO_SQLSRV_VERSION 5.11.1
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1
@@ -71,6 +71,6 @@ CMD \
# Install dependencies if the vendor dir does not exist
if [[ ! -d "./vendor" ]]; then /usr/local/bin/composer install ; fi && \
# Download roadrunner binary
if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --no-interaction --location bin/ && chmod +x bin/rr ; fi && \
if [[ ! -f "./bin/rr" ]]; then ./vendor/bin/rr get --no-interaction --no-config --location bin/ && chmod +x bin/rr ; fi && \
# This forces the app to be started every second until the exit code is 0
until ./bin/rr serve -c config/roadrunner/.rr.dev.yml; do sleep 1 ; done

View File

@@ -3,8 +3,8 @@ MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.21
ENV INOTIFY_VERSION 3.0.0
ENV OPENSWOOLE_VERSION 4.12.1
ENV PDO_SQLSRV_VERSION 5.10.1
ENV OPENSWOOLE_VERSION 22.1.0
ENV PDO_SQLSRV_VERSION 5.11.1
ENV MS_ODBC_DOWNLOAD 'b/9/f/b9f3cce4-3925-46d4-9f46-da08869c6486'
ENV MS_ODBC_SQL_VERSION 18_18.1.1.1

View File

@@ -3,7 +3,7 @@ version: '3'
services:
shlink_nginx:
container_name: shlink_nginx
image: nginx:1.19.6-alpine
image: nginx:1.25-alpine
ports:
- "8000:80"
volumes:
@@ -33,6 +33,7 @@ services:
- shlink_mercure
- shlink_mercure_proxy
- shlink_rabbitmq
- shlink_matomo
environment:
LC_ALL: C
extra_hosts:
@@ -40,7 +41,7 @@ services:
shlink_swoole_proxy:
container_name: shlink_swoole_proxy
image: nginx:1.19.6-alpine
image: nginx:1.25-alpine
ports:
- "8002:80"
volumes:
@@ -70,6 +71,7 @@ services:
- shlink_mercure
- shlink_mercure_proxy
- shlink_rabbitmq
- shlink_matomo
environment:
LC_ALL: C
extra_hosts:
@@ -95,6 +97,7 @@ services:
- shlink_mercure
- shlink_mercure_proxy
- shlink_rabbitmq
- shlink_matomo
environment:
LC_ALL: C
extra_hosts:
@@ -164,7 +167,7 @@ services:
shlink_mercure_proxy:
container_name: shlink_mercure_proxy
image: nginx:1.19.6-alpine
image: nginx:1.25-alpine
ports:
- "8001:80"
volumes:
@@ -175,7 +178,7 @@ services:
shlink_mercure:
container_name: shlink_mercure
image: dunglas/mercure:v0.14
image: dunglas/mercure:v0.15
ports:
- "3080:80"
environment:
@@ -186,10 +189,36 @@ services:
shlink_rabbitmq:
container_name: shlink_rabbitmq
image: rabbitmq:3.9-management-alpine
image: rabbitmq:3.11-management-alpine
ports:
- "15672:15672"
- "5672:5672"
environment:
RABBITMQ_DEFAULT_USER: "rabbit"
RABBITMQ_DEFAULT_PASS: "rabbit"
shlink_swagger_ui:
container_name: shlink_swagger_ui
image: swaggerapi/swagger-ui:v5.10.3
ports:
- "8005:8080"
volumes:
- ./docs/swagger:/app
shlink_matomo:
container_name: shlink_matomo
image: matomo:4.15-apache
ports:
- "8003:80"
volumes:
# Matomo does not persist port in trusted hosts. This volume is needed to edit config afterward
# https://github.com/matomo-org/matomo/issues/9549
- ./data/infra/matomo:/var/www/html
links:
- shlink_db_mysql
environment:
MATOMO_DATABASE_HOST: "shlink_db_mysql"
MATOMO_DATABASE_ADAPTER: "mysql"
MATOMO_DATABASE_DBNAME: "matomo"
MATOMO_DATABASE_USERNAME: "root"
MATOMO_DATABASE_PASSWORD: "root"

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 [openswoole](https://openswoole.com/), which can be linked to external databases to persist data.
It exposes a shlink instance served with [RoadRunner](https://roadrunner.dev) or [openswoole](https://openswoole.com/), which can be linked to external databases to persist data.
## Usage

View File

@@ -1,4 +1,4 @@
log_errors_max_len=0
zend.assertions=1
assert.exception=1
memory_limit=256M
memory_limit=512M

View File

@@ -6,14 +6,12 @@ namespace Shlinkio\Shlink;
use Shlinkio\Shlink\Common\Logger\LoggerType;
use function Shlinkio\Shlink\Config\runningInRoadRunner;
return [
'logger' => [
'Shlink' => [
'type' => LoggerType::STREAM->value,
'destination' => runningInRoadRunner() ? 'php://stderr' : 'php://stdout',
'destination' => 'php://stderr',
],
],

View File

@@ -1,48 +1,38 @@
#!/usr/bin/env sh
set -e
# If SHELL_VERBOSITY was not explicitly provided, run commands in quite mode (-q)
[ $SHELL_VERBOSITY ] && flags="" || flags="-q"
cd /etc/shlink
echo "Creating fresh database if needed..."
php bin/cli db:create -n ${flags}
# Create data directories if they do not exist. This allows data dir to be mounted as an empty dir if needed
mkdir -p data/cache data/locks data/log data/proxies
echo "Updating database..."
php bin/cli db:migrate -n ${flags}
flags="--no-interaction --clear-db-cache"
echo "Generating proxies..."
php bin/doctrine orm:generate-proxies -n ${flags}
echo "Clearing entities cache..."
php bin/doctrine orm:clear-cache:metadata -n ${flags}
# Try to download GeoLite2 db file only if the license key env var was defined and skipping was not explicitly set
if [ ! -z "${GEOLITE_LICENSE_KEY}" ] && [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" != "true" ]; then
echo "Downloading GeoLite2 db file..."
php bin/cli visit:download-db -n ${flags}
# Skip downloading GeoLite2 db file if the license key env var was not defined or skipping was explicitly set
if [ -z "${GEOLITE_LICENSE_KEY}" ] || [ "${SKIP_INITIAL_GEOLITE_DOWNLOAD}" = "true" ]; then
flags="${flags} --skip-download-geolite"
fi
# Periodically run visit:locate every hour, if ENABLE_PERIODIC_VISIT_LOCATE=true was provided
if [ "${ENABLE_PERIODIC_VISIT_LOCATE}" = "true" ]; then
# If INITIAL_API_KEY was provided, create an initial API key
if [ -n "${INITIAL_API_KEY}" ]; then
flags="${flags} --initial-api-key=${INITIAL_API_KEY}"
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
# RoadRunner config needs these to have been set, so falling back to default values if not set yet
if [ "$SHLINK_RUNTIME" == 'rr' ]; then
export PORT="${PORT:-"8080"}"
# Default to 0 so that RoadRunner decides the number of workers based on the amount of logical CPUs
export WEB_WORKER_NUM="${WEB_WORKER_NUM:-"0"}"
export TASK_WORKER_NUM="${TASK_WORKER_NUM:-"0"}"
fi
if [ "$SHLINK_RUNTIME" == 'openswoole' ]; then
if [ "$SHLINK_RUNTIME" = 'openswoole' ]; then
# Openswoole is deprecated. Remove in Shlink 4.0.0
# When restarting the container, openswoole might think it is already in execution
# This forces the app to be started every second until the exit code is 0
until php vendor/bin/laminas mezzio:swoole:start; do sleep 1 ; done
elif [ "$SHLINK_RUNTIME" == 'rr' ]; then
elif [ "$SHLINK_RUNTIME" = 'rr' ]; then
./bin/rr serve -c config/roadrunner/.rr.yml
fi

View File

@@ -0,0 +1,52 @@
# Build `latest` docker image only for actual releases
* Status: Accepted
* Date: 2023-07-09
## Context and problem statement
Historically, this project has re-tagged the `latest´ docker image every time a PR was merged into default branch.
The reason was to be able to:
* Periodically test the docker building and publishing process.
* Provide "partial" images for quick testing of new "un-released" features.
However, this was considered non-stable, and not recommended to use in production. Instead, a convenient `stable` tag was provided, which was re-tagged for every new non-beta/non-alpha release.
The approach described above for `latest` has some problems, though:
* Many people ignore the recommendation of not using it in production. There have even been reports of bugs on things which were, technically speaking, not yet released.
* Since it is not always built for an actual new project version, the project itself cannot inform about anything other than `latest`, which can quickly become a lie if you don't update your local version.
## Considered options
* Try to provide a pseudo-version when `latest` is built. Something like `<prev_version>-<commit_hash>.
* Change how `latest` is published, and start tagging it only for actual new version releases.
* Same as the above, but exclude alpha/beta versions, deprecating `stable` tag.
## Decision outcome
Since testing un-released features has never been needed, it is probably a not-very useful thing to have.
Periodically testing the build and publish process can also be moved somewhere else, like a testing "hidden" account.
Also, having `stable` with non-alpha/non-beta releases seems sensible, so the decision is to "Change how `latest` is published, and start tagging it only for actual new version releases".
## Pros and Cons of the Options
### Try to provide a pseudo-version when `latest` is built.
* Good: because we keep publishing process intact, from a user point of view.
* Bad: because it requires adding some non-trivial logic to the image building, which needs to find out what was the latest stable release.
### Make `latest` hold latest published version, including unstable releases.
* Good: because it provides a way for users to test bleeding-edge features, with less risk than relying on the very last content from default branch.
* Good: because it allows for `stable` to be used together with `latest`.
* Bad: because partial features cannot be tested without publishing an alpha or beta version.
### Make `latest` hold latest published version, excluding unstable releases.
* Bad: because there's no longer a way to test bleeding-edge features, other than installing that specific version.
* Bad: because it drives `stable` useless, which means it needs to be deprecated, documented, and eventually removed.

View File

@@ -2,6 +2,7 @@
Here listed you will find the different architectural decisions taken in the project, including all the reasoning behind it, options considered, and final outcome.
* [2023-07-09Build `latest` docker image only for actual releases](2023-07-09-build-latest-docker-image-only-for-actual-releases.md)
* [2023-01-06 Support any HTTP method in short URLs](2023-01-06-support-any-http-method-in-short-urls.md)
* [2022-08-05 Support multi-segment custom slugs](2022-08-05-support-multi-segment-custom-slugs.md)
* [2022-01-15 Update env vars behavior to have precedence over installer options](2022-01-15-update-env-vars-behavior-to-have-precedence-over-installer-options.md)

View File

@@ -3,18 +3,15 @@
"properties": {
"android": {
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
"type": "string",
"nullable": false
"type": ["string"]
},
"ios": {
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
"type": "string",
"nullable": false
"type": ["string"]
},
"desktop": {
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
"type": "string",
"nullable": false
"type": ["string"]
}
}
}

View File

@@ -5,13 +5,13 @@
}],
"properties": {
"android": {
"nullable": true
"type": ["null"]
},
"ios": {
"nullable": true
"type": ["null"]
},
"desktop": {
"nullable": true
"type": ["null"]
}
}
}

View File

@@ -2,18 +2,15 @@
"type": "object",
"properties": {
"baseUrlRedirect": {
"type": "string",
"nullable": true,
"type": ["string", "null"],
"description": "URL to redirect to when a user hits the domain's base URL"
},
"regular404Redirect": {
"type": "string",
"nullable": true,
"type": ["string", "null"],
"description": "URL to redirect to when a user hits a not found URL other than an invalid short URL"
},
"invalidShortUrlRedirect": {
"type": "string",
"nullable": true,
"type": ["string", "null"],
"description": "URL to redirect to when a user hits an invalid short URL"
}
}

View File

@@ -6,8 +6,7 @@
}],
"properties": {
"visitedUrl": {
"type": "string",
"nullable": true,
"type": ["string", "null"],
"description": "The originally visited URL that triggered the tracking of this visit"
},
"type": {

View File

@@ -55,13 +55,11 @@
"$ref": "./ShortUrlMeta.json"
},
"domain": {
"type": "string",
"nullable": true,
"type": ["string", "null"],
"description": "The domain in which the short URL was created. Null if it belongs to default domain."
},
"title": {
"type": "string",
"nullable": true,
"type": ["string", "null"],
"description": "A descriptive title of the short URL."
},
"crawlable": {

View File

@@ -10,18 +10,15 @@
},
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": "string",
"nullable": true
"type": ["string", "null"]
},
"validUntil": {
"description": "The date (in ISO-8601 format) until which this short code will be valid",
"type": "string",
"nullable": true
"type": ["string", "null"]
},
"maxVisits": {
"description": "The maximum number of allowed visits for this short code",
"type": "number",
"nullable": true
"type": ["number", "null"]
},
"validateUrl": {
"deprecated": true,
@@ -36,9 +33,8 @@
"description": "The list of tags to set to the short URL."
},
"title": {
"type": "string",
"description": "A descriptive title of the short URL.",
"nullable": true
"type": ["string", "null"],
"description": "A descriptive title of the short URL."
},
"crawlable": {
"type": "boolean",

View File

@@ -4,18 +4,15 @@
"properties": {
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": "string",
"nullable": true
"type": ["string", "null"]
},
"validUntil": {
"description": "The date (in ISO-8601 format) until which this short code will be valid",
"type": "string",
"nullable": true
"type": ["string", "null"]
},
"maxVisits": {
"description": "The maximum number of allowed visits for this short code",
"type": "number",
"nullable": true
"type": ["number", "null"]
}
}
}

View File

@@ -0,0 +1,9 @@
{
"name": "shortCode",
"in": "path",
"description": "The short code for the short URL.",
"required": true,
"schema": {
"type": "string"
}
}

View File

@@ -11,13 +11,7 @@
"$ref": "../parameters/version.json"
},
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
"$ref": "../parameters/shortCode.json"
},
{
"$ref": "../parameters/domain.json"
@@ -127,13 +121,7 @@
"$ref": "../parameters/version.json"
},
{
"name": "shortCode",
"in": "path",
"description": "The short code to edit.",
"required": true,
"schema": {
"type": "string"
}
"$ref": "../parameters/shortCode.json"
},
{
"$ref": "../parameters/domain.json"
@@ -295,13 +283,7 @@
"$ref": "../parameters/version.json"
},
{
"name": "shortCode",
"in": "path",
"description": "The short code to edit.",
"required": true,
"schema": {
"type": "string"
}
"$ref": "../parameters/shortCode.json"
},
{
"$ref": "../parameters/domain.json"

View File

@@ -11,13 +11,7 @@
"$ref": "../parameters/version.json"
},
{
"name": "shortCode",
"in": "path",
"description": "The short code for the short URL from which we want to get the visits.",
"required": true,
"schema": {
"type": "string"
}
"$ref": "../parameters/shortCode.json"
},
{
"$ref": "../parameters/domain.json"
@@ -172,5 +166,79 @@
}
}
}
},
"delete": {
"operationId": "deleteShortUrlVisits",
"tags": [
"Visits"
],
"summary": "Delete visits for short URL",
"description": "Delete all existing visits on the short URL behind provided short code.",
"parameters": [
{
"$ref": "../parameters/version.json"
},
{
"$ref": "../parameters/shortCode.json"
},
{
"$ref": "../parameters/domain.json"
}
],
"security": [
{
"ApiKey": []
}
],
"responses": {
"200": {
"description": "Deleted visits",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"deletedVisits": {
"description": "Amount of affected visits",
"type": "number"
}
}
},
"example": {
"deletedVisits": 536
}
}
}
},
"404": {
"description": "The short code does not belong to any short URL.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
},
"examples": {
"Short URL not found with API v3 and newer": {
"$ref": "../examples/short-url-not-found-v3.json"
},
"Short URL not found previous to API v3": {
"$ref": "../examples/short-url-not-found-v2.json"
}
}
}
}
},
"default": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -148,5 +148,55 @@
}
}
}
},
"delete": {
"operationId": "deleteOrphanVisits",
"tags": [
"Visits"
],
"summary": "Delete orphan visits",
"description": "Delete all visits to invalid short URLs, the base URL or any other 404.",
"parameters": [
{
"$ref": "../parameters/version.json"
}
],
"security": [
{
"ApiKey": []
}
],
"responses": {
"200": {
"description": "Deleted visits",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"deletedVisits": {
"description": "Amount of affected visits",
"type": "number"
}
}
},
"example": {
"deletedVisits": 536
}
}
}
},
"default": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -8,13 +8,7 @@
"description": "Represents a short URL. Tracks the visit and redirects tio the corresponding long URL",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
"$ref": "../parameters/shortCode.json"
}
],
"responses": {

View File

@@ -8,13 +8,7 @@
"description": "Generates a QR code image pointing to a short URL.<br />Since this is not an API endpoint but an image one, when an invalid value is provided for any of the query params, they will fall to their default values instead of throwing an error.",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
"$ref": "../parameters/shortCode.json"
},
{
"name": "size",

View File

@@ -8,13 +8,7 @@
"description": "Generates a 1px transparent image which can be used to track emails with a short URL",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
"$ref": "../parameters/shortCode.json"
}
],
"responses": {

View File

@@ -1,5 +1,5 @@
{
"openapi": "3.0.3",
"openapi": "3.1.0",
"info": {
"title": "Shlink",
"description": "Shlink, the self-hosted URL shortener",

View File

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

View File

@@ -1,15 +0,0 @@
<?php
declare(strict_types=1);
return [
'migrations_paths' => [
'ShlinkMigrations' => 'data/migrations',
],
'table_storage' => [
'table_name' => 'migrations',
],
'custom_template' => 'data/migrations_template.txt',
];

View File

@@ -13,15 +13,18 @@ return [
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
Command\ShortUrl\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::class,
Command\ShortUrl\DeleteShortUrlCommand::NAME => Command\ShortUrl\DeleteShortUrlCommand::class,
Command\ShortUrl\DeleteShortUrlVisitsCommand::NAME => Command\ShortUrl\DeleteShortUrlVisitsCommand::class,
Command\Visit\LocateVisitsCommand::NAME => Command\Visit\LocateVisitsCommand::class,
Command\Visit\DownloadGeoLiteDbCommand::NAME => Command\Visit\DownloadGeoLiteDbCommand::class,
Command\Visit\GetOrphanVisitsCommand::NAME => Command\Visit\GetOrphanVisitsCommand::class,
Command\Visit\DeleteOrphanVisitsCommand::NAME => Command\Visit\DeleteOrphanVisitsCommand::class,
Command\Visit\GetNonOrphanVisitsCommand::NAME => Command\Visit\GetNonOrphanVisitsCommand::class,
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,
Command\Api\ListKeysCommand::NAME => Command\Api\ListKeysCommand::class,
Command\Api\InitialApiKeyCommand::NAME => Command\Api\InitialApiKeyCommand::class,
Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class,
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,

View File

@@ -42,15 +42,18 @@ return [
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\DeleteShortUrlCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\DeleteShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
Command\Visit\DownloadGeoLiteDbCommand::class => ConfigAbstractFactory::class,
Command\Visit\LocateVisitsCommand::class => ConfigAbstractFactory::class,
Command\Visit\GetOrphanVisitsCommand::class => ConfigAbstractFactory::class,
Command\Visit\DeleteOrphanVisitsCommand::class => ConfigAbstractFactory::class,
Command\Visit\GetNonOrphanVisitsCommand::class => ConfigAbstractFactory::class,
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\ListKeysCommand::class => ConfigAbstractFactory::class,
Command\Api\InitialApiKeyCommand::class => ConfigAbstractFactory::class,
Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class,
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
@@ -88,6 +91,7 @@ return [
],
Command\ShortUrl\GetShortUrlVisitsCommand::class => [Visit\VisitsStatsHelper::class],
Command\ShortUrl\DeleteShortUrlCommand::class => [ShortUrl\DeleteShortUrlService::class],
Command\ShortUrl\DeleteShortUrlVisitsCommand::class => [ShortUrl\ShortUrlVisitsDeleter::class],
Command\Visit\DownloadGeoLiteDbCommand::class => [GeoLite\GeolocationDbUpdater::class],
Command\Visit\LocateVisitsCommand::class => [
@@ -96,11 +100,13 @@ return [
LockFactory::class,
],
Command\Visit\GetOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class],
Command\Visit\DeleteOrphanVisitsCommand::class => [Visit\VisitsDeleter::class],
Command\Visit\GetNonOrphanVisitsCommand::class => [Visit\VisitsStatsHelper::class, ShortUrlStringifier::class],
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
Command\Api\ListKeysCommand::class => [ApiKeyService::class],
Command\Api\InitialApiKeyCommand::class => [ApiKeyService::class],
Command\Tag\ListTagsCommand::class => [TagService::class],
Command\Tag\RenameTagCommand::class => [TagService::class],

View File

@@ -14,24 +14,27 @@ use function is_string;
class RoleResolver implements RoleResolverInterface
{
public function __construct(private DomainServiceInterface $domainService, private string $defaultDomain)
{
public function __construct(
private readonly DomainServiceInterface $domainService,
private readonly string $defaultDomain,
) {
}
public function determineRoles(InputInterface $input): array
public function determineRoles(InputInterface $input): iterable
{
$domainAuthority = $input->getOption(Role::DOMAIN_SPECIFIC->paramName());
$author = $input->getOption(Role::AUTHORED_SHORT_URLS->paramName());
$noOrphanVisits = $input->getOption(Role::NO_ORPHAN_VISITS->paramName());
$roleDefinitions = [];
if ($author) {
$roleDefinitions[] = RoleDefinition::forAuthoredShortUrls();
yield RoleDefinition::forAuthoredShortUrls();
}
if (is_string($domainAuthority)) {
$roleDefinitions[] = $this->resolveRoleForAuthority($domainAuthority);
yield $this->resolveRoleForAuthority($domainAuthority);
}
if ($noOrphanVisits) {
yield RoleDefinition::forNoOrphanVisits();
}
return $roleDefinitions;
}
private function resolveRoleForAuthority(string $domainAuthority): RoleDefinition

View File

@@ -10,7 +10,7 @@ use Symfony\Component\Console\Input\InputInterface;
interface RoleResolverInterface
{
/**
* @return RoleDefinition[]
* @return iterable<RoleDefinition>
*/
public function determineRoles(InputInterface $input): array;
public function determineRoles(InputInterface $input): iterable;
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
@@ -39,10 +39,10 @@ class DisableKeyCommand extends Command
try {
$this->apiKeyService->disable($apiKey);
$io->success(sprintf('API key "%s" properly disabled', $apiKey));
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
} catch (InvalidArgumentException $e) {
$io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE;
return ExitCode::EXIT_FAILURE;
}
}
}

View File

@@ -6,9 +6,11 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Model\ApiKeyMeta;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -24,8 +26,8 @@ class GenerateKeyCommand extends Command
public const NAME = 'api-key:generate';
public function __construct(
private ApiKeyServiceInterface $apiKeyService,
private RoleResolverInterface $roleResolver,
private readonly ApiKeyServiceInterface $apiKeyService,
private readonly RoleResolverInterface $roleResolver,
) {
parent::__construct();
}
@@ -34,6 +36,8 @@ class GenerateKeyCommand extends Command
{
$authorOnly = Role::AUTHORED_SHORT_URLS->paramName();
$domainOnly = Role::DOMAIN_SPECIFIC->paramName();
$noOrphanVisits = Role::NO_ORPHAN_VISITS->paramName();
$help = <<<HELP
The <info>%command.name%</info> generates a new valid API key.
@@ -51,12 +55,13 @@ class GenerateKeyCommand extends Command
* Can interact with short URLs created with this API key: <info>%command.full_name% --{$authorOnly}</info>
* Can interact with short URLs for one domain: <info>%command.full_name% --{$domainOnly}=example.com</info>
* Both: <info>%command.full_name% --{$authorOnly} --{$domainOnly}=example.com</info>
* Cannot see orphan visits: <info>%command.full_name% --{$noOrphanVisits}</info>
* All: <info>%command.full_name% --{$authorOnly} --{$domainOnly}=example.com --{$noOrphanVisits}</info>
HELP;
$this
->setName(self::NAME)
->setDescription('Generates a new valid API key.')
->setDescription('Generate a new valid API key.')
->addOption(
'name',
'm',
@@ -84,22 +89,29 @@ class GenerateKeyCommand extends Command
Role::DOMAIN_SPECIFIC->value,
),
)
->addOption(
$noOrphanVisits,
'o',
InputOption::VALUE_NONE,
sprintf('Adds the "%s" role to the new API key.', Role::NO_ORPHAN_VISITS->value),
)
->setHelp($help);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$expirationDate = $input->getOption('expiration-date');
$apiKey = $this->apiKeyService->create(
isset($expirationDate) ? Chronos::parse($expirationDate) : null,
$input->getOption('name'),
...$this->roleResolver->determineRoles($input),
);
$apiKey = $this->apiKeyService->create(ApiKeyMeta::fromParams(
name: $input->getOption('name'),
expirationDate: isset($expirationDate) ? Chronos::parse($expirationDate) : null,
roleDefinitions: $this->roleResolver->determineRoles($input),
));
$io = new SymfonyStyle($input, $output);
$io->success(sprintf('Generated API key: "%s"', $apiKey->toString()));
if (! $apiKey->isAdmin()) {
if (! ApiKey::isAdmin($apiKey)) {
ShlinkTable::default($io)->render(
['Role name', 'Role metadata'],
$apiKey->mapRoles(fn (Role $role, array $meta) => [$role->value, arrayToString($meta, 0)]),
@@ -108,6 +120,6 @@ class GenerateKeyCommand extends Command
);
}
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class InitialApiKeyCommand extends Command
{
public const NAME = 'api-key:initial';
public function __construct(private readonly ApiKeyServiceInterface $apiKeyService)
{
parent::__construct();
}
protected function configure(): void
{
$this
->setHidden()
->setName(self::NAME)
->setDescription('Tries to create initial API key')
->addArgument('apiKey', InputArgument::REQUIRED, 'The initial API to create');
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$key = $input->getArgument('apiKey');
$result = $this->apiKeyService->createInitial($key);
if ($result === null && $output->isVerbose()) {
$output->writeln('<comment>Other API keys already exist. Initial API key creation skipped.</comment>');
}
return ExitCode::EXIT_SUCCESS;
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
@@ -15,7 +15,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function array_filter;
use function Functional\map;
use function array_map;
use function implode;
use function sprintf;
@@ -27,7 +27,7 @@ class ListKeysCommand extends Command
public const NAME = 'api-key:list';
public function __construct(private ApiKeyServiceInterface $apiKeyService)
public function __construct(private readonly ApiKeyServiceInterface $apiKeyService)
{
parent::__construct();
}
@@ -49,7 +49,7 @@ class ListKeysCommand extends Command
{
$enabledOnly = $input->getOption('enabled-only');
$rows = map($this->apiKeyService->listKeys($enabledOnly), function (ApiKey $apiKey) use ($enabledOnly) {
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
$expiration = $apiKey->getExpirationDate();
$messagePattern = $this->determineMessagePattern($apiKey);
@@ -59,15 +59,12 @@ class ListKeysCommand extends Command
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
}
$rowData[] = $expiration?->toAtomString() ?? '-';
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
fn (Role $role, array $meta) =>
empty($meta)
? $role->toFriendlyName()
: sprintf('%s: %s', $role->toFriendlyName(), Role::domainAuthorityFromMeta($meta)),
$rowData[] = ApiKey::isAdmin($apiKey) ? 'Admin' : implode("\n", $apiKey->mapRoles(
fn (Role $role, array $meta) => $role->toFriendlyName($meta),
));
return $rowData;
});
}, $this->apiKeyService->listKeys($enabledOnly));
ShlinkTable::withRowSeparators($output)->render(array_filter([
'Key',
@@ -77,7 +74,7 @@ class ListKeysCommand extends Command
'Roles',
]), $rows);
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
private function determineMessagePattern(ApiKey $apiKey): string

View File

@@ -5,20 +5,20 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Db;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\SqlitePlatform;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Mapping\ClassMetadata;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ProcessRunnerInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Process\PhpExecutableFinder;
use Throwable;
use function Functional\contains;
use function Functional\map;
use function Functional\some;
use function array_map;
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
use function Shlinkio\Shlink\Core\ArrayUtils\some;
class CreateDatabaseCommand extends AbstractDatabaseCommand
{
@@ -53,11 +53,9 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
{
$io = new SymfonyStyle($input, $output);
$this->checkDbExists();
if ($this->schemaExists()) {
if ($this->databaseTablesExist()) {
$io->success('Database already exists. Run "db:migrate" command to make sure it is up to date.');
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
// Create database
@@ -65,38 +63,34 @@ class CreateDatabaseCommand extends AbstractDatabaseCommand
$this->runPhpCommand($output, [self::DOCTRINE_SCRIPT, self::DOCTRINE_CREATE_SCHEMA_COMMAND]);
$io->success('Database properly created!');
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
private function checkDbExists(): void
private function databaseTablesExist(): bool
{
if ($this->regularConn->getDriver()->getDatabasePlatform() instanceof SqlitePlatform) {
return;
}
// In order to create the new database, we have to use a connection where the dbname was not set.
// Otherwise, it will fail to connect and will not be able to create the new database
$schemaManager = $this->noDbNameConn->createSchemaManager();
$databases = $schemaManager->listDatabases();
// We cannot use getDatabase() to get the database name here, because then the driver will try to connect, and
// it does not exist yet. We need to read from the raw params instead.
$shlinkDatabase = $this->regularConn->getParams()['dbname'] ?? null;
if ($shlinkDatabase !== null && ! contains($databases, $shlinkDatabase)) {
$schemaManager->createDatabase($shlinkDatabase);
}
}
private function schemaExists(): bool
{
$schemaManager = $this->regularConn->createSchemaManager();
$existingTables = $schemaManager->listTableNames();
$existingTables = $this->ensureDatabaseExistsAndGetTables();
$allMetadata = $this->em->getMetadataFactory()->getAllMetadata();
$shlinkTables = map($allMetadata, static fn (ClassMetadata $metadata) => $metadata->getTableName());
$shlinkTables = array_map(static fn (ClassMetadata $metadata) => $metadata->getTableName(), $allMetadata);
// If at least one of the shlink tables exist, we will consider the database exists somehow.
// Any other inconsistency will be taken care of by the migrations.
return some($shlinkTables, static fn (string $shlinkTable) => contains($existingTables, $shlinkTable));
return some($shlinkTables, static fn (string $shlinkTable) => contains($shlinkTable, $existingTables));
}
private function ensureDatabaseExistsAndGetTables(): array
{
try {
// Trying to list tables requires opening a connection to configured database.
// If it fails, it means it does not exist yet.
return $this->regularConn->createSchemaManager()->listTableNames();
} catch (Throwable) {
// We cannot use getDatabase() to get the database name here, because then the driver will try to connect.
// Instead, we read from the raw params.
$shlinkDatabase = $this->regularConn->getParams()['dbname'] ?? '';
// Create the database using a connection where the dbname was not set.
$this->noDbNameConn->createSchemaManager()->createDatabase($shlinkDatabase);
return [];
}
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Db;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -31,6 +31,6 @@ class MigrateDatabaseCommand extends AbstractDatabaseCommand
$this->runPhpCommand($output, [self::DOCTRINE_MIGRATIONS_SCRIPT, self::DOCTRINE_MIGRATE_COMMAND]);
$io->success('Database properly migrated!');
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Domain;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Config\NotFoundRedirects;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
@@ -14,8 +14,8 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function Functional\filter;
use function Functional\invoke;
use function array_filter;
use function array_map;
use function sprintf;
use function str_contains;
@@ -23,7 +23,7 @@ class DomainRedirectsCommand extends Command
{
public const NAME = 'domain:redirects';
public function __construct(private DomainServiceInterface $domainService)
public function __construct(private readonly DomainServiceInterface $domainService)
{
parent::__construct();
}
@@ -52,9 +52,9 @@ class DomainRedirectsCommand extends Command
$askNewDomain = static fn () => $io->ask('Domain authority for which you want to set specific redirects');
/** @var string[] $availableDomains */
$availableDomains = invoke(
filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
'toString',
$availableDomains = array_map(
static fn (DomainItem $item) => $item->toString(),
array_filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
);
if (empty($availableDomains)) {
$input->setArgument('domain', $askNewDomain());
@@ -109,6 +109,6 @@ class DomainRedirectsCommand extends Command
$io->success(sprintf('"Not found" redirects properly set for "%s"', $domainAuthority));
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Domain;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Core\Config\NotFoundRedirectConfigInterface;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
@@ -14,13 +14,13 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function Functional\map;
use function array_map;
class ListDomainsCommand extends Command
{
public const NAME = 'domain:list';
public function __construct(private DomainServiceInterface $domainService)
public function __construct(private readonly DomainServiceInterface $domainService)
{
parent::__construct();
}
@@ -47,7 +47,7 @@ class ListDomainsCommand extends Command
$table->render(
$showRedirects ? [...$commonFields, '"Not found" redirects'] : $commonFields,
map($domains, function (DomainItem $domain) use ($showRedirects) {
array_map(function (DomainItem $domain) use ($showRedirects) {
$commonValues = [$domain->toString(), $domain->isDefault ? 'Yes' : 'No'];
return $showRedirects
@@ -56,10 +56,10 @@ class ListDomainsCommand extends Command
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig),
]
: $commonValues;
}),
}, $domains),
);
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
private function notFoundRedirectsToString(NotFoundRedirectConfigInterface $config): string

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
@@ -20,10 +20,9 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function array_map;
use function array_unique;
use function explode;
use function Functional\curry;
use function Functional\flatten;
use function Functional\unique;
use function Shlinkio\Shlink\Core\ArrayUtils\flatten;
use function sprintf;
class CreateShortUrlCommand extends Command
@@ -31,7 +30,6 @@ class CreateShortUrlCommand extends Command
public const NAME = 'short-url:create';
private ?SymfonyStyle $io;
private string $defaultDomain;
public function __construct(
private readonly UrlShortenerInterface $urlShortener,
@@ -39,7 +37,6 @@ class CreateShortUrlCommand extends Command
private readonly UrlShortenerOptions $options,
) {
parent::__construct();
$this->defaultDomain = $this->options->domain['hostname'] ?? '';
}
protected function configure(): void
@@ -121,7 +118,6 @@ class CreateShortUrlCommand extends Command
protected function interact(InputInterface $input, OutputInterface $output): void
{
$this->verifyLongUrlArgument($input, $output);
$this->verifyDomainArgument($input);
}
private function verifyLongUrlArgument(InputInterface $input, OutputInterface $output): void
@@ -138,30 +134,24 @@ class CreateShortUrlCommand extends Command
}
}
private function verifyDomainArgument(InputInterface $input): void
{
$domain = $input->getOption('domain');
$input->setOption('domain', $domain === $this->defaultDomain ? null : $domain);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = $this->getIO($input, $output);
$longUrl = $input->getArgument('longUrl');
if (empty($longUrl)) {
$io->error('A URL was not provided!');
return ExitCodes::EXIT_FAILURE;
return ExitCode::EXIT_FAILURE;
}
$explodeWithComma = curry(explode(...))(',');
$tags = unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
$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 {
$shortUrl = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([
$result = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([
ShortUrlInputFilter::LONG_URL => $longUrl,
ShortUrlInputFilter::VALID_SINCE => $input->getOption('valid-since'),
ShortUrlInputFilter::VALID_UNTIL => $input->getOption('valid-until'),
@@ -176,14 +166,19 @@ class CreateShortUrlCommand extends Command
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
], $this->options));
$result->onEventDispatchingError(static fn () => $io->isVerbose() && $io->warning(
'Short URL properly created, but the real-time updates cannot be notified when generating the '
. 'short URL from the command line. Migrate to roadrunner in order to bypass this limitation.',
));
$io->writeln([
sprintf('Processed long URL: <info>%s</info>', $longUrl),
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($shortUrl)),
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)),
]);
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
} catch (InvalidUrlException | NonUniqueSlugException $e) {
$io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE;
return ExitCode::EXIT_FAILURE;
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception;
use Shlinkio\Shlink\Core\ShortUrl\DeleteShortUrlServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
@@ -55,10 +55,10 @@ class DeleteShortUrlCommand extends Command
try {
$this->runDelete($io, $identifier, $ignoreThreshold);
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
} catch (Exception\ShortUrlNotFoundException $e) {
$io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE;
return ExitCode::EXIT_FAILURE;
} catch (Exception\DeleteShortUrlException $e) {
return $this->retry($io, $identifier, $e->getMessage());
}
@@ -75,7 +75,7 @@ class DeleteShortUrlCommand extends Command
$io->warning('Short URL was not deleted.');
}
return $forceDelete ? ExitCodes::EXIT_SUCCESS : ExitCodes::EXIT_WARNING;
return $forceDelete ? ExitCode::EXIT_SUCCESS : ExitCode::EXIT_WARNING;
}
private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void

View File

@@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Command\Visit\AbstractDeleteVisitsCommand;
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;
class DeleteShortUrlVisitsCommand extends AbstractDeleteVisitsCommand
{
public const NAME = 'short-url:visits-delete';
public function __construct(private readonly ShortUrlVisitsDeleterInterface $deleter)
{
parent::__construct();
}
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',
);
}
protected function doExecute(InputInterface $input, SymfonyStyle $io): ?int
{
$identifier = ShortUrlIdentifier::fromCli($input);
try {
$result = $this->deleter->deleteShortUrlVisits($identifier);
$io->success(sprintf('Successfully deleted %s visits', $result->affectedItems));
return ExitCode::EXIT_SUCCESS;
} catch (ShortUrlNotFoundException) {
$io->warning(sprintf('Short URL not found for "%s"', $identifier->__toString()));
return ExitCode::EXIT_WARNING;
}
}
protected function getWarningMessage(): string
{
return 'You are about to delete all visits for a short URL. This operation cannot be undone.';
}
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Option\EndDateOption;
use Shlinkio\Shlink\CLI\Option\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Input\EndDateOption;
use Shlinkio\Shlink\CLI\Input\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Paginator\Util\PagerfantaUtilsTrait;
@@ -23,9 +23,9 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function array_keys;
use function array_map;
use function array_pad;
use function explode;
use function Functional\map;
use function implode;
use function sprintf;
@@ -102,6 +102,12 @@ class ListShortUrlsCommand extends Command
InputOption::VALUE_NONE,
'Whether to display the tags or not.',
)
->addOption(
'show-domain',
null,
InputOption::VALUE_NONE,
'Whether to display the domain or not. Those belonging to default domain will have value "DEFAULT".',
)
->addOption(
'show-api-key',
'k',
@@ -167,7 +173,7 @@ class ListShortUrlsCommand extends Command
$io->newLine();
$io->success('Short URLs properly listed');
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
private function renderPage(
@@ -178,10 +184,10 @@ class ListShortUrlsCommand extends Command
): Paginator {
$shortUrls = $this->shortUrlService->listShortUrls($params);
$rows = map($shortUrls, function (ShortUrl $shortUrl) use ($columnsMap) {
$rows = array_map(function (ShortUrl $shortUrl) use ($columnsMap) {
$rawShortUrl = $this->transformer->transform($shortUrl);
return map($columnsMap, fn (callable $call) => $call($rawShortUrl, $shortUrl));
});
return array_map(fn (callable $call) => $call($rawShortUrl, $shortUrl), $columnsMap);
}, [...$shortUrls]);
ShlinkTable::default($output)->render(
array_keys($columnsMap),
@@ -217,6 +223,10 @@ class ListShortUrlsCommand extends Command
if ($input->getOption('show-tags')) {
$columnsMap['Tags'] = static fn (array $shortUrl): string => implode(', ', $shortUrl['tags']);
}
if ($input->getOption('show-domain')) {
$columnsMap['Domain'] = static fn (array $_, ShortUrl $shortUrl): string =>
$shortUrl->getDomain()?->authority ?? 'DEFAULT';
}
if ($input->getOption('show-api-key')) {
$columnsMap['API Key'] = static fn (array $_, ShortUrl $shortUrl): string =>
$shortUrl->authorApiKey()?->__toString() ?? '';

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
@@ -56,10 +56,10 @@ class ResolveUrlCommand extends Command
try {
$url = $this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromCli($input));
$output->writeln(sprintf('Long URL: <info>%s</info>', $url->getLongUrl()));
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
} catch (ShortUrlNotFoundException $e) {
$io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE;
return ExitCode::EXIT_FAILURE;
}
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -41,11 +41,11 @@ class DeleteTagsCommand extends Command
if (empty($tagNames)) {
$io->warning('You have to provide at least one tag name');
return ExitCodes::EXIT_WARNING;
return ExitCode::EXIT_WARNING;
}
$this->tagService->deleteTags($tagNames);
$io->success('Tags properly deleted');
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
use Shlinkio\Shlink\Core\Tag\Model\TagsParams;
@@ -13,13 +13,13 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function Functional\map;
use function array_map;
class ListTagsCommand extends Command
{
public const NAME = 'tag:list';
public function __construct(private TagServiceInterface $tagService)
public function __construct(private readonly TagServiceInterface $tagService)
{
parent::__construct();
}
@@ -34,7 +34,7 @@ class ListTagsCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
ShlinkTable::default($output)->render(['Name', 'URLs amount', 'Visits amount'], $this->getTagsRows());
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
private function getTagsRows(): array
@@ -44,9 +44,9 @@ class ListTagsCommand extends Command
return [['No tags found', '-', '-']];
}
return map(
$tags,
return array_map(
static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsSummary->total],
[...$tags],
);
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\TagConflictException;
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
@@ -42,10 +42,10 @@ class RenameTagCommand extends Command
try {
$this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName));
$io->success('Tag properly renamed.');
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
} catch (TagNotFoundException | TagConflictException $e) {
$io->error($e->getMessage());
return ExitCodes::EXIT_FAILURE;
return ExitCode::EXIT_FAILURE;
}
}
}

View File

@@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Util;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -28,7 +28,7 @@ abstract class AbstractLockedCommand extends Command
$output->writeln(
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName),
);
return ExitCodes::EXIT_WARNING;
return ExitCode::EXIT_WARNING;
}
try {

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
abstract class AbstractDeleteVisitsCommand extends Command
{
final protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
if (! $this->confirm($io)) {
$io->info('Operation aborted');
return ExitCode::EXIT_SUCCESS;
}
return $this->doExecute($input, $io);
}
private function confirm(SymfonyStyle $io): bool
{
$io->warning($this->getWarningMessage());
return $io->confirm('<comment>Continue deleting visits?</comment>', false);
}
abstract protected function doExecute(InputInterface $input, SymfonyStyle $io): ?int;
abstract protected function getWarningMessage(): string;
}

View File

@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Option\EndDateOption;
use Shlinkio\Shlink\CLI\Option\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Input\EndDateOption;
use Shlinkio\Shlink\CLI\Input\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
@@ -17,9 +17,9 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function array_keys;
use function Functional\map;
use function Functional\select_keys;
use function array_map;
use function Shlinkio\Shlink\Common\buildDateRange;
use function Shlinkio\Shlink\Core\ArrayUtils\select_keys;
use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly;
abstract class AbstractVisitsListCommand extends Command
@@ -43,13 +43,13 @@ abstract class AbstractVisitsListCommand extends Command
ShlinkTable::default($output)->render($headers, $rows);
return ExitCodes::EXIT_SUCCESS;
return ExitCode::EXIT_SUCCESS;
}
private function resolveRowsAndHeaders(Paginator $paginator): array
{
$extraKeys = [];
$rows = map($paginator->getCurrentPageResults(), function (Visit $visit) use (&$extraKeys) {
$rows = array_map(function (Visit $visit) use (&$extraKeys) {
$extraFields = $this->mapExtraFields($visit);
$extraKeys = array_keys($extraFields);
@@ -60,9 +60,10 @@ abstract class AbstractVisitsListCommand extends Command
...$extraFields,
];
// Filter out unknown keys
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country', 'city', ...$extraKeys]);
});
$extra = map($extraKeys, camelCaseToHumanFriendly(...));
}, [...$paginator->getCurrentPageResults()]);
$extra = array_map(camelCaseToHumanFriendly(...), $extraKeys);
return [
$rows,

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Visit\VisitsDeleterInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
class DeleteOrphanVisitsCommand extends AbstractDeleteVisitsCommand
{
public const NAME = 'visit:orphan-delete';
public function __construct(private readonly VisitsDeleterInterface $deleter)
{
parent::__construct();
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('Deletes all orphan visits');
}
protected function doExecute(InputInterface $input, SymfonyStyle $io): ?int
{
$result = $this->deleter->deleteOrphanVisits();
$io->success(sprintf('Successfully deleted %s visits', $result->affectedItems));
return ExitCode::EXIT_SUCCESS;
}
protected function getWarningMessage(): string
{
return 'You are about to delete all orphan visits. This operation cannot be undone.';
}
}

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