Compare commits

...

448 Commits

Author SHA1 Message Date
Alejandro Celaya
da9e9df4ba Merge pull request #960 from acelaya-forks/feature/api-roles-cli
Feature/api roles cli
2021-01-11 20:35:48 +01:00
Alejandro Celaya
1c75519f9b Displayed 'Admin' as default role in API keys list 2021-01-11 20:23:28 +01:00
Alejandro Celaya
fca19f265b Removed duplicated lines in GenerateKeyCommand 2021-01-11 20:14:18 +01:00
Alejandro Celaya
75dab92225 Improved tests covering ListKeysCommand 2021-01-11 17:01:01 +01:00
Alejandro Celaya
9e9d213f20 Added roles info to api key generation and api key list 2021-01-11 16:32:59 +01:00
Alejandro Celaya
c49a0ca040 Added list of roles to print after an API is generated 2021-01-11 15:20:26 +01:00
Alejandro Celaya
1f2e16184c Extracted function to render arrays from inside ValidationException 2021-01-10 20:28:52 +01:00
Alejandro Celaya
7a19b8765d Created RoleResolverTest 2021-01-10 20:24:13 +01:00
Alejandro Celaya
a639a4eb94 Added role capabilities to api-key:generate command 2021-01-10 20:14:06 +01:00
Alejandro Celaya
c9ff2b3834 Updated services required to initialize API keys with roles 2021-01-10 20:05:14 +01:00
Alejandro Celaya
95e51665b1 Merge pull request #958 from acelaya-forks/feature/api-key-permissions
Feature/api key permissions
2021-01-10 11:25:29 +01:00
Alejandro Celaya
91da241434 Updated changelog 2021-01-10 11:12:22 +01:00
Alejandro Celaya
5bec9f5b65 Extended swagger docs with errors on delete/rename tags 2021-01-10 11:07:17 +01:00
Alejandro Celaya
34bb023b7d Created API tests to cover deletion and renaming of tags with non-admin API keys 2021-01-10 10:28:00 +01:00
Alejandro Celaya
2be0050f3d Improved tag list api test to cover different API key cases 2021-01-10 10:17:27 +01:00
Alejandro Celaya
ff1af82ffd Improved tag visits api test to cover different API key cases 2021-01-10 10:00:00 +01:00
Alejandro Celaya
13cc70e6d4 Added more tags to more fixture short URLs in API keys 2021-01-10 09:54:19 +01:00
Alejandro Celaya
fa5934b8b6 Improved global visits api test to cover different API key cases 2021-01-10 09:36:10 +01:00
Alejandro Celaya
c8eb956778 Improved list domains api test to cover different API key cases 2021-01-10 09:32:19 +01:00
Alejandro Celaya
5283ee2c6b Moved common data provider for core unit tests to trait 2021-01-10 09:31:51 +01:00
Alejandro Celaya
c56d56d38c Added api tests to cover implicit domain when creating short URLs with proper API key 2021-01-10 09:09:56 +01:00
Alejandro Celaya
ea05259bbe Improved api tests where a short URL needs to be resolved, covering cases where API key lacks permissions 2021-01-10 09:02:05 +01:00
Alejandro Celaya
f17873b527 Added api tests for short URLs lists using API keys with permissions 2021-01-10 08:49:31 +01:00
Alejandro Celaya
f827186c77 Updated API test fixtures to include API keys with roles 2021-01-10 08:40:32 +01:00
Alejandro Celaya
380915948b Improved TagRepositoryTest 2021-01-09 18:00:08 +01:00
Alejandro Celaya
14eeb91c58 Added db test for VisitRepository::countVisits 2021-01-09 17:54:04 +01:00
Alejandro Celaya
01dceca9ef Enhanced ShorturlRepository::findOneMatching test to cover ApiKey use cases 2021-01-09 14:39:19 +01:00
Alejandro Celaya
ba32366b0b Added tagExists to TagRepositoryTest 2021-01-09 13:44:47 +01:00
Alejandro Celaya
bef1b13a33 Enhanced DomainRepositoryTest covering API key permissions 2021-01-09 13:16:33 +01:00
Alejandro Celaya
caa1ae0de8 Added all missing unit tests covering API key permissions 2021-01-09 12:38:06 +01:00
Alejandro Celaya
b0c4582f3f Used EntitySpecificationRepository as default entity repository 2021-01-09 10:56:02 +01:00
Alejandro Celaya
a8b68f07b5 Ensured delete/rename tags cannot be done with non-admin API keys 2021-01-06 17:31:49 +01:00
Alejandro Celaya
b5710f87e2 Created value object to wrap the renaming of a tag 2021-01-06 13:11:28 +01:00
Alejandro Celaya
041f231ff2 Implemented mechanism to add/remove roles from API keys 2021-01-06 10:59:08 +01:00
Alejandro Celaya
01b3c504f8 Ensured fixed commit for happyr/doctrine-specification is installed, until a stable v2.0 is released 2021-01-05 19:32:18 +01:00
Alejandro Celaya
f821dea06c Fixed typo on fixture 2021-01-05 19:29:42 +01:00
Alejandro Celaya
4b67d41362 Applied API role specs to short URL creation 2021-01-04 20:15:42 +01:00
Alejandro Celaya
19834f6715 Applied API role specs to domains list 2021-01-04 15:55:59 +01:00
Alejandro Celaya
262a06f624 Renamed method to be more consistent to what it actually does 2021-01-04 15:16:51 +01:00
Alejandro Celaya
a01e0ba337 Changed logic to list domains to centralize conditions in service 2021-01-04 15:02:37 +01:00
Alejandro Celaya
364be2420b Applied API role specs to short URL creation when findIfExists is provided 2021-01-04 13:54:38 +01:00
Alejandro Celaya
29cdfaed39 Changed ShortUrlMeta so that it expects an ApiKey instance instead of the key as string 2021-01-04 13:32:44 +01:00
Alejandro Celaya
24f7fb9c4f Applied API role specs to tags list without stats 2021-01-04 12:44:29 +01:00
Alejandro Celaya
68c601a5a8 Applied API role specs to global visits 2021-01-04 11:27:55 +01:00
Alejandro Celaya
8aa6bdb934 Applied API role specs to tag visits 2021-01-04 11:14:28 +01:00
Alejandro Celaya
4a1e7b761a Applied API role specs to short URL visits 2021-01-03 17:48:32 +01:00
Alejandro Celaya
25ee9b5daf Applied API role specs to single short URL tags edition 2021-01-03 16:50:47 +01:00
Alejandro Celaya
fff10ebee4 Applied API role specs to single short URL edition 2021-01-03 16:41:44 +01:00
Alejandro Celaya
65797b61a0 Applied API role specs to single short URL deletion 2021-01-03 14:03:10 +01:00
Alejandro Celaya
3e565d3830 Removed unnecesary if statements 2021-01-03 13:52:08 +01:00
Alejandro Celaya
dc08286a72 Applied API role specs to single short URL resolution 2021-01-03 13:33:07 +01:00
Alejandro Celaya
940383646b Applied API role specs to short URLs list 2021-01-03 13:05:21 +01:00
Alejandro Celaya
6e1d6ab795 Changed point in which specs are applied for tags list 2021-01-03 12:00:25 +01:00
Alejandro Celaya
df53e6c6f2 Created specs for API key roles 2021-01-02 20:08:49 +01:00
Alejandro Celaya
7e6882960e Added a system to set roles to API keys 2021-01-02 19:35:16 +01:00
Alejandro Celaya
ecf22ae4b6 Added happyr/doctrine-specification to support dunamically applying specs to queries 2021-01-02 17:14:42 +01:00
Alejandro Celaya
90551ff3bc Added used API key to request 2021-01-02 10:34:35 +01:00
Alejandro Celaya
598f2d8622 Merge pull request #950 from acelaya-forks/feature/run-parallel
Feature/run parallel
2021-01-01 11:32:21 +01:00
Alejandro Celaya
f3b4e94def Documented missing composer commands 2021-01-01 11:19:57 +01:00
Alejandro Celaya
6eb3dae8c3 Added dependency on composer parallel to speed-up dev commnds 2021-01-01 11:13:51 +01:00
Alejandro Celaya
09029dff37 Merge pull request #948 from acelaya-forks/feature/cors-improvements
Feature/cors improvements
2020-12-31 15:54:31 +01:00
Alejandro Celaya
9e7f2aea0d Updated changelog 2020-12-31 15:42:00 +01:00
Alejandro Celaya
850a5b412c Removed Access-Control-Expose-Headers header from CrossDomainM;iddleware, as it's actually not correct 2020-12-31 15:41:02 +01:00
Alejandro Celaya
84331135f7 Created API tests for CORS 2020-12-31 13:28:06 +01:00
Alejandro Celaya
202a7327d3 Updated more deps to increase PHP 8 compatibility 2020-12-24 10:37:07 +01:00
Alejandro Celaya
f42e2d87b3 Small update in docker docs 2020-12-22 16:12:39 +01:00
Alejandro Celaya
22124aced7 Updated more dependencies for PHP 8 compatibility 2020-12-22 09:34:58 +01:00
Alejandro Celaya
40676f2167 Removed scrutinizer coverage 2020-12-19 10:37:28 +01:00
Alejandro Celaya
d7b4720327 Merge pull request #936 from acelaya-forks/feature/php8-on-mutation
Added PHP 8 on mutation tests
2020-12-19 10:36:53 +01:00
Alejandro Celaya
3a4a2e4483 Replaced scrutinizer with codecov 2020-12-19 10:25:19 +01:00
Alejandro Celaya
71a83aa384 Added PHP 8 on mutation tests 2020-12-19 10:04:00 +01:00
Alejandro Celaya
291393eeeb Fixed branch for build badge 2020-12-13 18:07:13 +01:00
Alejandro Celaya
ea06c369b0 Merge pull request #933 from acelaya-forks/feature/ci-github-action
Feature/ci GitHub action
2020-12-13 17:56:50 +01:00
Alejandro Celaya
625c870417 Added step to build docker image, and deleted travis config file 2020-12-13 17:45:48 +01:00
Alejandro Celaya
a9e9f89799 Ensured code is cloned before using ocular to upload code coverage to scrutinizer during ci workflow 2020-12-13 17:31:22 +01:00
Alejandro Celaya
f2210ca0cb Added coverage driver to upload coverage job 2020-12-13 17:23:58 +01:00
Alejandro Celaya
1a42ca9239 Added missing dependency between upload coverage job and test jobs 2020-12-13 17:17:16 +01:00
Alejandro Celaya
53726bc679 Added steps to upload code coverage and delete artifacts to ci workflow 2020-12-13 13:34:22 +01:00
Alejandro Celaya
d8a7f3e08c Added mutation-tests step in ci workflow 2020-12-13 13:11:41 +01:00
Alejandro Celaya
ac5a22a3d0 Added static analysis and generation of code coverage artifacts 2020-12-13 12:59:06 +01:00
Alejandro Celaya
5dc2c1640a Added command to create mssql database for tests 2020-12-13 12:47:17 +01:00
Alejandro Celaya
7fe7354a27 Ensured mssql odbc installation is done as super user 2020-12-13 12:38:12 +01:00
Alejandro Celaya
ac85b913c2 Added other database test envs to ci workflow 2020-12-13 12:31:34 +01:00
Alejandro Celaya
0e58d1a242 Added pcov as code coverage driver in github action 2020-12-13 11:37:45 +01:00
Alejandro Celaya
5040f5b177 Changed condition to determine if tests are run in CI 2020-12-13 11:07:37 +01:00
Alejandro Celaya
77deb9c111 Created first version of the ci workflow 2020-12-13 10:44:02 +01:00
Alejandro Celaya
74bafefa68 Merge pull request #931 from acelaya-forks/feature/installer-update-option
Feature/installer update option
2020-12-11 22:00:27 +01:00
Alejandro Celaya
d564404bfe Updated changelog 2020-12-11 21:43:43 +01:00
Alejandro Celaya
b2658073b3 Created script to update config options 2020-12-11 21:42:40 +01:00
Alejandro Celaya
63bd95a123 Merge pull request #928 from acelaya-forks/feature/php8-support
Feature/php8 support
2020-12-06 12:00:45 +01:00
Alejandro Celaya
40105d7aaf Updated to latest swoole and pdo_sqlsrv extensions 2020-12-06 11:41:27 +01:00
Alejandro Celaya
c78991761f Fixed quotes in travis config 2020-12-06 11:29:23 +01:00
Alejandro Celaya
b7a0d319b3 Updated more dependencies to support PHP8 2020-12-04 18:50:00 +01:00
Alejandro Celaya
55bfa9776a Updated to shlinkio/shlink-event-dispatcher 1.6 2020-12-03 23:25:27 +01:00
Alejandro Celaya
d3a4ed607c Replaced --ignore-platform-reqs by --ignore-platform-req=php when running build on PHP 8 2020-12-03 22:27:25 +01:00
Alejandro Celaya
8c79619ff2 Updated to PHP8 compatible versions of symfony/mercure and pugx/shortid-php 2020-12-03 22:26:33 +01:00
Alejandro Celaya
6bedca4ee6 Added more tests covering unicode in custom slugs 2020-12-02 18:45:57 +01:00
Alejandro Celaya
9857f105ec Merge pull request #926 from acelaya-forks/feature/custom-slug-unicode
Feature/custom slug unicode
2020-12-02 12:12:31 +01:00
Alejandro Celaya
7ac1c32ad6 Fixed typo 2020-12-02 12:02:49 +01:00
Alejandro Celaya
6e9fa6553d Updated changelog 2020-12-02 12:01:35 +01:00
Alejandro Celaya
55ea8a6912 #896 Added support for unicode characters in custom slugs 2020-12-02 12:00:47 +01:00
Alejandro Celaya
179ddc5bd7 Merge pull request #925 from acelaya-forks/feature/db-socket-connection
Feature/db socket connection
2020-11-29 20:08:51 +01:00
Alejandro Celaya
bfd886604e Updated changelog 2020-11-29 19:50:39 +01:00
Alejandro Celaya
f34033aa9c Documented how to provide the unix socket to connect to mysql, maria and postgres databases 2020-11-29 19:46:34 +01:00
Alejandro Celaya
e54745b250 #833 Enabled unix socket option during installation 2020-11-29 14:01:26 +01:00
Alejandro Celaya
1975a35837 Updated to lcobucci/json 4.0 stable 2020-11-29 12:54:22 +01:00
Alejandro Celaya
5db66dcf0e Merge pull request #923 from acelaya-forks/feature/qr-codes-query-size
Feature/qr codes query size
2020-11-27 18:00:01 +01:00
Alejandro Celaya
cfdf2f9480 #917 Updated changelog 2020-11-27 17:50:09 +01:00
Alejandro Celaya
c13adb04ef #917 Documented QR endpoint with query size and path size 2020-11-27 17:47:52 +01:00
Alejandro Celaya
4f1ab977a1 #917 Added tests covering the different ways to provide sizes to the QR codes 2020-11-27 17:42:33 +01:00
Alejandro Celaya
fe59a5ad86 #917 Fixed cast to int on QR code action 2020-11-27 17:16:54 +01:00
Alejandro Celaya
a72dc16d85 #917 2020-11-27 17:05:13 +01:00
Alejandro Celaya
74108a19e5 Merge pull request #915 from acelaya-forks/feature/remove-plates
Feature/remove plates
2020-11-22 18:42:19 +01:00
Alejandro Celaya
abe0fc16df #912 Updated changelog 2020-11-22 18:13:12 +01:00
Alejandro Celaya
39bda5113b #912 Fixed unit tests 2020-11-22 18:11:31 +01:00
Alejandro Celaya
49ea5cc78b #912 Removed dependency on league/plates 2020-11-22 18:03:27 +01:00
Alejandro Celaya
8acde332b2 Merge pull request #914 from acelaya-forks/feature/mercure-10-compat
Feature/mercure 10 compat
2020-11-22 16:41:26 +01:00
Alejandro Celaya
600f7a7388 #869 Updated changelog 2020-11-22 16:27:24 +01:00
Alejandro Celaya
fd007ea4a9 #869 Updated dependencies to support mercure 0.10 2020-11-22 16:26:17 +01:00
Alejandro Celaya
b66922b3d5 Ensured lcobucci/jwt stays in alpha 2020-11-22 10:44:13 +01:00
Alejandro Celaya
7d981434e1 Merge pull request #910 from acelaya-forks/feature/swoole-bug
Feature/swoole bug
2020-11-22 10:41:10 +01:00
Alejandro Celaya
c672d35b4a #827 Updated changelog 2020-11-22 10:26:18 +01:00
Alejandro Celaya
6259c73b33 #827 Fixed swoole config getting loaded on non-swoole contexts when running CLI command first 2020-11-22 10:24:06 +01:00
Alejandro Celaya
e4b00e832a Merge pull request #909 from acelaya-forks/feature/geolite-temp-dir
Feature/geolite temp dir
2020-11-21 12:48:28 +01:00
Alejandro Celaya
a452aeaf7e #899 Updated changelog 2020-11-21 12:38:14 +01:00
Alejandro Celaya
6e83b90028 #899 Changed temp directory in which geolite DB files are downloaded 2020-11-21 12:36:30 +01:00
Alejandro Celaya
45ffdce312 Merge pull request #908 from acelaya-forks/feature/domains-list
Feature/domains list
2020-11-21 09:46:16 +01:00
Alejandro Celaya
5485efc9ae #901 Fixed condition type 2020-11-21 08:51:30 +01:00
Alejandro Celaya
850360dd2b #901 Updated changelog 2020-11-21 08:45:57 +01:00
Alejandro Celaya
8d3ceaf462 #901 Ensured only domains in use are returned to lists 2020-11-21 08:44:28 +01:00
Alejandro Celaya
bb6c5de697 Merge pull request #907 from acelaya-forks/feature/missing-swagger-info
Feature/missing swagger info
2020-11-21 08:18:14 +01:00
Alejandro Celaya
ca4c1b00dc #904 Updated changelog 2020-11-21 08:16:22 +01:00
Alejandro Celaya
dda6d30c12 #904 Explicitly added missing Domains and Integrations tags to swagger docs 2020-11-21 08:13:29 +01:00
Alejandro Celaya
4515a83e9b Fixed github action syntax 2020-11-10 19:06:50 +01:00
Alejandro Celaya
907a282b73 Merge pull request #894 from acelaya-forks/feature/docker-publish-action
Feature/docker publish action
2020-11-10 19:05:24 +01:00
Alejandro Celaya
5154638ddf Added v2.4.1 to changelog 2020-11-10 19:04:08 +01:00
Alejandro Celaya
52c9994eb4 #890 Migrated to official docker actions for docker-image-build workflow 2020-11-10 19:03:14 +01:00
Alejandro Celaya
912f287a27 Merge pull request #893 from acelaya-forks/feature/wrong-redirect-status
Feature/wrong redirect status
2020-11-10 18:59:09 +01:00
Alejandro Celaya
e99ab66afd Updated changelog 2020-11-10 18:33:33 +01:00
Alejandro Celaya
fb022eae68 #867 Changed use of deprecated functions by their replacements 2020-11-10 18:13:24 +01:00
Alejandro Celaya
259c52a698 #867 Ensured status code config is honored when doing not-found redirects 2020-11-10 18:08:25 +01:00
Alejandro Celaya
deeca582db #867 Small refactoring on NotFoundRedirecthandler 2020-11-10 17:30:14 +01:00
Alejandro Celaya
4dbcf6857e Merge pull request #892 from acelaya-forks/feature/fix-typehint
Feature/fix typehint
2020-11-10 17:28:10 +01:00
Alejandro Celaya
5190a03113 #846 Fixed base image used for PHP-FPM dev container 2020-11-10 16:08:22 +01:00
Alejandro Celaya
d60c3a4aa9 #891 Updated changelog 2020-11-10 15:51:04 +01:00
Alejandro Celaya
ce1c70fd7c #891 Fixed wrong return type hint on method inside migration when using postgres 2020-11-10 15:49:05 +01:00
Alejandro Celaya
29bb201581 Merge pull request #889 from shlinkio/develop
Release v2.4.0
2020-11-08 12:27:50 +01:00
Alejandro Celaya
006ec7c1d0 Added v2.4 to changelog 2020-11-08 12:14:41 +01:00
Alejandro Celaya
1bc9e0643d Merge pull request #888 from acelaya-forks/feature/simplify-auth-checks
Deleted everything related with authentication plugins, as shlink onl…
2020-11-07 13:02:51 +01:00
Alejandro Celaya
d6395a3de8 Deleted everything related with authentication plugins, as shlink only supports API key auth since v2.0.0 2020-11-07 12:53:14 +01:00
Alejandro Celaya
098751d256 Fixed link in changelog 2020-11-07 10:54:21 +01:00
Alejandro Celaya
8577d6bd99 Merge pull request #887 from acelaya-forks/feature/track-url-creator
Feature/track url creator
2020-11-07 10:40:43 +01:00
Alejandro Celaya
fe4e171ecb Removed unused mock 2020-11-07 10:30:25 +01:00
Alejandro Celaya
d99ea82761 Added migrations folder to the static analysis 2020-11-07 10:27:35 +01:00
Alejandro Celaya
27bc8d4823 Ensured API key is tracked when creating short URLs from the REST API 2020-11-07 10:23:08 +01:00
Alejandro Celaya
7c9f572eb1 Deleted old domain resolvers and added tests for new short url relation resolvers 2020-11-07 09:49:09 +01:00
Alejandro Celaya
2732b05834 Added mechanisms to be able to provide the API key when creating a short URL 2020-11-07 09:34:10 +01:00
Alejandro Celaya
97f89bcede Simplified transactional URL shortening 2020-11-06 20:05:57 +01:00
Alejandro Celaya
00255b04eb Added migration to create new author_api_key_id in short_urls 2020-11-06 19:43:05 +01:00
Alejandro Celaya
f90ea4bd98 Updated dependencies 2020-11-06 18:58:07 +01:00
Alejandro Celaya
0d7fb1163a Merge pull request #886 from acelaya-forks/feature/update-dependencies
Updated dependencies
2020-11-02 12:17:55 +01:00
Alejandro Celaya
cb340b5867 Updated phpunit configs to use new schema introduced in v9.3 2020-11-02 12:07:45 +01:00
Alejandro Celaya
1621f3a943 Updated dependencies 2020-11-02 11:53:14 +01:00
Alejandro Celaya
ae636aef5a Merge pull request #885 from acelaya-forks/feature/deprecate-create-tag
Feature/deprecate create tag
2020-11-02 11:17:30 +01:00
Alejandro Celaya
1346d7902e Updated changelog 2020-11-02 11:06:41 +01:00
Alejandro Celaya
544836b986 Deprecated tags creation 2020-11-02 11:05:14 +01:00
Alejandro Celaya
397f7d09e3 Merge pull request #884 from acelaya-forks/feature/missing-docker-extension
Feature/missing docker extension
2020-11-02 09:50:52 +01:00
Alejandro Celaya
efa707c676 Updated changelog 2020-11-02 09:25:17 +01:00
Alejandro Celaya
51c8b80489 Changed to for consistency in the Dockerfile 2020-11-02 09:24:14 +01:00
Alejandro Celaya
e71fb0ac7f Added gmp extension to docker images, as it seems to be required by geolite in some cases 2020-11-02 09:02:00 +01:00
Alejandro Celaya
681b7c836d Added swoole extension to publish-release github action 2020-11-01 11:47:04 +01:00
Alejandro Celaya
7c2c90fc49 Merge pull request #879 from acelaya-forks/feature/github-release-action
Feature/GitHub release action
2020-11-01 11:42:08 +01:00
Alejandro Celaya
ebe6a5f4aa Moved github release creation from travis to github action 2020-11-01 11:23:11 +01:00
Alejandro Celaya
65651e4bbd Updated changelog to more strictly endorse to keepachangelog spec 2020-11-01 11:22:29 +01:00
Alejandro Celaya
33190c07c7 Updated references from travis-ci.org to travis-ci.com 2020-10-31 08:25:03 +01:00
Alejandro Celaya
f651b0e5a1 Merge pull request #873 from acelaya-forks/feature/disable-platform-checks
Disabled platform checks in composer
2020-10-29 17:28:18 +01:00
Alejandro Celaya
c85eb84b4c Disabled platform checks in composer 2020-10-29 17:24:12 +01:00
Alejandro Celaya
86d428184e Merge pull request #866 from acelaya-forks/feature/composer-2
Updated to composer 2
2020-10-26 19:47:35 +01:00
Alejandro Celaya
c1529b7d6c Updated to composer 2 2020-10-25 17:59:37 +01:00
Alejandro Celaya
7ecc3aacc4 Merge pull request #865 from acelaya-forks/feature/importer
Feature/importer
2020-10-25 14:17:50 +01:00
Alejandro Celaya
b091bd4e2a Ensured composer 1 for now 2020-10-25 13:46:39 +01:00
Alejandro Celaya
90b4bc9b1a Updated changelog 2020-10-25 13:36:21 +01:00
Alejandro Celaya
de7096010e Created DoctrineBatchHelperTest 2020-10-25 13:30:18 +01:00
Alejandro Celaya
03a9697298 Created ImportedLinksProcessorTest 2020-10-25 13:20:34 +01:00
Alejandro Celaya
fdcf88de67 Added database tests for ShortUrlRepository::importedUrlExists 2020-10-25 12:06:48 +01:00
Alejandro Celaya
7c343f42c1 Improved how existing imported short URLs are checked by tracking its original short code 2020-10-25 11:57:26 +01:00
Alejandro Celaya
786e4f642b Moved short code uniqueness checks to external helper class that is used in UrlShortener and ImportedLinksProcessor 2020-10-25 11:16:42 +01:00
Alejandro Celaya
b1a073b1ab Ensured uniqueness on imported short URLs short code 2020-10-25 10:26:11 +01:00
Alejandro Celaya
2256f6a9e7 Added feedback to ImportedLinksProcessor 2020-10-24 15:09:46 +02:00
Alejandro Celaya
ec3e7212b2 Basic short-úrl import implementation 2020-10-24 13:55:54 +02:00
Alejandro Celaya
554d9b092f Added import_source column in ShortUrls 2020-10-23 12:59:39 +02:00
Alejandro Celaya
33d3837795 Added dependency on shlinkio/shlink-importer 2020-10-22 18:12:22 +02:00
Alejandro Celaya
0686ac2fb1 Merge pull request #857 from acelaya-forks/feature/php8
Feature/php8
2020-10-16 20:14:57 +02:00
Alejandro Celaya
ce3d267572 Updated changelog 2020-10-16 19:54:09 +02:00
Alejandro Celaya
4ec90e02c9 Updated to latest infection 2020-10-16 19:53:05 +02:00
Alejandro Celaya
e7bccb088d Updated to latest swoole and pdo_sqlsrv versions which are compatible with PHP8 2020-10-16 19:28:57 +02:00
Alejandro Celaya
cbc9f1257d Enabled Diactoros as module 2020-10-16 19:21:40 +02:00
Alejandro Celaya
c7f15b77fd Merge pull request #853 from dlondero/phpunit-static-assertions
PHPUnit static assertions
2020-10-04 09:44:18 +02:00
Daniel Londero
a8b0c46142 Fix typo 2020-10-04 00:35:29 +02:00
Daniel Londero
065d314608 Invoke PHPUnit's assertions statically 2020-10-04 00:35:14 +02:00
Alejandro Celaya
d426dbc684 Merge pull request #850 from acelaya-forks/feature/env-docker-port
Feature/env docker port
2020-10-03 12:12:25 +02:00
Alejandro Celaya
c6c78f383f Updated changelog 2020-10-03 11:56:09 +02:00
Alejandro Celaya
450eea64aa Added support for port option in SimplifiedConfigParser 2020-10-03 11:54:31 +02:00
Alejandro Celaya
c8d7413dd4 Documented support for PORT env var in Docker image 2020-10-03 11:52:27 +02:00
Alejandro Celaya
00a96e6215 Allowed to change swoole port in docker image by using the PORT env var 2020-10-03 11:49:25 +02:00
Alejandro Celaya
b15e90408f Merge pull request #849 from acelaya-forks/feature/domains-endpoint
Feature/domains endpoint
2020-09-27 12:59:54 +02:00
Alejandro Celaya
34c10c0bc9 Updated changelog 2020-09-27 12:50:03 +02:00
Alejandro Celaya
63a24342e3 Created unit test for ListDomainsCommand 2020-09-27 12:48:24 +02:00
Alejandro Celaya
073e4eeac8 Created command to list domains 2020-09-27 12:39:02 +02:00
Alejandro Celaya
06eda073bf Added API test for /domains endpoint 2020-09-27 10:23:17 +02:00
Alejandro Celaya
614e1c37f8 Added database test for Domainrepository 2020-09-27 10:18:49 +02:00
Alejandro Celaya
24aab5cc0e Created unit tests for new Domain-related elements 2020-09-27 10:11:41 +02:00
Alejandro Celaya
76d6d9a7a9 Created rest endpoint to list existing domains 2020-09-27 09:53:12 +02:00
Alejandro Celaya
8109ceb7eb Merge pull request #845 from acelaya-forks/feature/api-test-coverage
Feature/api test coverage
2020-09-26 11:33:36 +02:00
Alejandro Celaya
6163e34327 Directly run API tests on travis, because they get stuck when run through composer 2020-09-26 11:16:35 +02:00
Alejandro Celaya
84b291e310 Added message with exit code in API tests script 2020-09-26 11:07:02 +02:00
Alejandro Celaya
20cd5cd752 Updated changelog 2020-09-26 10:54:52 +02:00
Alejandro Celaya
d9d57743e6 Fixed code copverage on API tests being exported as Clover instead of PHP 2020-09-26 10:49:56 +02:00
Alejandro Celaya
cc57dcd01a Added code coverage to API tests 2020-09-26 10:43:50 +02:00
Alejandro Celaya
10fbf8f8ff Merge pull request #843 from acelaya-forks/feature/runtime-validation-flag
Feature/runtime validation flag
2020-09-24 22:29:32 +02:00
Alejandro Celaya
cfc9a1b772 Ensure string casting safety 2020-09-24 22:15:26 +02:00
Alejandro Celaya
2555424124 Updated changelog 2020-09-24 22:04:38 +02:00
Alejandro Celaya
405369824b Added hability to override URL validation from the CLI 2020-09-24 21:54:03 +02:00
Alejandro Celaya
cdd87f5962 Documented validateUrl params on create/edit short URL endpoints 2020-09-23 19:24:15 +02:00
Alejandro Celaya
d5eac3b1c3 Added validateUrl optional flag for create/edit short URLs 2020-09-23 19:19:17 +02:00
Alejandro Celaya
1f78f5266a Merge pull request #842 from acelaya-forks/feature/find-if-exists-performance
Feature/find if exists performance
2020-09-23 08:01:02 +02:00
Alejandro Celaya
aa0124f4e9 Moved API tests back to composer ci command 2020-09-23 07:49:59 +02:00
Alejandro Celaya
641f35ae05 Updated changelog 2020-09-23 07:46:25 +02:00
Alejandro Celaya
4e94f07050 Added tests for new ShortUrlRepository::findOneMatching method 2020-09-23 07:34:36 +02:00
Alejandro Celaya
460ca032d2 Drastically improved performance when creating new short URLs with findIfExists by moving logic to DB query 2020-09-23 00:22:29 +02:00
Alejandro Celaya
8d438aa6aa Merge pull request #841 from acelaya-forks/feature/svg-qr-codes
Feature/svg qr codes
2020-09-21 23:05:11 +02:00
Alejandro Celaya
504d08101a Updated changelog 2020-09-21 22:55:18 +02:00
Alejandro Celaya
4b7184ac85 Added tests for new QR code format 2020-09-21 22:54:05 +02:00
Alejandro Celaya
55d9f2a4a1 Added support to return the QR code in SVG format 2020-09-21 22:48:52 +02:00
Alejandro Celaya
319b790628 Merge pull request #840 from acelaya-forks/feature/extended-ordering-support
Feature/extended ordering support
2020-09-21 22:19:55 +02:00
Alejandro Celaya
ee563978ac Updated changelog 2020-09-21 22:06:41 +02:00
Alejandro Celaya
be71a6eeb4 Replaced colon by hyphen as the ordering field-dir separator as it's a valid URL character 2020-09-21 22:03:43 +02:00
Alejandro Celaya
25fbbee883 Added support to order short urls liusts using the <field>:<dir> notaiton as string 2020-09-20 13:21:21 +02:00
Alejandro Celaya
8dbd9ca33d Merge pull request #824 from shlinkio/develop
Release v2.3.0
2020-08-09 11:47:57 +02:00
Alejandro Celaya
cad8c7ed48 Added v2.3.0 to changelog 2020-08-09 11:42:26 +02:00
Alejandro Celaya
c11c731bef Merge pull request #823 from acelaya-forks/feature/docker-updates
Feature/docker updates
2020-08-09 11:41:16 +02:00
Alejandro Celaya
a79362d520 Updated changelog 2020-08-09 11:14:50 +02:00
Alejandro Celaya
c708df2029 Updated to latest docker 2020-08-09 11:13:14 +02:00
Alejandro Celaya
e0760c371a Merge pull request #821 from acelaya-forks/feature/slug-regex
Feature/slug regex
2020-08-09 10:55:32 +02:00
Alejandro Celaya
714a58945e Fixed access to magic method that no longer exists 2020-08-09 10:46:44 +02:00
Alejandro Celaya
87e8ae7af6 Moved custom salugs regex to constant 2020-08-09 10:24:59 +02:00
Alejandro Celaya
a66dca4f07 Merge branch 'develop' of github.com:shlinkio/shlink into develop 2020-07-31 21:44:18 +02:00
Alejandro Celaya
9853b0916f Merge pull request #817 from acelaya-forks/feature/gh-action-docker-build
Feature/gh action docker build
2020-07-31 21:43:21 +02:00
Alejandro Celaya
18afd92fc3 Fixed how docker image version is extracted from github ref 2020-07-31 21:32:06 +02:00
Alejandro Celaya
0474b32c34 Recovered real docker image on docker build script 2020-07-31 21:25:42 +02:00
Alejandro Celaya
ca6fb1c656 Merge pull request #15 from acelaya-forks/feature/gh-action-docker-build
Feature/gh action docker build
2020-07-31 20:42:30 +02:00
Alejandro Celaya
a7a69506a0 Fixed how docker credentials are read from secrets 2020-07-31 20:41:39 +02:00
Alejandro Celaya
a32651aab3 Replace -u by --username on docker login command 2020-07-31 20:30:30 +02:00
Alejandro Celaya
977af0ee43 Fixed pattern for tags on github action 2020-07-31 20:24:44 +02:00
Alejandro Celaya
53bbcd34a6 Replaced built image by lab one while testing functionality 2020-07-31 20:19:46 +02:00
Alejandro Celaya
1eb9ef0361 Moved docker image build to github actions 2020-07-31 20:17:14 +02:00
Alejandro Celaya
1ac05fd3a4 Update CONTRIBUTING.md 2020-07-26 22:10:26 +02:00
Alejandro Celaya
4aef0fa728 Merge pull request #813 from acelaya-forks/feature/php8-ci
Added Builds on PHP nightly
2020-07-24 11:00:43 +02:00
Alejandro Celaya
f4da1b0a2e Fixed wrong regexes in phpstan.neon 2020-07-23 16:53:28 +02:00
Alejandro Celaya
163839494b Added Builds on PHP nightly 2020-07-23 16:34:25 +02:00
Alejandro Celaya
8a811c5b33 Merge pull request #809 from acelaya-forks/feature/list-all-command
Feature/list all command
2020-07-14 15:50:29 +02:00
Alejandro Celaya
007139e4ff Updated changelog 2020-07-14 15:37:21 +02:00
Alejandro Celaya
6be0310933 Improved command flag description 2020-07-14 15:31:18 +02:00
Alejandro Celaya
5f9b629676 Added test for short URLs with all items 2020-07-14 13:28:38 +02:00
Alejandro Celaya
8e84b0e8ac Ensured page footer on list short URLs is not displayed when printing all URLs 2020-07-14 13:14:53 +02:00
Alejandro Celaya
3ff9e101a8 Added support to print all short URLs at once from CLI 2020-07-14 13:00:56 +02:00
Alejandro Celaya
71570af7db Merge pull request #808 from acelaya-forks/feature/trailing-question-mark
Fixed issue introduced with league/uri library
2020-07-10 23:36:16 +02:00
Alejandro Celaya
1401dd9156 Fixed issue introduced with league/uri library 2020-07-10 23:25:31 +02:00
Alejandro Celaya
36c12a69b1 Added project structure explanation to CONTRIBUTING doc 2020-07-08 15:38:12 +02:00
Alejandro Celaya
742e2d724e Updated comment on issue templates 2020-07-06 09:28:31 +02:00
Alejandro Celaya
f74851b0d8 Merge pull request #804 from acelaya-forks/feature/document-tests
Added project tests section to the CONTRIBUTING file
2020-07-01 16:38:46 +02:00
Alejandro Celaya
dd5dcf6ec1 Fixed typo 2020-07-01 16:38:19 +02:00
Alejandro Celaya
a448972e3c Added project tests section to the CONTRIBUTING file 2020-07-01 16:35:25 +02:00
Alejandro Celaya
f784a4f794 Merge pull request #799 from acelaya-forks/feature/guzzle7
Feature/guzzle7
2020-06-28 10:23:20 +02:00
Alejandro Celaya
554a66503f Updated changelog 2020-06-28 10:07:43 +02:00
Alejandro Celaya
73c6c52b2a Updated to guzzle 7 2020-06-28 10:06:49 +02:00
Alejandro Celaya
509672f4c7 Added intl to required PHP extensions 2020-06-27 16:42:17 +02:00
Alejandro Celaya
e4f01e4cf8 Merge pull request #797 from acelaya-forks/feature/deeplinks-support
Feature/deeplinks support
2020-06-27 11:26:35 +02:00
Alejandro Celaya
156eae56d0 Fixed typo in contributing doc 2020-06-27 11:16:59 +02:00
Alejandro Celaya
2df6e694ea Updated changelog 2020-06-27 11:15:17 +02:00
Alejandro Celaya
78b838f6b6 Used league/uri to validate URLs including deeplinks, and fixed tests 2020-06-27 11:14:10 +02:00
Alejandro Celaya
08950f6433 Replaced UriInterface by string when creating a short URL 2020-06-27 10:48:35 +02:00
Alejandro Celaya
a74e1df55c Merge pull request #796 from acelaya-forks/feature/contributing
Feature/contributing
2020-06-27 10:45:09 +02:00
Alejandro Celaya
bf1c6e3d43 Referenced CONTRIBUTING doc from README 2020-06-27 10:43:43 +02:00
Alejandro Celaya
d234e114db Added description on how to create pull requests to CONTRIBUTING file 2020-06-27 10:41:29 +02:00
Alejandro Celaya
035743ef6a Added minor imporovements to CONTRIBUTING file 2020-06-27 10:34:26 +02:00
Alejandro Celaya
c7c9ab71ff Created first draft of the contributing file 2020-06-26 21:22:54 +02:00
Alejandro Celaya
e107aa9ed8 Removed commented migrations option 2020-06-23 19:23:33 +02:00
Alejandro Celaya
e9191732bd Merge pull request #794 from acelaya-forks/feature/migrations3
Feature/migrations3
2020-06-21 13:21:14 +02:00
Alejandro Celaya
f44540f95e Updated changelog 2020-06-21 13:01:10 +02:00
Alejandro Celaya
6b3fd2ac83 Commented out name config option for migrations, since it makes it fail 2020-06-21 13:00:32 +02:00
Alejandro Celaya
eed353fedf Updated migration template 2020-06-21 12:29:56 +02:00
Alejandro Celaya
b4e58cc1bb Updated doctrine config for v3 2020-06-21 12:24:47 +02:00
Alejandro Celaya
56d690d9a6 Removed references to master branch 2020-06-21 12:21:39 +02:00
Alejandro Celaya
bffc044bc7 Fixed typo 2020-06-20 11:34:09 +02:00
Alejandro Celaya
58dd1c54f9 Merge pull request #792 from acelaya-forks/feature/configurable-redirect
Feature/configurable redirect
2020-06-20 11:33:48 +02:00
Alejandro Celaya
5c163490c7 Allowed new redirect config options to be pased as env vars to the docker image 2020-06-20 11:21:37 +02:00
Alejandro Celaya
f2f07be11f Updated to latest installer, supporting redirects customizations 2020-06-20 11:07:15 +02:00
Alejandro Celaya
0bea843e7f Added test covering how redirects config works 2020-06-20 09:50:56 +02:00
Alejandro Celaya
83cc11030d Updated changelog 2020-06-20 09:30:23 +02:00
Alejandro Celaya
cb70dc5389 Removed stuff from local config file which already comes on third party config 2020-06-20 09:20:01 +02:00
Alejandro Celaya
68db52679b Added support to serve redirects with status 301 and Cache-Control 2020-06-17 19:01:56 +02:00
Alejandro Celaya
186168b26c Merge pull request #789 from acelaya-forks/feature/simplified-travis-config
Simplified travis configuration, by removing all env vars checks
2020-06-10 18:05:31 +02:00
Alejandro Celaya
e9c64b46b7 Removed condition from travis that is now implicit 2020-06-10 17:54:41 +02:00
Alejandro Celaya
f476cfc30f Simplified travis configuration, by removing all env vars checks 2020-06-10 17:51:20 +02:00
Alejandro Celaya
3706d6c82d Merge pull request #783 from acelaya-forks/feature/extended-mutation-checks
Feature/extended mutation checks
2020-06-09 12:11:56 +02:00
Alejandro Celaya
248209ab41 Updated changelog 2020-06-08 23:30:19 +02:00
Alejandro Celaya
2867a9b7b0 Added commands to run infection checks on database tests 2020-06-08 23:26:27 +02:00
Alejandro Celaya
68919c19b8 Added deprecation in BodyParserMiddleware 2020-06-08 23:25:54 +02:00
Alejandro Celaya
ee1aa42900 Improved titles on error templates 2020-06-08 23:25:54 +02:00
Alejandro Celaya
c3de39d313 Merge pull request #787 from shlinkio/develop
Release v2.2.2
2020-06-08 23:09:28 +02:00
Alejandro Celaya
8ecc9c69a2 Added v2.2.2 to changelog 2020-06-08 22:49:40 +02:00
Alejandro Celaya
e814f3afcf Merge pull request #784 from acelaya-forks/feature/tag-visits-many-short-urls
Feature/tag visits many short urls
2020-06-08 22:48:52 +02:00
Alejandro Celaya
a4eda9d761 Moved execution of API tests outside composer script 2020-06-08 22:38:51 +02:00
Alejandro Celaya
f3f3ef5c18 Removed unused import 2020-06-08 18:37:45 +02:00
Alejandro Celaya
296134078c Updated changelog 2020-06-08 18:37:45 +02:00
Alejandro Celaya
527faf27a8 Changed how visits for a tag are fetched, avoiding thousands of values to be loaded in memory 2020-06-08 18:37:22 +02:00
Alejandro Celaya
9c339b9c4f Merge pull request #785 from acelaya-forks/feature/improve-custom-slugs
Improved custom slug sluggification, allowing valid URL characters
2020-06-08 18:36:36 +02:00
Alejandro Celaya
f274cafa7c Updated changelog 2020-06-08 18:10:34 +02:00
Alejandro Celaya
371f246c41 Improved custom slug sluggification, allowing valid URL characters 2020-06-08 18:08:53 +02:00
Alejandro Celaya
95ae540799 Defined docker image to build in a var 2020-05-17 10:19:54 +02:00
Alejandro Celaya
f340e0e76e Temporary disabled ARM docker images to reduce build times 2020-05-17 09:37:05 +02:00
Alejandro Celaya
14e0766f72 Merge pull request #773 from acelaya-forks/feature/temporal-build-fix
Going back to single travis job for docker image building
2020-05-16 22:18:03 +02:00
Alejandro Celaya
17f3897746 Going back to single travis job for docker image building 2020-05-16 22:01:20 +02:00
Alejandro Celaya
3c3a30cc0e Merge pull request #772 from acelaya-forks/feature/separate-docker-builds
Separated docker builds in different platforms
2020-05-16 15:15:47 +02:00
Alejandro Celaya
726811f91f Separated docker builds in different platforms 2020-05-16 15:06:37 +02:00
Alejandro Celaya
75f5da5846 Fixed docker install in travis 2020-05-16 14:05:39 +02:00
Alejandro Celaya
489c739be2 Updated condition to run docker publish 2020-05-16 14:00:03 +02:00
Alejandro Celaya
9d6f14c81a Merge pull request #771 from acelaya-forks/feature/build-time-improvements
Changed travis build so that docker image publishing runs on its own …
2020-05-16 13:50:54 +02:00
Alejandro Celaya
788f9635dd Fixed travis config syntax error 2020-05-16 13:40:59 +02:00
Alejandro Celaya
09aa4cc977 Changed travis build so that docker image publishing runs on its own separated job 2020-05-16 13:28:29 +02:00
Alejandro Celaya
9252cc269b Merge pull request #770 from acelaya-forks/feature/multi-arch-improvements
Feature/multi arch improvements
2020-05-16 11:35:56 +02:00
Alejandro Celaya
65e6676c00 Removed docker image building on non-PR builds 2020-05-16 11:25:50 +02:00
Alejandro Celaya
135b62a9cc Documented multi-architecture on docker image 2020-05-16 10:39:47 +02:00
Alejandro Celaya
2ea58acde2 Updated changelog 2020-05-16 10:28:09 +02:00
Alejandro Celaya
e1085f3ef5 Merge pull request #756 from Starbix/multi-arch
Add multi arch support
2020-05-16 10:22:59 +02:00
Cédric Laubacher
f1db195a06 Merge branch 'develop' into multi-arch 2020-05-15 20:37:41 +02:00
Cédric Laubacher
fa646b0176 Add multi arch support 2020-05-15 18:32:35 +02:00
Alejandro Celaya
21ef1dfee8 Merge pull request #765 from acelaya-forks/feature/fix-dates-match
Feature/fix dates match
2020-05-11 13:27:38 +02:00
Alejandro Celaya
5ef548bc2a Updated changelog with v2.2.1 2020-05-11 13:19:01 +02:00
Alejandro Celaya
1fa9896524 Fixed error when trying to match creteria on a Short URL with dates 2020-05-11 13:12:55 +02:00
Alejandro Celaya
cb6756d801 Merge pull request #763 from shlinkio/develop
Release 2.2.0
2020-05-09 11:10:31 +02:00
Alejandro Celaya
cf605407ad Used definitive dependency versions for shlink-common and shlñink-installer 2020-05-09 10:56:07 +02:00
Alejandro Celaya
1a4eee1c81 Merge pull request #762 from acelaya-forks/feature/visits-by-tag
Feature/visits by tag
2020-05-09 10:52:33 +02:00
Alejandro Celaya
4c5cd88041 Updated changelog 2020-05-09 10:38:18 +02:00
Alejandro Celaya
4d346d1fea Created API test for tags visits endpoint 2020-05-09 10:31:39 +02:00
Alejandro Celaya
7f39e6d768 Created TagVisitsActionTest 2020-05-09 10:22:07 +02:00
Alejandro Celaya
9b9de8e290 Updated VisitsTrackerTest 2020-05-09 10:14:26 +02:00
Alejandro Celaya
e1e3c7f061 Created paginator adapter tests 2020-05-09 10:10:48 +02:00
Alejandro Celaya
3218f8c283 Added Created endpoint to serve visits by tag 2020-05-09 09:53:45 +02:00
Alejandro Celaya
f0acce1be0 Updated to latest common 2020-05-09 09:34:59 +02:00
Alejandro Celaya
dd4b4277c9 Added test for VisitRepository tag methods 2020-05-08 20:11:37 +02:00
Alejandro Celaya
baf77b6ffb Implemented methods to get paginated list of visits by tag, reusing methods used for short code filtering 2020-05-08 19:55:05 +02:00
Alejandro Celaya
5be882a31b Improved parameter definition in some private queries in VisitRepository 2020-05-08 19:41:21 +02:00
Alejandro Celaya
ae060f3b13 Merge pull request #761 from acelaya-forks/feature/optional-obfuscation
Feature/optional obfuscation
2020-05-08 16:03:11 +02:00
Alejandro Celaya
e8ab664561 Updated changelog 2020-05-08 15:54:50 +02:00
Alejandro Celaya
f4bf3551f6 Updated shlink-installer to a version supporting IP anonymization param 2020-05-08 15:50:16 +02:00
Alejandro Celaya
8f06e4b20f Replaced references to obfuscate by anonymize 2020-05-08 15:43:09 +02:00
Alejandro Celaya
bfdd6e0c50 Ensured SimplifiedConfigParser properly handles obfuscate_remote_addr option 2020-05-08 13:21:49 +02:00
Alejandro Celaya
ba13d99a71 Allowed remote addr obfuscation to be configured on docker image by using the OBFUSCATE_REMOTE_ADDR env var 2020-05-08 13:19:40 +02:00
Alejandro Celaya
eac468514b Allow to determine if remote addresses should be obfuscated at configuration level 2020-05-08 13:10:58 +02:00
Alejandro Celaya
7da00fbc8c Updated Visit entity so that the address can be optionally obfuscated 2020-05-08 12:58:49 +02:00
Alejandro Celaya
4b7c54d7a9 Merge pull request #760 from acelaya-forks/feature/list-tags-command
Updated ListTagsCommand so that it displays extended information
2020-05-08 12:57:35 +02:00
Alejandro Celaya
c336bb1901 Updated ListTagsCommand so that it displays extended information 2020-05-08 12:39:02 +02:00
Alejandro Celaya
fbb1c449da Merge pull request #759 from acelaya-forks/feature/improved-tags-endpoint
Feature/improved tags endpoint
2020-05-08 12:17:32 +02:00
Alejandro Celaya
252cc7f49d Updated changelog 2020-05-08 11:53:26 +02:00
Alejandro Celaya
00cac4ba72 Created rest test for list tags action 2020-05-08 11:51:28 +02:00
Alejandro Celaya
91aaffc6db Updated ListTagsActionTest 2020-05-08 11:32:06 +02:00
Alejandro Celaya
2e269bcacd Updated TagServiceTest 2020-05-08 11:14:39 +02:00
Alejandro Celaya
bdd14427d9 Added tests for TagRepository::findTagsWithInfo 2020-05-08 11:09:28 +02:00
Alejandro Celaya
06c59fe2dd Fixed invalid imports after class refactoring 2020-05-08 10:29:24 +02:00
Alejandro Celaya
9a78fd1a26 Fixed definition of inversed many to many entity relationship 2020-05-08 10:25:33 +02:00
Alejandro Celaya
626c92460b Enhanced list tags endpoint so that it can also return stats foir every tag 2020-05-08 10:15:33 +02:00
Alejandro Celaya
7e0a14493e Documented updates on the tags endpoint to return more detailed information 2020-05-08 10:14:38 +02:00
Alejandro Celaya
8d23e60d3a Merge pull request #758 from acelaya-forks/feature/non-stable-alpha
Ensured stable tag is not pushed when building docker image for alpha or beta versions
2020-05-07 10:57:52 +02:00
Alejandro Celaya
5f0293bc21 Ensured stable tag is not pushed when building docker image for alpha or beta versions 2020-05-07 10:45:53 +02:00
Alejandro Celaya
afe7381263 Merge pull request #757 from acelaya-forks/feature/docker-img-impr
Feature/docker img impr
2020-05-07 10:31:32 +02:00
Alejandro Celaya
b75922f1d3 Updated changelog 2020-05-07 10:17:34 +02:00
Alejandro Celaya
d9ae83a92b Updated everything related with dependencies in docker images 2020-05-07 10:16:20 +02:00
Alejandro Celaya
22cc9ace4d Merge pull request #755 from acelaya-forks/feature/fix-logged-remote-ip
Feature/fix logged remote ip
2020-05-05 13:04:02 +02:00
Alejandro Celaya
53a37feafe Updated changelogs 2020-05-05 12:54:08 +02:00
Alejandro Celaya
0cab51b01b Enforced mezzio-swoole 2.6.4 or greater 2020-05-05 12:51:47 +02:00
Alejandro Celaya
5f258b6a28 Merge pull request #752 from acelaya-forks/feature/travis-db-tests
Feature/travis db tests
2020-05-04 22:06:04 +02:00
Alejandro Celaya
cc41c51f77 Removed duplicated pdo_sqlsrv enabling on travis config 2020-05-04 21:55:18 +02:00
Alejandro Celaya
5f42266cf2 Moved ms odbc commands to a script 2020-05-04 21:48:54 +02:00
Alejandro Celaya
522d8ed236 Ensured some commands are run as sudo during travis CI 2020-05-04 21:33:19 +02:00
Alejandro Celaya
78359c28c7 Added MS ODBC package installation to travis 2020-05-04 21:22:41 +02:00
Alejandro Celaya
13bb48d068 Installed pdo_sqlsrv extension in travis 2020-05-04 21:12:49 +02:00
Alejandro Celaya
f6d9a83202 Moved initial ci databases to specific docker-compose file 2020-05-04 21:00:09 +02:00
Alejandro Celaya
dfdae96da5 Added commands to initially create all testing database for all database engines in travis 2020-05-04 20:34:28 +02:00
Alejandro Celaya
9f13063b1f Fixed docker-compose command run in travis 2020-05-04 20:02:48 +02:00
Alejandro Celaya
1e8c36b5f1 Updated changelog 2020-05-04 19:55:52 +02:00
Alejandro Celaya
e747a0b250 Updated how database tests are run in travis, so that all DB engines are covered 2020-05-04 19:55:03 +02:00
Alejandro Celaya
79b8834c61 Merge pull request #748 from acelaya-forks/feature/visits-perf-improvements
Feature/visits perf improvements
2020-05-03 20:11:40 +02:00
Alejandro Celaya
313b6a59b9 Updated changelog 2020-05-03 20:02:50 +02:00
Alejandro Celaya
d5288f756e Fixed entity mapping for visits without a visit location 2020-05-03 19:52:40 +02:00
Alejandro Celaya
867659ea25 Created index on visits.date column 2020-05-03 19:15:26 +02:00
Alejandro Celaya
74ad3553cb Hardcoded types on date fields when filtering visits lists 2020-05-03 19:02:13 +02:00
Alejandro Celaya
8b0ce8e6f3 Improved performance when loading visits chuncks at high offsets 2020-05-03 18:20:01 +02:00
Alejandro Celaya
0e4bccc4bb Cached result of the count query on VisitsPaginatorAdapter 2020-05-03 10:44:01 +02:00
Alejandro Celaya
c4ae89a279 Removed DISTINCT when counting visits for a short URL 2020-05-03 10:22:00 +02:00
Alejandro Celaya
80d41db901 Improved performance on query that returns the list of visits for a short URL 2020-05-02 22:47:59 +02:00
Alejandro Celaya
6c30fc73ee Added swoole reverse proxy container 2020-05-02 12:04:42 +02:00
Alejandro Celaya
56932e4ea6 Disabled swoole coroutines 2020-05-01 18:24:48 +02:00
Alejandro Celaya
84b38c4940 Merge pull request #745 from acelaya-forks/feature/general-visits
Feature/general visits
2020-05-01 12:16:22 +02:00
Alejandro Celaya
aece9e68ba Removed logger dependency from rest actions 2020-05-01 12:08:44 +02:00
Alejandro Celaya
d067f52ac2 Updated changelog 2020-05-01 11:58:59 +02:00
Alejandro Celaya
b5947d1642 Created more unit tests 2020-05-01 11:57:46 +02:00
Alejandro Celaya
3232ab401f Documented new visits endpoint 2020-05-01 11:44:55 +02:00
Alejandro Celaya
1ef10f11cb Created new action to get default visit stats 2020-05-01 11:40:02 +02:00
Alejandro Celaya
5beaab85ac Renamed GetVisitsAction to ShortUrlVisitsAction 2020-05-01 11:17:07 +02:00
Alejandro Celaya
4498386f56 Fixed merge conflicts 2020-04-30 20:26:00 +02:00
Alejandro Celaya
704958994d Merge pull request #738 from acelaya-forks/feature/health-fix
Feature/health fix
2020-04-25 20:07:09 +02:00
Alejandro Celaya
a6864bca7c Updated changelog 2020-04-25 20:00:01 +02:00
Alejandro Celaya
15a8305209 Fixed random 503 responses from the HealthAction when the database connection injected on it has expired 2020-04-25 19:58:49 +02:00
Alejandro Celaya
469b70d708 Merge pull request #737 from acelaya-forks/feature/installation-error
Fixed error when cleaning metadata cache during installation with APC…
2020-04-25 19:30:06 +02:00
Alejandro Celaya
4f988d223b Fixed error when cleaning metadata cache during installation with APCu enabled 2020-04-25 19:13:47 +02:00
Alejandro Celaya
e95abc4efb Merge pull request #736 from acelaya-forks/feature/mercure-proxy
Configured an nginx container acting as a reverse proxy for the mercu…
2020-04-25 13:56:07 +02:00
Alejandro Celaya
4917e53acd Configured an nginx container acting as a reverse proxy for the mercure container 2020-04-25 13:44:09 +02:00
Alejandro Celaya
45db4c321a Merge pull request #731 from acelaya-forks/feature/fix-local-sqlite-tests
Ensured mysql config is not loaded for sqlite test envs
2020-04-18 14:06:44 +02:00
Alejandro Celaya
e6d914cfe1 Ensured mysql config is not loaded for sqlite test envs 2020-04-18 14:01:24 +02:00
Alejandro Celaya
85714c931d Merge pull request #730 from acelaya-forks/feature/fix-mysql-buffered-error
Feature/fix mysql buffered error
2020-04-18 13:29:24 +02:00
Alejandro Celaya
66a7f279c2 Updated changelog 2020-04-18 13:22:51 +02:00
Alejandro Celaya
7c6827ea9f Added MYSQL_ATTR_USE_BUFFERED_QUERY driver option with value true for mysql/maria connections 2020-04-18 13:21:46 +02:00
Alejandro Celaya
078c8ea011 Changed default mercure token duration to 1 day 2020-04-18 11:29:49 +02:00
Alejandro Celaya
655fd58a9d Added async API spec file 2020-04-16 22:44:08 +02:00
Alejandro Celaya
6ba6b951bf Changed mercure topics to be dash-cased 2020-04-16 22:25:12 +02:00
Alejandro Celaya
8e0e11f3b3 Merge pull request #727 from acelaya-forks/feature/mercure-improvement
Feature/mercure improvement
2020-04-14 21:16:16 +02:00
Alejandro Celaya
18b12ab1e6 Updated NotifyVisitToMercure to send both an update for all short URLs and one specific short URL 2020-04-14 20:57:25 +02:00
Alejandro Celaya
3908f63b0d Updated to latest installer version 2020-04-14 20:30:05 +02:00
Alejandro Celaya
ca2c32fa8c Removed no-longer used dependencies 2020-04-14 20:24:36 +02:00
Alejandro Celaya
a3a3ac1859 Added missing escaped characters 2020-04-13 13:23:26 +02:00
Alejandro Celaya
f5e0d0c2b1 Merge pull request #726 from acelaya-forks/feature/mercure-integration
Feature/mercure integration
2020-04-13 10:03:12 +02:00
Alejandro Celaya
ba0678946f Updated installer to use a version supporting mercure options 2020-04-13 09:38:18 +02:00
Alejandro Celaya
934fa937b5 Updated config parsers for docker image to accept new mercure env vars and configs 2020-04-12 20:41:23 +02:00
Alejandro Celaya
8d888cb43d Documented how to use a mercure hub when using the docker image 2020-04-12 18:39:28 +02:00
Alejandro Celaya
7f888c49b4 Created MercureUpdatesGeneratorTest 2020-04-12 18:01:13 +02:00
Alejandro Celaya
e97dfbfdda Created NotifyVisitToMercureTest 2020-04-12 17:50:40 +02:00
Alejandro Celaya
b858d79b9e Fixed mercure hub URL returned by MercureInfoAction 2020-04-12 17:50:09 +02:00
Alejandro Celaya
72d8edf4ff Created event listener that notifies mercure hub for new visits 2020-04-12 17:05:59 +02:00
Alejandro Celaya
31db97228d Created MercureInfoActionTest 2020-04-12 14:22:23 +02:00
Alejandro Celaya
2ffbf03cf8 Created action to get mercure integration info 2020-04-12 13:59:10 +02:00
Alejandro Celaya
85440c1c5f Improved mercure-related configs 2020-04-12 12:21:05 +02:00
Alejandro Celaya
69962f1fe8 Added package to handle JWTs 2020-04-11 18:10:56 +02:00
Alejandro Celaya
10cad33248 Added configuration for mercure integration 2020-04-11 18:10:56 +02:00
Alejandro Celaya
0c9deca3f8 Added symfony/mercure package and a container for development 2020-04-11 18:10:56 +02:00
Alejandro Celaya
e1cd4a6ee3 Merge pull request #724 from acelaya-forks/feature/clean-tasks
Created decorator for database connection closing and reopening for s…
2020-04-11 18:09:26 +02:00
Alejandro Celaya
f915b97606 Created decorator for database connection closing and reopening for swoole tasks 2020-04-11 18:00:40 +02:00
367 changed files with 10547 additions and 3399 deletions

View File

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

1
.gitattributes vendored
View File

@@ -10,7 +10,6 @@
.gitattributes export-ignore
.gitignore export-ignore
.phpstorm.meta.php export-ignore
.scrutinizer.yml export-ignore
.travis.yml export-ignore
build.sh export-ignore
CHANGELOG.md export-ignore

View File

@@ -1,6 +1,7 @@
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
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 personal 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.
-->

View File

@@ -5,9 +5,10 @@ labels: bug
---
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
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 personal 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).

View File

@@ -5,9 +5,10 @@ labels: feature
---
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
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 personal 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).

View File

@@ -5,9 +5,10 @@ labels: question
---
<!--
Before opening an issue, just take into account that this is a completely free of charge open source project.
I'm always happy to help and provide support, but some understanding will be required.
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 personal 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).

319
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,319 @@
name: Continuous integration
on:
pull_request: null
push:
branches:
- main
- develop
jobs:
lint:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['7.4']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
extensions: swoole-4.5.9
coverage: none
- run: composer install --no-interaction --prefer-dist
- run: composer cs
static-analysis:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['7.4']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
extensions: swoole-4.5.9
coverage: none
- run: composer install --no-interaction --prefer-dist
- run: composer stan
unit-tests:
runs-on: ubuntu-20.04
continue-on-error: ${{ matrix.php-version == '8.0' }}
strategy:
matrix:
php-version: ['7.4', '8.0']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
extensions: swoole-4.5.9
coverage: pcov
ini-values: pcov.directory=module
- if: ${{ matrix.php-version == '8.0' }}
run: composer install --no-interaction --prefer-dist --ignore-platform-req=php
- if: ${{ matrix.php-version != '8.0' }}
run: composer install --no-interaction --prefer-dist
- run: composer test:unit:ci
- uses: actions/upload-artifact@v2
if: ${{ matrix.php-version == '7.4' }}
with:
name: coverage-unit
path: |
build/coverage-unit
build/coverage-unit.cov
db-tests-sqlite:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['7.4', '8.0']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
extensions: swoole-4.5.9
coverage: pcov
ini-values: pcov.directory=module
- if: ${{ matrix.php-version == '8.0' }}
run: composer install --no-interaction --prefer-dist --ignore-platform-req=php
- if: ${{ matrix.php-version != '8.0' }}
run: composer install --no-interaction --prefer-dist
- run: composer test:db:sqlite:ci
- uses: actions/upload-artifact@v2
if: ${{ matrix.php-version == '7.4' }}
with:
name: coverage-db
path: |
build/coverage-db
build/coverage-db.cov
db-tests-mysql:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['7.4', '8.0']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Start database server
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
extensions: swoole-4.5.9
coverage: none
- if: ${{ matrix.php-version == '8.0' }}
run: composer install --no-interaction --prefer-dist --ignore-platform-req=php
- if: ${{ matrix.php-version != '8.0' }}
run: composer install --no-interaction --prefer-dist
- run: composer test:db:mysql
db-tests-maria:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['7.4', '8.0']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Start database server
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_maria
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
extensions: swoole-4.5.9
coverage: none
- if: ${{ matrix.php-version == '8.0' }}
run: composer install --no-interaction --prefer-dist --ignore-platform-req=php
- if: ${{ matrix.php-version != '8.0' }}
run: composer install --no-interaction --prefer-dist
- run: composer test:db:maria
db-tests-postgres:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['7.4', '8.0']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Start database server
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_postgres
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
extensions: swoole-4.5.9
coverage: none
- if: ${{ matrix.php-version == '8.0' }}
run: composer install --no-interaction --prefer-dist --ignore-platform-req=php
- if: ${{ matrix.php-version != '8.0' }}
run: composer install --no-interaction --prefer-dist
- run: composer test:db:postgres
db-tests-ms:
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['7.4', '8.0']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install MSSQL ODBC
run: sudo ./data/infra/ci/install-ms-odbc.sh
- name: Start database server
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db_ms
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
extensions: swoole-4.5.9, pdo_sqlsrv-5.9.0beta2
coverage: none
- if: ${{ matrix.php-version == '8.0' }}
run: composer install --no-interaction --prefer-dist --ignore-platform-req=php
- if: ${{ matrix.php-version != '8.0' }}
run: composer install --no-interaction --prefer-dist
- name: Create test database
run: docker-compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
- run: composer test:db:ms
api-tests:
runs-on: ubuntu-20.04
continue-on-error: ${{ matrix.php-version == '8.0' }}
strategy:
matrix:
php-version: ['7.4', '8.0']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Start database server
run: docker-compose -f docker-compose.yml -f docker-compose.ci.yml up -d shlink_db
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
extensions: swoole-4.5.9
coverage: pcov
ini-values: pcov.directory=module
- if: ${{ matrix.php-version == '8.0' }}
run: composer install --no-interaction --prefer-dist --ignore-platform-req=php
- if: ${{ matrix.php-version != '8.0' }}
run: composer install --no-interaction --prefer-dist
- run: bin/test/run-api-tests.sh
- uses: actions/upload-artifact@v2
if: ${{ matrix.php-version == '7.4' }}
with:
name: coverage-api
path: |
build/coverage-api
build/coverage-api.cov
mutation-tests:
needs:
- unit-tests
- db-tests-sqlite
- api-tests
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['7.4', '8.0']
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
tools: composer
extensions: swoole-4.5.9
coverage: pcov
ini-values: pcov.directory=module
- if: ${{ matrix.php-version == '8.0' }}
run: composer install --no-interaction --prefer-dist --ignore-platform-req=php
- if: ${{ matrix.php-version != '8.0' }}
run: composer install --no-interaction --prefer-dist
- uses: actions/download-artifact@v2
with:
path: build
- run: composer infect:ci
upload-coverage:
needs:
- unit-tests
- db-tests-sqlite
- api-tests
runs-on: ubuntu-20.04
strategy:
matrix:
php-version: ['7.4']
steps:
- name: Checkout code
uses: actions/checkout@v2
- 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@v2
with:
path: build
- run: mv build/coverage-unit/coverage-unit.cov build/coverage-unit.cov
- run: mv build/coverage-db/coverage-db.cov build/coverage-db.cov
- run: mv build/coverage-api/coverage-api.cov build/coverage-api.cov
- run: wget https://phar.phpunit.de/phpcov-8.2.0.phar
- run: php phpcov-8.2.0.phar merge build --clover build/clover.xml
- name: Publish coverage
uses: codecov/codecov-action@v1
with:
file: ./build/clover.xml
delete-artifacts:
needs:
- mutation-tests
- upload-coverage
runs-on: ubuntu-20.04
steps:
- uses: geekyeggo/delete-artifact@v1
with:
name: |
coverage-unit
coverage-db
coverage-api
build-docker-image:
runs-on: ubuntu-20.04
steps:
- name: Checkout code
uses: actions/checkout@v2
- uses: marceloprado/has-changed-path@v1
id: changed-dockerfile
with:
paths: ./Dockerfile
- if: ${{ steps.changed-dockerfile.outputs.changed == 'true' }}
run: docker build -t shlink-docker-image:temp .
- if: ${{ steps.changed-dockerfile.outputs.changed != 'true' }}
run: echo "Dockerfile didn't change. Skipped"

View File

@@ -0,0 +1,28 @@
name: Build docker image
on:
push:
branches:
- develop
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
with:
version: latest
- name: Login to docker hub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build the image
run: bash ./docker/build

30
.github/workflows/publish-release.yml vendored Normal file
View File

@@ -0,0 +1,30 @@
name: Publish release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Use PHP 7.4
uses: shivammathur/setup-php@v2
with:
php-version: '7.4' # Publish release with lowest supported PHP version
tools: composer
extensions: swoole-4.5.9
- name: Generate release assets
run: ./build.sh ${GITHUB_REF#refs/tags/v}
- name: Publish release with assets
uses: docker://antonyurchenko/git-release:latest
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ALLOW_TAG_PREFIX: "true"
ALLOW_EMPTY_CHANGELOG: "true"
with:
args: |
build/shlink_*_dist.zip

View File

@@ -1,16 +0,0 @@
tools:
external_code_coverage:
timeout: 600
checks:
php:
code_rating: true
duplication: true
build:
dependencies:
override:
- composer install --no-interaction --no-scripts --ignore-platform-reqs
nodes:
analysis:
tests:
override:
- php-scrutinizer-run

View File

@@ -1,64 +0,0 @@
language: php
branches:
only:
- /.*/
php:
- '7.4'
services:
- mysql
- postgresql
- docker
cache:
directories:
- $HOME/.composer/cache/files
before_install:
- echo 'extension = apcu.so' >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
- yes | pecl install swoole-4.4.15
- phpenv config-rm xdebug.ini || return 0
install:
- composer self-update
- composer install --no-interaction --prefer-dist
before_script:
- mysql -e 'CREATE DATABASE shlink_test;'
- psql -c 'create database shlink_test;' -U postgres
- mkdir build
- export DOCKERFILE_CHANGED=$(git diff ${TRAVIS_COMMIT_RANGE:-origin/master} --name-only | grep Dockerfile)
script:
- composer ci
- if [[ ! -z "$DOCKERFILE_CHANGED" && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then docker build -t shlink-docker-image:temp . ; fi
after_success:
- rm -f build/clover.xml
- wget https://phar.phpunit.de/phpcov-7.0.2.phar
- phpdbg -qrr phpcov-7.0.2.phar merge build --clover build/clover.xml
- wget https://scrutinizer-ci.com/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml
# Before deploying, build dist file for current travis tag
before_deploy:
- rm -f ocular.phar
- if [[ ! -z $TRAVIS_TAG && "${TRAVIS_PHP_VERSION}" == "7.4" ]]; then ./build.sh ${TRAVIS_TAG#?} ; fi
deploy:
- provider: releases
api_key:
secure: a9dbZchocqeuOViwUeNH54bQR5Sz7rEYXx5b9WPFtnFn9LGKKUaLbA2U91UQ9QKPrcTpsALubUYbw2CnNmvCwzaY+R8lCD3gkU4ohsEnbpnw3deOeixI74sqBHJAuCH9FSaRDGILoBMtUKx2xlzIymFxkIsgIukkGbdkWHDlRWY3oTUUuw1SQ2Xk9KDsbJQtjIc1+G/O6gHaV4qv/R9W8NPmJExKTNDrAZbC1vIUnxqp4UpVo1hst8qPd1at94CndDYM5rG+7imGbdtxTxzamt819qdTO1OfvtctKawNAm7YXZrrWft6c7gI6j6SI4hxd+ZrrPBqbaRFHkZHjnNssO/yn4SaOHFFzccmu0MzvpPCf0qWZwd3sGHVYer1MnR2mHYqU84QPlW3nrHwJjkrpq3+q0JcBY6GsJs+RskHNtkMTKV05Iz6QUI5YZGwTpuXaRm036SmavjGc4IDlMaYCk/NmbB9BKpthJxLdUpczOHpnjXXHziotWD6cfEnbjU3byfD8HY5WrxSjsNT7SKmXN3hRof7bk985ewQVjGT42O3NbnfnqjQQWr/B7/zFTpLR4f526Bkq12CdCyf5lvrbq+POkLVdJ+uFfR7ds248Ue/jBQy6kM1tWmKF9QiwisFlA84eQ4CW3I93Rp97URv+AQa9zmbD0Ve3Udp+g6nF5I=
file: "./build/shlink_${TRAVIS_TAG#?}_dist.zip"
skip_cleanup: true
on:
tags: true
php: '7.4'
- provider: script
script: bash ./docker/build
on:
all_branches: true
condition: $TRAVIS_PULL_REQUEST == 'false'
php: '7.4'

File diff suppressed because it is too large Load Diff

136
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,136 @@
# Contributing
This file will guide you through the process of getting to project up and running, in case you want to provide coding contributions.
You will also see how to ensure the code fulfills the expected code checks, and how to create a pull request.
## System dependencies
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/).
## Setting up the project
The first thing you need to do is fork the repository, and clone it in your local machine.
Then you will have to follow these steps:
* Copy all files with `.local.php.dist` extension from `config/autoload` by removing the dist extension.
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`.
The first time this command is run, it will create several containers that are used during development, so it may take some time.
It will also create some empty databases and install the project dependencies with composer.
* Run `./indocker bin/cli db:create` to create the initial database.
* 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 swoole.
> Note: The `indocker` shell script is a helper used to run commands inside the main docker container.
## Project structure
This project is structured as a modular application, using [laminas/laminas-config-aggregator](https://github.com/laminas/laminas-config-aggregator) to merge the configuration provided by every module.
All modules are inside the `module` folder, and each one has its own `src`, `test` and `config` folders, with the source code, tests and configuration. They also have their own `ConfigProvider` class, which is consumed by the config aggregator.
This is a simplified version of the project structure:
```
shlink
├── bin
│   ├── cli
│   ├── install
│   └── update
├── config
│   ├── autoload
│   ├── params
│   ├── config.php
│   └── container.php
├── data
│   ├── cache
│   ├── locks
│   ├── log
│   ├── migrations
│   └── proxies
├── docs
│   ├── async-api
│   └── swagger
├── module
│   ├── CLI
│   ├── 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, while `install` and `update` are helper tools used to install and update shlink when not using the docker image.
* `config`: Contains application-wide configurations, which are later merged with the ones provided by every module.
* `data`: Common runtime-generated git-ignored assets, like logs, caches, etc.
* `docs`: Any project documentation is stored here, like API spec definitions.
* `module`: Contains a subfolder 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 swoole.
## Project tests
In order to ensure stability and no regressions are introduced while developing new features, this project has different types of tests.
* **Unit tests**: These are the simplest to run, and usually test individual pieces of code, replacing any external dependency by mocks.
The code coverage of unit tests is pretty high, and only entity repositories are excluded because of their nature.
* **Database tests**: These are integration tests that run against a real database, and only cover entity repositories.
Its purpose is to verify all the database queries behave as expected and return what's expected.
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 and test it from the outside, by interacting with the REST API.
These are the best tests to catch regressions, and to verify everything interacts as expected.
They use MySQL as the database engine, and include some fixtures that ensure the same data exists at the beginning of the execution.
* **CLI tests**: *TBD. Once included, its purpose will be the same as API tests, but running through the command line*
Depending on the kind of contribution, maybe not all kinds of tests are needed, but the more you provide, the better.
## Running code checks
* Run `./indocker composer cs` to check coding styles are fulfilled.
* Run `./indocker composer cs:fix` to fix coding styles (some may not be fixable from the CLI)
* Run `./indocker composer stan` to statically analyze the code with [phpstan](https://phpstan.org/). This tool is the closest to "compile" PHP and verify everything would work as expected.
* Run `./indocker composer test:unit` to run the unit tests.
* Run `./indocker composer test:db` to run the database integration tests.
This command runs the same test suite against all supported database engines. If you just want to run one of them, you can add one of `:sqlite`, `:mysql`, `:maria`, `:postgres`, `:mssql` at the end of the command.
For example, `test:db:postgres`.
* Run `./indocker composer test:api` to run API E2E tests. For these, the MySQL database engine is used.
* Run `./indocker composer infect:test` ti 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. This command is run during the project's continuous integration.
> Note: Due to some limitations in the tooling used by shlink, the testing databases need to exist beforehand, both for db and api tests (except sqlite).
>
> However, they just need to be created empty, with no tables. Also, once created, they are automatically reset before every new execution.
>
> The testing database is always called `shlink_test`. You can create it using the database client of your choice. [DBeaver](https://dbeaver.io/) is a good multi-platform desktop database client which supports all the engines supported by shlink.
## Pull request process
In order to provide pull requests to this project, you should always start by creating a new branch, where you will make all desired changes.
The base branch should always be `develop`, and the target branch for the pull request should also be `develop`.
Before your branch can be merged, all the checks described in [Running code checks](#running-code-checks) have to be passing. You can verify that manually by running `./indocker composer ci`, or wait for the build to be run automatically after the pull request is created.

View File

@@ -1,12 +1,13 @@
FROM php:7.4.2-alpine3.11 as base
FROM php:7.4.11-alpine3.12 as base
ARG SHLINK_VERSION=2.0.5
ARG SHLINK_VERSION=2.4.0
ENV SHLINK_VERSION ${SHLINK_VERSION}
ENV SWOOLE_VERSION 4.4.15
ENV SWOOLE_VERSION 4.5.9
ENV LC_ALL "C"
WORKDIR /etc/shlink
# Install required PHP extensions
RUN \
# Install mysql and calendar
docker-php-ext-install -j"$(nproc)" pdo_mysql calendar && \
@@ -21,25 +22,33 @@ RUN \
docker-php-ext-install -j"$(nproc)" intl && \
# Install zip and gd
apk add --no-cache libzip-dev zlib-dev libpng-dev && \
docker-php-ext-install -j"$(nproc)" zip gd
docker-php-ext-install -j"$(nproc)" zip gd && \
# Install gmp
apk add --no-cache gmp-dev && \
docker-php-ext-install -j"$(nproc)" gmp
# Install swoole and sqlsrv driver
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/mssql-tools_17.5.1.1-1_amd64.apk && \
apk add --allow-untrusted msodbcsql17_17.5.1.1-1_amd64.apk && \
apk add --allow-untrusted mssql-tools_17.5.1.1-1_amd64.apk && \
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
pecl install swoole-${SWOOLE_VERSION} pdo_sqlsrv && \
docker-php-ext-enable swoole pdo_sqlsrv && \
apk del .phpize-deps && \
rm msodbcsql17_17.5.1.1-1_amd64.apk && \
rm mssql-tools_17.5.1.1-1_amd64.apk
# Install sqlsrv driver
RUN if [ $(uname -m) == "x86_64" ]; then \
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
apk add --allow-untrusted msodbcsql17_17.5.1.1-1_amd64.apk && \
apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} unixodbc-dev && \
pecl install pdo_sqlsrv && \
docker-php-ext-enable pdo_sqlsrv && \
apk del .phpize-deps && \
rm msodbcsql17_17.5.1.1-1_amd64.apk ; \
fi
# Install swoole
RUN apk add --no-cache --virtual .phpize-deps ${PHPIZE_DEPS} && \
pecl install swoole-${SWOOLE_VERSION} && \
docker-php-ext-enable swoole && \
apk del .phpize-deps
# Install shlink
FROM base as builder
COPY . .
COPY --from=composer:1.10.1 /usr/bin/composer ./composer.phar
COPY --from=composer:2 /usr/bin/composer ./composer.phar
RUN apk add --no-cache git && \
php composer.phar install --no-dev --optimize-autoloader --prefer-dist --no-progress --no-interaction && \
php composer.phar clear-cache && \
@@ -54,7 +63,7 @@ LABEL maintainer="Alejandro Celaya <alejandro@alejandrocelaya.com>"
COPY --from=builder /etc/shlink .
RUN ln -s /etc/shlink/bin/cli /usr/local/bin/shlink
# Expose swoole port
# Expose default swoole port
EXPOSE 8080
# Expose params config dir, since the user is expected to provide custom config from there

View File

@@ -1,17 +1,18 @@
![Shlink](https://raw.githubusercontent.com/shlinkio/shlink.io/master/public/images/shlink-hero.png)
![Shlink](https://raw.githubusercontent.com/shlinkio/shlink.io/main/public/images/shlink-hero.png)
[![Build Status](https://img.shields.io/travis/shlinkio/shlink.svg?style=flat-square)](https://travis-ci.org/shlinkio/shlink)
[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/shlinkio/shlink.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
[![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/shlinkio/shlink.svg?style=flat-square)](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
[![Build Status](https://img.shields.io/github/workflow/status/shlinkio/shlink/Continuous%20integration/develop?logo=github&style=flat-square)](https://github.com/shlinkio/shlink/actions?query=workflow%3A%22Continuous+integration%22)
[![Code Coverage](https://img.shields.io/codecov/c/gh/shlinkio/shlink/develop?style=flat-square)](https://app.codecov.io/gh/shlinkio/shlink)
[![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?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/master/LICENSE)
[![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)
[![Paypal donate](https://img.shields.io/badge/Donate-paypal-blue.svg?style=flat-square&logo=paypal&colorA=aaaaaa)](https://slnk.to/donate)
A PHP-based self-hosted URL shortener that can be used to serve shortened URLs under your own custom domain.
> This document references Shlink 2.x. If you are using an older version and want to upgrade, follow the [UPGRADE](UPGRADE.md) doc.
> If you are trying to find out how to run the project in development mode or how to provide contributions, read the [CONTRIBUTING](CONTRIBUTING.md) doc.
## Table of Contents
- [Installation](#installation)
@@ -20,6 +21,7 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u
- [Serve](#serve)
- [Bonus](#bonus)
- [Update to new version](#update-to-new-version)
- [Update a configuration option](#update-a-configuration-option)
- [Using a docker image](#using-a-docker-image)
- [Using shlink](#using-shlink)
- [Shlink CLI Help](#shlink-cli-help)
@@ -36,7 +38,7 @@ A PHP-based self-hosted URL shortener that can be used to serve shortened URLs u
First, make sure the host where you are going to run shlink fulfills these requirements:
* PHP 7.4 or greater with JSON, curl, PDO and gd extensions enabled.
* PHP 7.4 or greater with JSON, curl, PDO, intl and gd extensions enabled.
* MySQL, MariaDB, PostgreSQL, Microsoft SQL Server or SQLite.
* The web server of your choice with PHP integration (Apache or Nginx recommended).
@@ -62,7 +64,7 @@ In order to run Shlink, you will need a built version of the project. There are
After that, you will have a `shlink_x.x.x_dist.zip` dist file inside the `build` directory, that you need to decompress in the location fo your choice.
> This is the process used when releasing new shlink versions. After tagging the new version with git, the Github release is automatically created by [travis](https://travis-ci.org/shlinkio/shlink), attaching the generated dist file to it.
> This is the process used when releasing new shlink versions. After tagging the new version with git, the Github release is automatically created by [travis](https://travis-ci.com/shlinkio/shlink), attaching the generated dist file to it.
### Configure
@@ -223,6 +225,16 @@ The `bin/update` will use the location from previous shlink version to import th
**Important!** It is recommended that you don't skip any version when using this process. The update tool gets better on every version, but older versions might make assumptions.
### Update a configuration option
Sometimes you need to update the configuration on your shlink instance. Maybe you want to change the GeoLite2 license key, or move from http to https.
In order to do that, run `bin/set-option` and follow the instructions. You will be asked to select the option to change, and then you will be asked to provide the new value.
This script will take care of updating that value without changing anything else, and it will also delete the configuration cache so that the new value is applied.
> This script will fail if you didn't run `bin/install` at least once.
## Using a docker image
Starting with version 1.15.0, an official docker image is provided. You can learn how to use it by reading [the docs](docker/README.md).

14
bin/set-option Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink;
use Shlinkio\Shlink\Installer\Command\SetOptionCommand;
use function chdir;
use function dirname;
chdir(dirname(__DIR__));
[,, $installer] = require __DIR__ . '/../vendor/shlinkio/shlink-installer/bin/run.php';
$installer(SetOptionCommand::NAME);

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env sh
export APP_ENV=test
export DB_DRIVER=mysql
export TEST_ENV=api
# Try to stop server just in case it hanged in last execution
vendor/bin/mezzio-swoole stop
@@ -9,7 +10,7 @@ echo 'Starting server...'
vendor/bin/mezzio-swoole start -d
sleep 2
phpdbg -qrr vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always $*
vendor/bin/phpunit --order-by=random -c phpunit-api.xml --testdox --colors=always --log-junit=build/coverage-api/junit.xml $*
testsExitCode=$?
vendor/bin/mezzio-swoole stop

View File

@@ -15,61 +15,65 @@
"php": "^7.4",
"ext-json": "*",
"ext-pdo": "*",
"akrabat/ip-address-middleware": "^1.0",
"cakephp/chronos": "^1.2",
"cocur/slugify": "^3.0",
"akrabat/ip-address-middleware": "^2.0",
"cakephp/chronos": "^2.0",
"cocur/slugify": "^4.0",
"doctrine/cache": "^1.9",
"doctrine/dbal": "^2.10",
"doctrine/migrations": "^2.2",
"doctrine/orm": "^2.7",
"doctrine/migrations": "^3.0.2",
"doctrine/orm": "^2.8",
"endroid/qr-code": "^3.6",
"firebase/php-jwt": "^4.0",
"geoip2/geoip2": "^2.9",
"guzzlehttp/guzzle": "^6.5.1",
"guzzlehttp/guzzle": "^7.0",
"happyr/doctrine-specification": "2.0.x-dev#cb116d3 as 2.0",
"laminas/laminas-config": "^3.3",
"laminas/laminas-config-aggregator": "^1.1",
"laminas/laminas-dependency-plugin": "^1.0",
"laminas/laminas-diactoros": "^2.1.3",
"laminas/laminas-inputfilter": "^2.10",
"laminas/laminas-paginator": "^2.8",
"laminas/laminas-servicemanager": "^3.4",
"laminas/laminas-servicemanager": "^3.6",
"laminas/laminas-stdlib": "^3.2",
"lstrojny/functional-php": "^1.9",
"lcobucci/jwt": "^4.0",
"league/uri": "^6.2",
"lstrojny/functional-php": "^1.15",
"mezzio/mezzio": "^3.2",
"mezzio/mezzio-fastroute": "^3.0",
"mezzio/mezzio-fastroute": "^3.1",
"mezzio/mezzio-helpers": "^5.3",
"mezzio/mezzio-platesrenderer": "^2.1",
"mezzio/mezzio-problem-details": "^1.1",
"mezzio/mezzio-swoole": "^2.6",
"mezzio/mezzio-swoole": "^2.6.4",
"monolog/monolog": "^2.0",
"nikolaposa/monolog-factory": "^3.0",
"ocramius/proxy-manager": "^2.7.0",
"phly/phly-event-dispatcher": "^1.0",
"php-middleware/request-id": "^4.0",
"nikolaposa/monolog-factory": "^3.1",
"ocramius/proxy-manager": "^2.11",
"php-middleware/request-id": "^4.1",
"predis/predis": "^1.1",
"pugx/shortid-php": "^0.5",
"pugx/shortid-php": "^0.7",
"ramsey/uuid": "^3.9",
"shlinkio/shlink-common": "^3.0",
"shlinkio/shlink-common": "dev-main#1311861 as 3.4",
"shlinkio/shlink-config": "^1.0",
"shlinkio/shlink-event-dispatcher": "^1.4",
"shlinkio/shlink-installer": "^4.4.0",
"shlinkio/shlink-ip-geolocation": "^1.4",
"symfony/console": "^5.0",
"symfony/filesystem": "^5.0",
"symfony/lock": "^5.0",
"symfony/process": "^5.0"
"shlinkio/shlink-event-dispatcher": "^1.6",
"shlinkio/shlink-importer": "^2.1",
"shlinkio/shlink-installer": "^5.3",
"shlinkio/shlink-ip-geolocation": "^1.5",
"symfony/console": "^5.1",
"symfony/filesystem": "^5.1",
"symfony/lock": "^5.1",
"symfony/mercure": "^0.4.1",
"symfony/process": "^5.1",
"symfony/string": "^5.1"
},
"require-dev": {
"devster/ubench": "^2.0",
"dms/phpunit-arraysubset-asserts": "^0.2.0",
"devster/ubench": "^2.1",
"dms/phpunit-arraysubset-asserts": "^0.2.1",
"eaglewu/swoole-ide-helper": "dev-master",
"infection/infection": "^0.16.1",
"phpstan/phpstan": "^0.12.18",
"phpunit/phpunit": "~9.0.1",
"infection/infection": "^0.20.2",
"phpspec/prophecy-phpunit": "^2.0",
"phpstan/phpstan": "^0.12.64",
"phpunit/php-code-coverage": "^9.2",
"phpunit/phpunit": "^9.5",
"roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.1.0",
"shlinkio/shlink-test-utils": "^1.4",
"symfony/var-dumper": "^5.0"
"shlinkio/php-coding-standard": "~2.1.1",
"shlinkio/shlink-test-utils": "^1.7",
"symfony/var-dumper": "^5.2",
"veewee/composer-run-parallel": "^0.1.0"
},
"autoload": {
"psr-4": {
@@ -90,7 +94,10 @@
"module/Core/test",
"module/Core/test-db"
]
}
},
"files": [
"config/test/constants.php"
]
},
"scripts": {
"ci": [
@@ -99,9 +106,13 @@
"@test:ci",
"@infect:ci"
],
"ci:parallel": [
"@parallel cs stan test:unit:ci test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
"@parallel test:api infect:ci:unit infect:ci:db"
],
"cs": "phpcs",
"cs:fix": "phpcbf",
"stan": "phpstan analyse module/*/src/ module/*/config config docker/config --level=6",
"stan": "phpstan analyse module/*/src/ module/*/config config docker/config data/migrations --level=6",
"test": [
"@test:unit",
"@test:db",
@@ -109,42 +120,33 @@
],
"test:ci": [
"@test:unit:ci",
"@test:db:ci",
"@test:api:ci"
"@test:db",
"@test:api"
],
"test:unit": "phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox",
"test:unit:ci": "@test:unit --coverage-clover=build/clover.xml --coverage-xml=build/coverage-xml --log-junit=build/junit.xml",
"test:db": [
"@test:db:sqlite",
"@test:db:mysql",
"@test:db:maria",
"@test:db:postgres",
"@test:db:ms"
],
"test:db:ci": [
"@test:db:sqlite",
"@test:db:mysql",
"@test:db:postgres"
],
"test:db:sqlite": "APP_ENV=test phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-db.cov --testdox -c phpunit-db.xml",
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox",
"test:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
"test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml",
"test:db:mysql": "DB_DRIVER=mysql composer test:db:sqlite",
"test:db:maria": "DB_DRIVER=maria composer test:db:sqlite",
"test:db:postgres": "DB_DRIVER=postgres composer test:db:sqlite",
"test:db:ms": "DB_DRIVER=mssql composer test:db:sqlite",
"test:api": "bin/test/run-api-tests.sh",
"test:api:ci": "@test:api --coverage-php build/coverage-api.cov",
"test:unit:pretty": "phpdbg -qrr vendor/bin/phpunit --order-by=random --colors=always --coverage-html build/coverage",
"infect": "infection --threads=4 --min-msi=80 --log-verbosity=default --only-covered",
"infect:ci": "@infect --coverage=build --skip-initial-tests",
"infect:show": "@infect --show-mutations",
"test:unit:pretty": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-html build/coverage-unit-html",
"infect:ci:base": "infection --threads=4 --log-verbosity=default --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.json",
"infect:ci": "@parallel infect:ci:unit infect:ci:db",
"infect:test": [
"@test:unit:ci",
"@parallel test:unit:ci test:db:sqlite:ci",
"@infect:ci"
],
"clean:dev": "rm -f data/database.sqlite && rm -f config/params/generated_config.php"
},
"scripts-descriptions": {
"ci": "<fg=blue;options=bold>Alias for \"cs\", \"stan\", \"test:ci\" and \"infect:ci\"</>",
"ci:parallel": "<fg=blue;options=bold>Same as \"ci\", but parallelizing tasks as much as possible</>",
"cs": "<fg=blue;options=bold>Checks coding styles</>",
"cs:fix": "<fg=blue;options=bold>Fixes coding styles, when possible</>",
"stan": "<fg=blue;options=bold>Inspects code with phpstan</>",
@@ -152,21 +154,23 @@
"test:ci": "<fg=blue;options=bold>Runs all test suites, generating all needed reports and logs for CI envs</>",
"test:unit": "<fg=blue;options=bold>Runs unit test suites</>",
"test:unit:ci": "<fg=blue;options=bold>Runs unit test suites, generating all needed reports and logs for CI envs</>",
"test:db": "<fg=blue;options=bold>Runs database test suites on a SQLite, MySQL, MariaDB and PostgreSQL</>",
"test:db:ci": "<fg=blue;options=bold>Runs database test suites on a SQLite, MySQL and PostgreSQL</>",
"test:db": "<fg=blue;options=bold>Runs database test suites on a SQLite, MySQL, MariaDB, PostgreSQL and MsSQL</>",
"test:db:sqlite": "<fg=blue;options=bold>Runs database test suites on a SQLite database</>",
"test:db:sqlite:ci": "<fg=blue;options=bold>Runs database test suites on a SQLite database, generating all needed reports and logs for CI envs</>",
"test:db:mysql": "<fg=blue;options=bold>Runs database test suites on a MySQL database</>",
"test:db:maria": "<fg=blue;options=bold>Runs database test suites on a MariaDB database</>",
"test:db:postgres": "<fg=blue;options=bold>Runs database test suites on a PostgreSQL database</>",
"test:db:ms": "<fg=blue;options=bold>Runs database test suites on a Miscrosoft SQL Server database</>",
"test:api": "<fg=blue;options=bold>Runs API test suites</>",
"test:unit:pretty": "<fg=blue;options=bold>Runs unit test suites and generates an HTML code coverage report</>",
"infect": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing</>",
"infect:ci": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing with existing reports and logs</>",
"infect:show": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing and shows applied mutators</>",
"infect:test": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing</>",
"infect:ci": "<fg=blue;options=bold>Checks unit and db tests quality applying mutation testing with existing reports and logs</>",
"infect:ci:unit": "<fg=blue;options=bold>Checks unit tests quality applying mutation testing with existing reports and logs</>",
"infect:ci:db": "<fg=blue;options=bold>Checks db tests quality applying mutation testing with existing reports and logs</>",
"infect:test": "<fg=blue;options=bold>Runs unit and db tests, then checks tests quality applying mutation testing</>",
"clean:dev": "<fg=blue;options=bold>Deletes artifacts which are gitignored and could affect dev env</>"
},
"config": {
"sort-packages": true
"sort-packages": true,
"platform-check": false
}
}

View File

@@ -7,6 +7,9 @@ use Laminas\ConfigAggregator\ConfigAggregator;
return [
'debug' => false,
ConfigAggregator::ENABLE_CACHE => true,
// Disabling config cache for cli, ensures it's never used for swoole and also that console commands don't generate
// a cache file that's then used by non-swoole web executions
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',
];

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
return [
'cors' => [
'max_age' => 3600,
],
];

View File

@@ -2,7 +2,9 @@
declare(strict_types=1);
use GuzzleHttp\Client;
use Mezzio\Container;
use Psr\Http\Client\ClientInterface;
return [
@@ -13,6 +15,10 @@ return [
],
],
'aliases' => [
ClientInterface::class => Client::class,
],
'lazy_services' => [
'proxies_target_dir' => 'data/proxies',
'proxies_namespace' => 'ShlinkProxy',

View File

@@ -4,12 +4,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Common;
use Happyr\DoctrineSpecification\EntitySpecificationRepository;
return [
'entity_manager' => [
'orm' => [
'proxies_dir' => 'data/proxies',
'load_mappings_using_functional_style' => true,
'default_repository_classname' => EntitySpecificationRepository::class,
],
'connection' => [
'user' => '',

View File

@@ -2,6 +2,14 @@
declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
// When running tests, any mysql-specific option can interfere with other drivers
$driverOptions = env('APP_ENV') === 'test' ? [] : [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
];
return [
'entity_manager' => [
@@ -10,9 +18,7 @@ return [
'password' => 'root',
'driver' => 'pdo_mysql',
'host' => 'shlink_db',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
],
'driverOptions' => $driverOptions,
],
],

View File

@@ -6,8 +6,8 @@ return [
'geolite2' => [
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
'temp_dir' => sys_get_temp_dir(),
'license_key' => 'G4Lm0C60yJsnkdPi',
'temp_dir' => __DIR__ . '/../../data',
'license_key' => 'G4Lm0C60yJsnkdPi', // Deprecated. Remove hardcoded license on v3
],
];

View File

@@ -8,30 +8,38 @@ return [
'installer' => [
'enabled_options' => [
Option\DatabaseDriverConfigOption::class,
Option\DatabaseNameConfigOption::class,
Option\DatabaseHostConfigOption::class,
Option\DatabasePortConfigOption::class,
Option\DatabaseUserConfigOption::class,
Option\DatabasePasswordConfigOption::class,
Option\DatabaseSqlitePathConfigOption::class,
Option\DatabaseMySqlOptionsConfigOption::class,
Option\ShortDomainHostConfigOption::class,
Option\ShortDomainSchemaConfigOption::class,
Option\ValidateUrlConfigOption::class,
Option\VisitsWebhooksConfigOption::class,
Option\BaseUrlRedirectConfigOption::class,
Option\InvalidShortUrlRedirectConfigOption::class,
Option\Regular404RedirectConfigOption::class,
Option\Database\DatabaseDriverConfigOption::class,
Option\Database\DatabaseNameConfigOption::class,
Option\Database\DatabaseHostConfigOption::class,
Option\Database\DatabasePortConfigOption::class,
Option\Database\DatabaseUserConfigOption::class,
Option\Database\DatabasePasswordConfigOption::class,
Option\Database\DatabaseUnixSocketConfigOption::class,
Option\Database\DatabaseSqlitePathConfigOption::class,
Option\Database\DatabaseMySqlOptionsConfigOption::class,
Option\UrlShortener\ShortDomainHostConfigOption::class,
Option\UrlShortener\ShortDomainSchemaConfigOption::class,
Option\UrlShortener\ValidateUrlConfigOption::class,
Option\Visit\VisitsWebhooksConfigOption::class,
Option\Redirect\BaseUrlRedirectConfigOption::class,
Option\Redirect\InvalidShortUrlRedirectConfigOption::class,
Option\Redirect\Regular404RedirectConfigOption::class,
Option\DisableTrackParamConfigOption::class,
Option\CheckVisitsThresholdConfigOption::class,
Option\VisitsThresholdConfigOption::class,
Option\Visit\CheckVisitsThresholdConfigOption::class,
Option\Visit\VisitsThresholdConfigOption::class,
Option\BasePathConfigOption::class,
Option\TaskWorkerNumConfigOption::class,
Option\WebWorkerNumConfigOption::class,
Option\Worker\TaskWorkerNumConfigOption::class,
Option\Worker\WebWorkerNumConfigOption::class,
Option\RedisServersConfigOption::class,
Option\ShortCodeLengthOption::class,
Option\GeoLiteLicenseKeyConfigOption::class,
Option\UrlShortener\ShortCodeLengthOption::class,
Option\Mercure\EnableMercureConfigOption::class,
Option\Mercure\MercurePublicUrlConfigOption::class,
Option\Mercure\MercureInternalUrlConfigOption::class,
Option\Mercure\MercureJwtSecretConfigOption::class,
Option\UrlShortener\GeoLiteLicenseKeyConfigOption::class,
Option\UrlShortener\IpAnonymizationConfigOption::class,
Option\UrlShortener\RedirectStatusCodeConfigOption::class,
Option\UrlShortener\RedirectCacheLifeTimeConfigOption::class,
],
'installation_commands' => [

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
use Laminas\ServiceManager\Proxy\LazyServiceFactory;
use Shlinkio\Shlink\Common\Mercure\LcobucciJwtProvider;
use Symfony\Component\Mercure\Publisher;
use Symfony\Component\Mercure\PublisherInterface;
return [
'mercure' => [
'public_hub_url' => null,
'internal_hub_url' => null,
'jwt_secret' => null,
'jwt_issuer' => 'Shlink',
],
'dependencies' => [
'delegators' => [
LcobucciJwtProvider::class => [
LazyServiceFactory::class,
],
Publisher::class => [
LazyServiceFactory::class,
],
],
'lazy_services' => [
'class_map' => [
LcobucciJwtProvider::class => LcobucciJwtProvider::class,
Publisher::class => PublisherInterface::class,
],
],
],
];

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
return [
'mercure' => [
'public_hub_url' => 'http://localhost:8001',
'internal_hub_url' => 'http://shlink_mercure_proxy',
'jwt_secret' => 'mercure_jwt_key',
],
];

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
return [
'not_found_redirects' => [
'invalid_short_url' => null, // Formerly url_shortener.not_found_short_url.redirect_to
'invalid_short_url' => null,
'regular_404' => null,
'base_url' => null,
],

View File

@@ -5,7 +5,8 @@ declare(strict_types=1);
return [
'mezzio-swoole' => [
'enable_coroutine' => true,
// Setting this to true can have unexpected behaviors when running several concurrent slow DB queries
'enable_coroutine' => false,
'swoole-http-server' => [
'host' => '0.0.0.0',

View File

@@ -2,9 +2,6 @@
declare(strict_types=1);
use Laminas\ServiceManager\Factory\InvokableFactory;
use Mezzio\Swoole\HotCodeReload\FileWatcher\InotifyFileWatcher;
return [
'mezzio-swoole' => [
@@ -13,10 +10,4 @@ return [
],
],
'dependencies' => [
'factories' => [
InotifyFileWatcher::class => InvokableFactory::class,
],
],
];

View File

@@ -1,17 +0,0 @@
<?php
declare(strict_types=1);
return [
'templates' => [
'extension' => 'phtml',
],
'plates' => [
'extensions' => [
// extension service names or instances
],
],
];

View File

@@ -2,6 +2,8 @@
declare(strict_types=1);
use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_CACHE_LIFETIME;
use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE;
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
return [
@@ -12,8 +14,11 @@ return [
'hostname' => '',
],
'validate_url' => false,
'anonymize_remote_addr' => true,
'visits_webhooks' => [],
'default_short_codes_length' => DEFAULT_SHORT_CODES_LENGTH,
'redirect_status_code' => DEFAULT_REDIRECT_STATUS_CODE,
'redirect_cache_lifetime' => DEFAULT_REDIRECT_CACHE_LIFETIME,
],
];

View File

@@ -4,11 +4,10 @@ declare(strict_types=1);
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Laminas\ServiceManager\ServiceManager;
use Psr\Container\ContainerInterface;
return (function () {
/** @var ContainerInterface|ServiceManager $container */
/** @var ContainerInterface $container */
$container = include __DIR__ . '/container.php';
$em = $container->get(EntityManager::class);

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink;
use Laminas\ConfigAggregator;
use Laminas\ZendFrameworkBridge;
use Laminas\Diactoros;
use Mezzio;
use Mezzio\ProblemDetails;
@@ -15,11 +15,12 @@ return (new ConfigAggregator\ConfigAggregator([
Mezzio\ConfigProvider::class,
Mezzio\Router\ConfigProvider::class,
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
Mezzio\Plates\ConfigProvider::class,
Mezzio\Swoole\ConfigProvider::class,
ProblemDetails\ConfigProvider::class,
Diactoros\ConfigProvider::class,
Common\ConfigProvider::class,
Config\ConfigProvider::class,
Importer\ConfigProvider::class,
IpGeolocation\ConfigProvider::class,
EventDispatcher\ConfigProvider::class,
Core\ConfigProvider::class,
@@ -30,7 +31,6 @@ return (new ConfigAggregator\ConfigAggregator([
? new ConfigAggregator\PhpFileProvider('config/test/*.global.php')
: new ConfigAggregator\LaminasConfigProvider('config/params/{generated_config.php,*.config.{php,json}}'),
], 'data/cache/app_config.php', [
ZendFrameworkBridge\ConfigPostProcessor::class,
Core\Config\SimplifiedConfigParser::class,
Core\Config\BasePathPrefixer::class,
Core\Config\DeprecatedConfigParser::class,

View File

@@ -7,12 +7,28 @@ namespace Shlinkio\Shlink\TestUtils;
use Doctrine\ORM\EntityManager;
use Psr\Container\ContainerInterface;
use function register_shutdown_function;
use function sprintf;
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT;
/** @var ContainerInterface $container */
$container = require __DIR__ . '/../container.php';
$testHelper = $container->get(Helper\TestHelper::class);
$config = $container->get('config');
$em = $container->get(EntityManager::class);
$httpClient = $container->get('shlink_test_api_client');
// Start code coverage collecting on swoole process, and stop it when process shuts down
$httpClient->request('GET', sprintf('http://%s:%s/api-tests/start-coverage', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT));
register_shutdown_function(function () use ($httpClient): void {
$httpClient->request(
'GET',
sprintf('http://%s:%s/api-tests/stop-coverage', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT),
);
});
$testHelper->createTestDb();
ApiTest\ApiTestCase::setApiClient($container->get('shlink_test_api_client'));
ApiTest\ApiTestCase::setApiClient($httpClient);
ApiTest\ApiTestCase::setSeedFixturesCallback(fn () => $testHelper->seedFixtures($em, $config['data_fixtures'] ?? []));

View File

@@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink;
const SWOOLE_TESTING_HOST = '127.0.0.1';
const SWOOLE_TESTING_PORT = 9999;

View File

@@ -6,20 +6,39 @@ namespace Shlinkio\Shlink;
use GuzzleHttp\Client;
use Laminas\ConfigAggregator\ConfigAggregator;
use Laminas\Diactoros\Response\EmptyResponse;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Laminas\Stdlib\Glob;
use PDO;
use PHPUnit\Runner\Version;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use SebastianBergmann\CodeCoverage\Driver\Selector;
use SebastianBergmann\CodeCoverage\Filter;
use SebastianBergmann\CodeCoverage\Report\PHP;
use SebastianBergmann\CodeCoverage\Report\Xml\Facade as Xml;
use function Laminas\Stratigility\middleware;
use function Shlinkio\Shlink\Common\env;
use function sprintf;
use function sys_get_temp_dir;
$swooleTestingHost = '127.0.0.1';
$swooleTestingPort = 9999;
use const ShlinkioTest\Shlink\SWOOLE_TESTING_HOST;
use const ShlinkioTest\Shlink\SWOOLE_TESTING_PORT;
$isApiTest = env('TEST_ENV') === 'api';
if ($isApiTest) {
$filter = new Filter();
foreach (Glob::glob(__DIR__ . '/../../module/*/src') as $item) {
$filter->includeDirectory($item);
}
$coverage = new CodeCoverage((new Selector())->forLineCoverage($filter), $filter);
}
$buildDbConnection = function (): array {
$driver = env('DB_DRIVER', 'sqlite');
$isCi = env('TRAVIS', false);
$isCi = env('CI', false);
$getMysqlHost = fn (string $driver) => sprintf('shlink_db%s', $driver === 'mysql' ? '' : '_maria');
$getCiMysqlPort = fn (string $driver) => $driver === 'mysql' ? '3307' : '3308';
$driverConfigMap = [
'sqlite' => [
@@ -29,19 +48,22 @@ $buildDbConnection = function (): array {
'mysql' => [
'driver' => 'pdo_mysql',
'host' => $isCi ? '127.0.0.1' : $getMysqlHost($driver),
'port' => $isCi ? $getCiMysqlPort($driver) : '3306',
'user' => 'root',
'password' => $isCi ? '' : 'root',
'password' => 'root',
'dbname' => 'shlink_test',
'charset' => 'utf8',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
],
],
'postgres' => [
'driver' => 'pdo_pgsql',
'host' => $isCi ? '127.0.0.1' : 'shlink_db_postgres',
'port' => $isCi ? '5433' : '5432',
'user' => 'postgres',
'password' => $isCi ? '' : 'root',
'password' => 'root',
'dbname' => 'shlink_test',
'charset' => 'utf8',
],
@@ -49,7 +71,7 @@ $buildDbConnection = function (): array {
'driver' => 'pdo_sqlsrv',
'host' => $isCi ? '127.0.0.1' : 'shlink_db_ms',
'user' => 'sa',
'password' => $isCi ? '' : 'Passw0rd!',
'password' => 'Passw0rd!',
'dbname' => 'shlink_test',
],
];
@@ -74,22 +96,55 @@ return [
'mezzio-swoole' => [
'enable_coroutine' => false,
'swoole-http-server' => [
'host' => $swooleTestingHost,
'port' => $swooleTestingPort,
'host' => SWOOLE_TESTING_HOST,
'port' => SWOOLE_TESTING_PORT,
'process-name' => 'shlink_test',
'options' => [
'pid_file' => sys_get_temp_dir() . '/shlink-test-swoole.pid',
'worker_num' => 1,
'task_worker_num' => 1,
'enable_coroutine' => false,
],
],
],
'routes' => !$isApiTest ? [] : [
[
'name' => 'start_collecting_coverage',
'path' => '/api-tests/start-coverage',
'middleware' => middleware(static function () use (&$coverage) {
if ($coverage) {
$coverage->start('API tests');
}
return new EmptyResponse();
}),
'allowed_methods' => ['GET'],
],
[
'name' => 'dump_coverage',
'path' => '/api-tests/stop-coverage',
'middleware' => middleware(static function () use (&$coverage) {
if ($coverage) {
$basePath = __DIR__ . '/../../build/coverage-api';
$coverage->stop();
(new PHP())->process($coverage, $basePath . '.cov');
(new Xml(Version::getVersionString()))->process($coverage, $basePath . '/coverage-xml');
}
return new EmptyResponse();
}),
'allowed_methods' => ['GET'],
],
],
'mercure' => [
'public_hub_url' => null,
'internal_hub_url' => null,
'jwt_secret' => null,
],
'dependencies' => [
'services' => [
'shlink_test_api_client' => new Client([
'base_uri' => sprintf('http://%s:%s/', $swooleTestingHost, $swooleTestingPort),
'base_uri' => sprintf('http://%s:%s/', SWOOLE_TESTING_HOST, SWOOLE_TESTING_PORT),
'http_errors' => false,
]),
],

View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -ex
curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list > /etc/apt/sources.list.d/mssql-release.list
apt-get update
ACCEPT_EULA=Y apt-get install msodbcsql17
apt-get install unixodbc-dev

View File

@@ -0,0 +1,17 @@
server {
listen 80 default_server;
error_log /home/shlink/www/data/infra/nginx/mercure_proxy.error.log;
location / {
proxy_pass http://shlink_mercure;
proxy_read_timeout 24h;
proxy_http_version 1.1;
proxy_set_header Connection "";
## Be sure to set USE_FORWARDED_HEADERS=1 to allow the hub to use those headers ##
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

@@ -1,9 +1,8 @@
FROM php:7.4.2-fpm-alpine3.11
FROM php:7.4.11-fpm-alpine3.12
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.18
ENV APCU_BC_VERSION 1.0.5
ENV XDEBUG_VERSION 2.9.0
RUN apk update
@@ -31,6 +30,9 @@ RUN docker-php-ext-install gd
RUN apk add --no-cache postgresql-dev
RUN docker-php-ext-install pdo_pgsql
RUN apk add --no-cache gmp-dev
RUN docker-php-ext-install gmp
# Install APCu extension
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu\
@@ -55,32 +57,17 @@ RUN rm /tmp/apcu_bc.tar.gz
RUN rm /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini
RUN echo extension=apcu.so > /usr/local/etc/php/conf.d/20-php-ext-apcu.ini
# Install xdebug
ADD https://pecl.php.net/get/xdebug-$XDEBUG_VERSION /tmp/xdebug.tar.gz
RUN mkdir -p /usr/src/php/ext/xdebug\
&& tar xf /tmp/xdebug.tar.gz -C /usr/src/php/ext/xdebug --strip-components=1
# configure and install
RUN docker-php-ext-configure xdebug\
&& docker-php-ext-install xdebug
# cleanup
RUN rm /tmp/xdebug.tar.gz
# Install sqlsrv driver
# Install pcov and sqlsrv driver
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/mssql-tools_17.5.1.1-1_amd64.apk && \
apk add --allow-untrusted msodbcsql17_17.5.1.1-1_amd64.apk && \
apk add --allow-untrusted mssql-tools_17.5.1.1-1_amd64.apk && \
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
pecl install pdo_sqlsrv && \
docker-php-ext-enable pdo_sqlsrv && \
pecl install pdo_sqlsrv pcov && \
docker-php-ext-enable pdo_sqlsrv pcov && \
apk del .phpize-deps && \
rm msodbcsql17_17.5.1.1-1_amd64.apk && \
rm mssql-tools_17.5.1.1-1_amd64.apk
rm msodbcsql17_17.5.1.1-1_amd64.apk
# Install composer
RUN php -r "readfile('https://getcomposer.org/installer');" | php
RUN chmod +x composer.phar
RUN mv composer.phar /usr/local/bin/composer
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
# Make home directory writable by anyone
RUN chmod 777 /home

View File

@@ -4,3 +4,5 @@ memory_limit=-1
log_errors_max_len=0
zend.assertions=1
assert.exception=1
pcov.enabled=1
pcov.directory=module

View File

@@ -1,10 +1,10 @@
FROM php:7.4.2-alpine3.11
FROM php:7.4.11-alpine3.12
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
ENV APCU_VERSION 5.1.18
ENV APCU_BC_VERSION 1.0.5
ENV INOTIFY_VERSION 2.0.0
ENV SWOOLE_VERSION 4.4.15
ENV SWOOLE_VERSION 4.5.9
RUN apk update
@@ -32,6 +32,9 @@ RUN docker-php-ext-install gd
RUN apk add --no-cache postgresql-dev
RUN docker-php-ext-install pdo_pgsql
RUN apk add --no-cache gmp-dev
RUN docker-php-ext-install gmp
# Install APCu extension
ADD https://pecl.php.net/get/apcu-$APCU_VERSION.tgz /tmp/apcu.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu\
@@ -66,22 +69,17 @@ RUN docker-php-ext-configure inotify\
# cleanup
RUN rm /tmp/inotify.tar.gz
# Install swoole and mssql driver
# Install swoole, pcov and mssql driver
RUN wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/msodbcsql17_17.5.1.1-1_amd64.apk && \
wget https://download.microsoft.com/download/e/4/e/e4e67866-dffd-428c-aac7-8d28ddafb39b/mssql-tools_17.5.1.1-1_amd64.apk && \
apk add --allow-untrusted msodbcsql17_17.5.1.1-1_amd64.apk && \
apk add --allow-untrusted mssql-tools_17.5.1.1-1_amd64.apk && \
apk add --no-cache --virtual .phpize-deps $PHPIZE_DEPS unixodbc-dev && \
pecl install swoole-${SWOOLE_VERSION} pdo_sqlsrv && \
docker-php-ext-enable swoole pdo_sqlsrv && \
pecl install swoole-${SWOOLE_VERSION} pdo_sqlsrv pcov && \
docker-php-ext-enable swoole pdo_sqlsrv pcov && \
apk del .phpize-deps && \
rm msodbcsql17_17.5.1.1-1_amd64.apk && \
rm mssql-tools_17.5.1.1-1_amd64.apk
rm msodbcsql17_17.5.1.1-1_amd64.apk
# Install composer
RUN php -r "readfile('https://getcomposer.org/installer');" | php
RUN chmod +x composer.phar
RUN mv composer.phar /usr/local/bin/composer
COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer
# Make home directory writable by anyone
RUN chmod 777 /home

View File

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

View File

@@ -24,10 +24,10 @@ class Version20171021093246 extends AbstractMigration
return;
}
$shortUrls->addColumn('valid_since', Types::DATETIME, [
$shortUrls->addColumn('valid_since', Types::DATETIME_MUTABLE, [
'notnull' => false,
]);
$shortUrls->addColumn('valid_until', Types::DATETIME, [
$shortUrls->addColumn('valid_until', Types::DATETIME_MUTABLE, [
'notnull' => false,
]);
}

View File

@@ -58,7 +58,7 @@ final class Version20180913205455 extends AbstractMigration
}
try {
return (string) IpAddress::fromString($addr)->getObfuscatedCopy();
return (string) IpAddress::fromString($addr)->getAnonymizedCopy();
} catch (InvalidArgumentException $e) {
return null;
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20200503170404 extends AbstractMigration
{
private const INDEX_NAME = 'IDX_visits_date';
public function up(Schema $schema): void
{
$visits = $schema->getTable('visits');
$this->skipIf($visits->hasIndex(self::INDEX_NAME));
$visits->addIndex(['date'], self::INDEX_NAME);
}
public function down(Schema $schema): void
{
$visits = $schema->getTable('visits');
$this->skipIf(! $visits->hasIndex(self::INDEX_NAME));
$visits->dropIndex(self::INDEX_NAME);
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
final class Version20201023090929 extends AbstractMigration
{
private const IMPORT_SOURCE_COLUMN = 'import_source';
public function up(Schema $schema): void
{
$shortUrls = $schema->getTable('short_urls');
$this->skipIf($shortUrls->hasColumn(self::IMPORT_SOURCE_COLUMN));
$shortUrls->addColumn(self::IMPORT_SOURCE_COLUMN, Types::STRING, [
'length' => 255,
'notnull' => false,
]);
$shortUrls->addColumn('import_original_short_code', Types::STRING, [
'length' => 255,
'notnull' => false,
]);
$shortUrls->addUniqueIndex(
[self::IMPORT_SOURCE_COLUMN, 'import_original_short_code', 'domain_id'],
'unique_imports',
);
}
public function down(Schema $schema): void
{
$shortUrls = $schema->getTable('short_urls');
$this->skipIf(! $shortUrls->hasColumn(self::IMPORT_SOURCE_COLUMN));
$shortUrls->dropColumn(self::IMPORT_SOURCE_COLUMN);
$shortUrls->dropColumn('import_original_short_code');
$shortUrls->dropIndex('unique_imports');
}
}

View File

@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Cake\Chronos\Chronos;
use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
final class Version20201102113208 extends AbstractMigration
{
private const API_KEY_COLUMN = 'author_api_key_id';
public function up(Schema $schema): void
{
$shortUrls = $schema->getTable('short_urls');
$this->skipIf($shortUrls->hasColumn(self::API_KEY_COLUMN));
$shortUrls->addColumn(self::API_KEY_COLUMN, Types::BIGINT, [
'unsigned' => true,
'notnull' => false,
]);
$shortUrls->addForeignKeyConstraint('api_keys', [self::API_KEY_COLUMN], ['id'], [
'onDelete' => 'SET NULL',
'onUpdate' => 'RESTRICT',
], 'FK_' . self::API_KEY_COLUMN);
}
public function postUp(Schema $schema): void
{
// If there's only one API key and it's active, link all existing URLs with it
$qb = $this->connection->createQueryBuilder();
$qb->select('id')
->from('api_keys')
->where($qb->expr()->eq('enabled', ':enabled'))
->andWhere($qb->expr()->or(
$qb->expr()->isNull('expiration_date'),
$qb->expr()->gt('expiration_date', ':expiration'),
))
->setParameters([
'enabled' => true,
'expiration' => Chronos::now()->toDateTimeString(),
]);
/** @var Result $result */
$result = $qb->execute();
$id = $this->resolveOneApiKeyId($result);
if ($id === null) {
return;
}
$qb = $this->connection->createQueryBuilder();
$qb->update('short_urls')
->set(self::API_KEY_COLUMN, ':apiKeyId')
->setParameter('apiKeyId', $id)
->execute();
}
/**
* @return string|int|null
*/
private function resolveOneApiKeyId(Result $result)
{
$results = [];
while ($row = $result->fetchAssociative()) {
// As soon as we have to iterate more than once, then we cannot resolve a single API key
if (! empty($results)) {
return null;
}
$results[] = $row['id'] ?? null;
}
return $results[0] ?? null;
}
public function down(Schema $schema): void
{
$shortUrls = $schema->getTable('short_urls');
$this->skipIf(! $shortUrls->hasColumn(self::API_KEY_COLUMN));
$shortUrls->removeForeignKey('FK_' . self::API_KEY_COLUMN);
$shortUrls->dropColumn(self::API_KEY_COLUMN);
}
}

View File

@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Types;
use Doctrine\Migrations\AbstractMigration;
final class Version20210102174433 extends AbstractMigration
{
private const TABLE_NAME = 'api_key_roles';
public function up(Schema $schema): void
{
$this->skipIf($schema->hasTable(self::TABLE_NAME));
$table = $schema->createTable(self::TABLE_NAME);
$table->addColumn('id', Types::BIGINT, [
'unsigned' => true,
'autoincrement' => true,
'notnull' => true,
]);
$table->setPrimaryKey(['id']);
$table->addColumn('role_name', Types::STRING, [
'length' => 256,
'notnull' => true,
]);
$table->addColumn('meta', Types::JSON, [
'notnull' => true,
]);
$table->addColumn('api_key_id', Types::BIGINT, [
'unsigned' => true,
'notnull' => true,
]);
$table->addForeignKeyConstraint('api_keys', ['api_key_id'], ['id'], [
'onDelete' => 'CASCADE',
'onUpdate' => 'RESTRICT',
]);
$table->addUniqueIndex(['role_name', 'api_key_id'], 'UQ_role_plus_api_key');
}
public function down(Schema $schema): void
{
$this->skipIf(! $schema->hasTable(self::TABLE_NAME));
$schema->getTable(self::TABLE_NAME)->dropIndex('UQ_role_plus_api_key');
$schema->dropTable(self::TABLE_NAME);
}
}

View File

@@ -7,7 +7,7 @@ namespace <namespace>;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version<version> extends AbstractMigration
final class <className> extends AbstractMigration
{
public function up(Schema $schema): void
{

14
docker-compose.ci.yml Normal file
View File

@@ -0,0 +1,14 @@
version: '3'
services:
shlink_db:
environment:
MYSQL_DATABASE: shlink_test
shlink_db_postgres:
environment:
POSTGRES_DB: shlink_test
shlink_db_maria:
environment:
MYSQL_DATABASE: shlink_test

View File

@@ -3,7 +3,7 @@ version: '3'
services:
shlink_nginx:
container_name: shlink_nginx
image: nginx:1.17.6-alpine
image: nginx:1.17.10-alpine
ports:
- "8000:80"
volumes:
@@ -27,9 +27,22 @@ services:
- shlink_db_maria
- shlink_db_ms
- shlink_redis
- shlink_mercure
- shlink_mercure_proxy
environment:
LC_ALL: C
shlink_swoole_proxy:
container_name: shlink_swoole_proxy
image: nginx:1.17.10-alpine
ports:
- "8002:80"
volumes:
- ./:/home/shlink/www
- ./data/infra/swoole_proxy_vhost.conf:/etc/nginx/conf.d/default.conf
links:
- shlink_swoole
shlink_swoole:
container_name: shlink_swoole
build:
@@ -47,6 +60,8 @@ services:
- shlink_db_maria
- shlink_db_ms
- shlink_redis
- shlink_mercure
- shlink_mercure_proxy
environment:
LC_ALL: C
@@ -64,7 +79,7 @@ services:
shlink_db_postgres:
container_name: shlink_db_postgres
image: postgres:10.7-alpine
image: postgres:12.2-alpine
ports:
- "5433:5432"
volumes:
@@ -77,7 +92,7 @@ services:
shlink_db_maria:
container_name: shlink_db_maria
image: mariadb:10.2
image: mariadb:10.5
ports:
- "3308:3306"
volumes:
@@ -99,6 +114,27 @@ services:
shlink_redis:
container_name: shlink_redis
image: redis:5.0-alpine
image: redis:6.0-alpine
ports:
- "6380:6379"
shlink_mercure_proxy:
container_name: shlink_mercure_proxy
image: nginx:1.17.10-alpine
ports:
- "8001:80"
volumes:
- ./:/home/shlink/www
- ./data/infra/mercure_proxy_vhost.conf:/etc/nginx/conf.d/default.conf
links:
- shlink_mercure
shlink_mercure:
container_name: shlink_mercure
image: dunglas/mercure:v0.10
ports:
- "3080:80"
environment:
CORS_ALLOWED_ORIGINS: "*"
JWT_KEY: "mercure_jwt_key"
USE_FORWARDED_HEADERS: "1"

View File

@@ -53,7 +53,7 @@ docker exec -it shlink_container shlink
## Use an external DB
The image comes with a working sqlite database, but in production you will probably want to usa a distributed database.
The image comes with a working sqlite database, but in production, it's strongly recommended using a distributed database.
It is possible to use a set of env vars to make this shlink instance interact with an external MySQL, MariaDB, PostgreSQL or Microsoft SQL Server database.
@@ -73,18 +73,73 @@ It is possible to use a set of env vars to make this shlink instance interact wi
Taking this into account, you could run shlink on a local docker service like this:
```bash
docker run --name shlink -p 8080:8080 -e SHORT_DOMAIN_HOST=doma.in -e SHORT_DOMAIN_SCHEMA=https -e DB_DRIVER=mysql -e DB_USER=root -e DB_PASSWORD=123abc -e DB_HOST=something.rds.amazonaws.com shlinkio/shlink:stable
docker run \
--name shlink \
-p 8080:8080 \
-e SHORT_DOMAIN_HOST=doma.in \
-e SHORT_DOMAIN_SCHEMA=https \
-e DB_DRIVER=mysql \
-e DB_USER=root \
-e DB_PASSWORD=123abc \
-e DB_HOST=something.rds.amazonaws.com \
shlinkio/shlink:stable
```
You could even link to a local database running on a different container:
```bash
docker run --name shlink -p 8080:8080 [...] -e DB_HOST=some_mysql_container --link some_mysql_container shlinkio/shlink:stable
docker run \
--name shlink \
-p 8080:8080 \
[...] \
-e DB_HOST=some_mysql_container \
--link some_mysql_container \
shlinkio/shlink:stable
```
> If you have considered using SQLite but sharing the database file with a volume, read [this issue](https://github.com/shlinkio/shlink-docker-image/issues/40) first.
## Supported env vars
## Other integrations
### Use an external redis server
If you plan to run more than one Shlink instance, there are some resources that should be shared ([Multi instance considerations](#multi-instance-considerations)).
One of those resources are the locks Shlink generates to prevent some operations to be run more than once in parallel (in the future, these redis servers could be used for other caching operations).
In order to share those locks, you should use an external redis server (or a cluster of redis servers), by providing the `REDIS_SERVERS` env var.
It can be either one server name or a comma-separated list of servers.
> If more than one redis server is provided, Shlink will expect them to be configured as a [redis cluster](https://redis.io/topics/cluster-tutorial).
### Integrate with a mercure hub server
One way to get real time updates when certain events happen in Shlink is by integrating it with a [mercure hub](https://mercure.rocks/) server.
If you do that, Shlink will publish updates and other clients can subscribe to those.
There are three env vars you need to provide if you want to enable this:
* `MERCURE_PUBLIC_HUB_URL`: **[Mandatory]**. The public URL of a mercure hub server to which Shlink will sent updates. This URL will also be served to consumers that want to subscribe to those updates.
* `MERCURE_INTERNAL_HUB_URL`: **[Optional]**. An internal URL for a mercure hub. Will be used only when publishing updates to mercure, and does not need to be public. If this is not provided, the `MERCURE_PUBLIC_HUB_URL` one will be used to publish updates.
* `MERCURE_JWT_SECRET`: **[Mandatory]**. The secret key that was provided to the mercure hub server, in order to be able to generate valid JWTs for publishing/subscribing to that server.
So in order to run shlink with mercure integration, you would do it like this:
```bash
docker run \
--name shlink \
-p 8080:8080 \
-e SHORT_DOMAIN_HOST=doma.in \
-e SHORT_DOMAIN_SCHEMA=https \
-e "MERCURE_PUBLIC_HUB_URL=https://example.com"
-e "MERCURE_INTERNAL_HUB_URL=http://my-mercure-hub.prod.svc.cluster.local"
-e MERCURE_JWT_SECRET=super_secret_key
shlinkio/shlink:stable
```
## All supported env vars
A few env vars have been already used in previous examples, but this image supports others that can be used to customize its behavior.
@@ -102,6 +157,7 @@ This is the complete list of supported env vars:
* **mysql** or **maria** -> `3306`
* **postgres** -> `5432`
* **mssql** -> `1433`
* `DB_UNIX_SOCKET`: Alternatively to the `DB_HOST`, you can provide this to connect through unix sockets when using `mysql`, `maria` or `postgres` drivers.
* `DISABLE_TRACK_PARAM`: The name of a query param that can be used to visit short URLs avoiding the visit to be tracked. This feature won't be available if not value is provided.
* `DELETE_SHORT_URL_THRESHOLD`: The amount of visits on short URLs which will not allow them to be deleted. Defaults to `15`.
* `VALIDATE_URLS`: Boolean which tells if shlink should validate a status 20x is returned (after following redirects) when trying to shorten a URL. Defaults to `false`.
@@ -113,24 +169,25 @@ This is the complete list of supported env vars:
* `TASK_WORKER_NUM`: The amount of concurrent background tasks this shlink instance will be able to execute. Defaults to 16.
* `VISITS_WEBHOOKS`: A comma-separated list of URLs that will receive a `POST` request when a short URL receives a visit.
* `DEFAULT_SHORT_CODES_LENGTH`: The length you want generated short codes to have. It defaults to 5 and has to be at least 4, so any value smaller than that will fall back to 4.
* `REDIS_SERVERS`: A comma-separated list of redis servers where Shlink locks are stored (locks are used to prevent some operations to be run more than once in parallel).
This is important when running more than one Shlink instance ([Multi instance considerations](#multi-instance-considerations)). If not provided, Shlink stores locks on every instance separately.
If more than one server is provided, Shlink will expect them to be configured as a [redis cluster](https://redis.io/topics/cluster-tutorial).
In the future, these redis servers could be used for other caching operations performed by shlink.
* `GEOLITE_LICENSE_KEY`: The license key used to download new GeoLite2 database files. This is not mandatory, as a default license key is provided, but it is **strongly recommended** that you provide your own. Go to [https://shlink.io/documentation/geolite-license-key](https://shlink.io/documentation/geolite-license-key) to know how to generate it.
* `REDIS_SERVERS`: A comma-separated list of redis servers where Shlink locks are stored (locks are used to prevent some operations to be run more than once in parallel).
* `MERCURE_PUBLIC_HUB_URL`: The public URL of a mercure hub server to which Shlink will sent updates. This URL will also be served to consumers that want to subscribe to those updates.
* `MERCURE_INTERNAL_HUB_URL`: An internal URL for a mercure hub. Will be used only when publishing updates to mercure, and does not need to be public. If this is not provided but `MERCURE_PUBLIC_HUB_URL` was, the former one will be used to publish updates.
* `MERCURE_JWT_SECRET`: The secret key that was provided to the mercure hub server, in order to be able to generate valid JWTs for publishing/subscribing to that server.
* `ANONYMIZE_REMOTE_ADDR`: Tells if IP addresses from visitors should be obfuscated before storing them in the database. Default value is `true`. **Careful!** Setting this to `false` will make your Shlink instance no longer be in compliance with the GDPR and other similar data protection regulations.
* `REDIRECT_STATUS_CODE`: Either **301** or **302**. Used to determine if redirects from short to long URLs should be done with a 301 or 302 status. Defaults to 302.
* `REDIRECT_CACHE_LIFETIME`: Allows to set the amount of seconds that redirects should be cached when redirect status is 301. Default values is 30.
* `PORT`: Can be used to set the port in which shlink listens. Defaults to 8080 (Some cloud providers, like Google cloud or Heroku, expect to be able to customize exposed port by providing this env var).
An example using all env vars could look like this:
```bash
docker run \
--name shlink \
-p 8080:8080 \
-p 8080:8888 \
-e SHORT_DOMAIN_HOST=doma.in \
-e SHORT_DOMAIN_SCHEMA=https \
-e PORT=8888 \
-e DB_DRIVER=mysql \
-e DB_NAME=shlink \
-e DB_USER=root \
@@ -150,10 +207,20 @@ docker run \
-e "VISITS_WEBHOOKS=http://my-api.com/api/v2.3/notify,https://third-party.io/foo" \
-e DEFAULT_SHORT_CODES_LENGTH=6 \
-e GEOLITE_LICENSE_KEY=kjh23ljkbndskj345 \
-e "MERCURE_PUBLIC_HUB_URL=https://example.com" \
-e "MERCURE_INTERNAL_HUB_URL=http://my-mercure-hub.prod.svc.cluster.local" \
-e MERCURE_JWT_SECRET=super_secret_key \
-e ANONYMIZE_REMOTE_ADDR=false \
-e REDIRECT_STATUS_CODE=301 \
-e REDIRECT_CACHE_LIFETIME=90 \
shlinkio/shlink:stable
```
## Provide config via volumes
## [DEPRECATED] Provide config via volumes
> As of v2.5.0, providing config through volumes is deprecated, and no new options will be added anymore. Use env vars instead.
>
> Support for config options through volumes will be removed in Shlink v3.0.0
Rather than providing custom configuration via env vars, it is also possible ot provide config files in json format.
@@ -191,7 +258,14 @@ The whole configuration should have this format, but it can be split into multip
"host": "something.rds.amazonaws.com",
"port": "3306"
},
"geolite_license_key": "kjh23ljkbndskj345"
"geolite_license_key": "kjh23ljkbndskj345",
"mercure_public_hub_url": "https://example.com",
"mercure_internal_hub_url": "http://my-mercure-hub.prod.svc.cluster.local",
"mercure_jwt_secret": "super_secret_key",
"anonymize_remote_addr": false,
"redirect_status_code": 301,
"redirect_cache_lifetime": 90,
"port": 8888
}
```
@@ -203,7 +277,13 @@ Once created just run shlink with the volume:
docker run --name shlink -p 8080:8080 -v ${PWD}/my/config/dir:/etc/shlink/config/params shlinkio/shlink:stable
```
## Multi instance considerations
## Multi-architecture
Starting on v2.3.0, Shlink's docker image is built for multiple architectures.
The only limitation is that images for architectures other than `amd64` will not have support for Microsoft SQL databases, since there are no official binaries.
## Multi-instance considerations
These are some considerations to take into account when running multiple instances of shlink.
@@ -219,6 +299,6 @@ Versioning on this docker image works as follows:
* `X.X.X`: when providing a specific version number, the image version will match the shlink version it contains. For example, installing `shlinkio/shlink:1.15.0`, you will get an image containing shlink v1.15.0.
* `stable`: always holds the latest stable tag. For example, if latest shlink version is 2.0.0, installing `shlinkio/shlink:stable`, you will get an image containing shlink v2.0.0
* `latest`: always holds the latest contents in master, and it's considered unstable and not suitable for production.
* `latest`: always holds the latest contents, and it's considered unstable and not suitable for production.
> **Important**: The docker image was introduced with shlink v1.15.0, so there are no official images previous to that versions.

View File

@@ -1,15 +1,25 @@
#!/bin/bash
set -e
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
set -ex
# If there is a tag, regardless the branch, build that docker tag and also "stable"
if [[ ! -z $TRAVIS_TAG ]]; then
docker build --build-arg SHLINK_VERSION=${TRAVIS_TAG#?} -t shlinkio/shlink:${TRAVIS_TAG#?} -t shlinkio/shlink:stable .
docker push shlinkio/shlink:${TRAVIS_TAG#?}
docker push shlinkio/shlink:stable
# If build branch is develop, build latest (on master, when there's no tag, do not build anything)
elif [[ "$TRAVIS_BRANCH" == 'develop' ]]; then
docker build -t shlinkio/shlink:latest .
docker push shlinkio/shlink:latest
PLATFORMS="linux/arm/v7,linux/arm64/v8,linux/amd64"
DOCKER_IMAGE="shlinkio/shlink"
# If ref is not develop, then this is a tag. Build that docker tag and also "stable"
if [[ "$GITHUB_REF" != *"develop"* ]]; then
VERSION=${GITHUB_REF#refs/tags/v}
TAGS="-t ${DOCKER_IMAGE}:${VERSION}"
# Push stable tag only if this is not an alpha or beta tag
[[ $GITHUB_REF != *"alpha"* && $GITHUB_REF != *"beta"* ]] && TAGS="${TAGS} -t ${DOCKER_IMAGE}:stable"
docker buildx build --push \
--build-arg SHLINK_VERSION=${VERSION} \
--platform ${PLATFORMS} \
${TAGS} .
# If build branch is develop, build latest
elif [[ "$GITHUB_REF" == *"develop"* ]]; then
docker buildx build --push \
--platform ${PLATFORMS} \
-t ${DOCKER_IMAGE}:latest .
fi

View File

@@ -11,6 +11,9 @@ use function explode;
use function Functional\contains;
use function Shlinkio\Shlink\Common\env;
use const Shlinkio\Shlink\Core\DEFAULT_DELETE_SHORT_URL_THRESHOLD;
use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_CACHE_LIFETIME;
use const Shlinkio\Shlink\Core\DEFAULT_REDIRECT_STATUS_CODE;
use const Shlinkio\Shlink\Core\DEFAULT_SHORT_CODES_LENGTH;
use const Shlinkio\Shlink\Core\MIN_SHORT_CODES_LENGTH;
@@ -31,6 +34,7 @@ $helper = new class {
public function getDbConfig(): array
{
$driver = env('DB_DRIVER');
$isMysql = contains(['maria', 'mysql'], $driver);
if ($driver === null || $driver === 'sqlite') {
return [
'driver' => 'pdo_sqlite',
@@ -38,18 +42,21 @@ $helper = new class {
];
}
$driverOptions = ! contains(['maria', 'mysql'], $driver) ? [] : [
$driverOptions = ! $isMysql ? [] : [
// 1002 -> PDO::MYSQL_ATTR_INIT_COMMAND
1002 => 'SET NAMES utf8',
// 1000 -> PDO::MYSQL_ATTR_USE_BUFFERED_QUERY
1000 => true,
];
return [
'driver' => self::DB_DRIVERS_MAP[$driver],
'dbname' => env('DB_NAME', 'shlink'),
'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'host' => env('DB_HOST'),
'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null),
'port' => env('DB_PORT', self::DB_PORTS_MAP[$driver]),
'driverOptions' => $driverOptions,
'unix_socket' => $isMysql ? env('DB_UNIX_SOCKET') : null,
];
}
@@ -79,19 +86,28 @@ $helper = new class {
$value = (int) env('DEFAULT_SHORT_CODES_LENGTH', DEFAULT_SHORT_CODES_LENGTH);
return $value < MIN_SHORT_CODES_LENGTH ? MIN_SHORT_CODES_LENGTH : $value;
}
public function getMercureConfig(): array
{
$publicUrl = env('MERCURE_PUBLIC_HUB_URL');
return [
'public_hub_url' => $publicUrl,
'internal_hub_url' => env('MERCURE_INTERNAL_HUB_URL', $publicUrl),
'jwt_secret' => env('MERCURE_JWT_SECRET'),
];
}
};
return [
'config_cache_enabled' => false,
'app_options' => [
'disable_track_param' => env('DISABLE_TRACK_PARAM'),
],
'delete_short_urls' => [
'check_visits_threshold' => true,
'visits_threshold' => (int) env('DELETE_SHORT_URL_THRESHOLD', 15),
'visits_threshold' => (int) env('DELETE_SHORT_URL_THRESHOLD', DEFAULT_DELETE_SHORT_URL_THRESHOLD),
],
'entity_manager' => [
@@ -104,8 +120,11 @@ return [
'hostname' => env('SHORT_DOMAIN_HOST', ''),
],
'validate_url' => (bool) env('VALIDATE_URLS', false),
'anonymize_remote_addr' => (bool) env('ANONYMIZE_REMOTE_ADDR', true),
'visits_webhooks' => $helper->getVisitsWebhooks(),
'default_short_codes_length' => $helper->getDefaultShortCodesLength(),
'redirect_status_code' => (int) env('REDIRECT_STATUS_CODE', DEFAULT_REDIRECT_STATUS_CODE),
'redirect_cache_lifetime' => (int) env('REDIRECT_CACHE_LIFETIME', DEFAULT_REDIRECT_CACHE_LIFETIME),
],
'not_found_redirects' => $helper->getNotFoundRedirectsConfig(),
@@ -140,6 +159,7 @@ return [
'mezzio-swoole' => [
'swoole-http-server' => [
'port' => (int) env('PORT', 8080),
'options' => [
'worker_num' => (int) env('WEB_WORKER_NUM', 16),
'task_worker_num' => (int) env('TASK_WORKER_NUM', 16),
@@ -151,4 +171,6 @@ return [
'license_key' => env('GEOLITE_LICENSE_KEY', 'G4Lm0C60yJsnkdPi'),
],
'mercure' => $helper->getMercureConfig(),
];

View File

@@ -0,0 +1,210 @@
{
"asyncapi": "2.0.0",
"info": {
"title": "Shlink",
"version": "2.0.0",
"description": "Shlink, the self-hosted URL shortener",
"license": {
"name": "MIT",
"url": "https://github.com/shlinkio/shlink/blob/develop/LICENSE"
}
},
"defaultContentType": "application/json",
"channels": {
"http://shlink.io/new-visit": {
"subscribe": {
"summary": "Receive information about any new visit occurring on any short URL.",
"operationId": "newVisit",
"message": {
"payload": {
"type": "object",
"additionalProperties": false,
"properties": {
"shortUrl": {
"$ref": "#/components/schemas/ShortUrl"
},
"visit": {
"$ref": "#/components/schemas/Visit"
}
}
}
}
}
},
"http://shlink.io/new-visit/{shortCode}": {
"parameters": {
"shortCode": {
"description": "The short code of the short URL",
"schema": {
"type": "string"
}
}
},
"subscribe": {
"summary": "Receive information about any new visit occurring on a specific short URL.",
"operationId": "newShortUrlVisit",
"message": {
"payload": {
"type": "object",
"additionalProperties": false,
"properties": {
"shortUrl": {
"$ref": "#/components/schemas/ShortUrl"
},
"visit": {
"$ref": "#/components/schemas/Visit"
}
}
}
}
}
}
},
"components": {
"schemas": {
"ShortUrl": {
"type": "object",
"properties": {
"shortCode": {
"type": "string",
"description": "The short code for this short URL."
},
"shortUrl": {
"type": "string",
"description": "The short URL."
},
"longUrl": {
"type": "string",
"description": "The original long URL."
},
"dateCreated": {
"type": "string",
"format": "date-time",
"description": "The date in which the short URL was created in ISO format."
},
"visitsCount": {
"type": "integer",
"description": "The number of visits that this short URL has recieved."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of tags applied to this short URL"
},
"meta": {
"$ref": "#/components/schemas/ShortUrlMeta"
},
"domain": {
"type": "string",
"description": "The domain in which the short URL was created. Null if it belongs to default domain."
}
},
"example": {
"shortCode": "12C18",
"shortUrl": "https://doma.in/12C18",
"longUrl": "https://store.steampowered.com",
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsCount": 328,
"tags": [
"games",
"tech"
],
"meta": {
"validSince": "2017-01-21T00:00:00+02:00",
"validUntil": null,
"maxVisits": 100
},
"domain": "example.com"
}
},
"ShortUrlMeta": {
"type": "object",
"required": [
"validSince",
"validUntil",
"maxVisits"
],
"properties": {
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": "string",
"nullable": true
},
"validUntil": {
"description": "The date (in ISO-8601 format) until which this short code will be valid",
"type": "string",
"nullable": true
},
"maxVisits": {
"description": "The maximum number of allowed visits for this short code",
"type": "number",
"nullable": true
}
}
},
"Visit": {
"type": "object",
"properties": {
"referer": {
"type": "string",
"description": "The origin from which the visit was performed"
},
"date": {
"type": "string",
"format": "date-time",
"description": "The date in which the visit was performed"
},
"userAgent": {
"type": "string",
"description": "The user agent from which the visit was performed"
},
"visitLocation": {
"$ref": "#/components/schemas/VisitLocation"
}
},
"example": {
"referer": "https://t.co",
"date": "2015-08-20T05:05:03+04:00",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"visitLocation": {
"cityName": "Cupertino",
"countryCode": "US",
"countryName": "United States",
"latitude": 37.3042,
"longitude": -122.0946,
"regionName": "California",
"timezone": "America/Los_Angeles"
}
}
},
"VisitLocation": {
"type": "object",
"properties": {
"cityName": {
"type": "string"
},
"countryCode": {
"type": "string"
},
"countryName": {
"type": "string"
},
"latitude": {
"type": "number"
},
"longitude": {
"type": "number"
},
"regionName": {
"type": "string"
},
"timezone": {
"type": "string"
}
}
}
}
}
}

View File

@@ -0,0 +1,18 @@
{
"type": "object",
"required": ["mercureHubUrl", "jwt", "jwtExpiration"],
"properties": {
"mercureHubUrl": {
"type": "string",
"description": "The public URL of the mercure hub that can be used to get real-time updates published by Shlink"
},
"jwt": {
"type": "string",
"description": "A JWT with subscribe permissions which is valid with the mercure hub"
},
"jwtExpiration": {
"type": "string",
"description": "The date (in ISO-8601 format) in which the JWT will expire"
}
}
}

View File

@@ -0,0 +1,17 @@
{
"type": "object",
"properties": {
"tag": {
"type": "string",
"description": "The unique tag name"
},
"shortUrlsCount": {
"type": "number",
"description": "The amount of short URLs using this tag"
},
"userAgent": {
"type": "number",
"description": "The combined amount of visits received by short URLs with this tag"
}
}
}

View File

@@ -0,0 +1,10 @@
{
"type": "object",
"required": ["visitsCount"],
"properties": {
"visitsCount": {
"type": "number",
"description": "The total amount of visits received."
}
}
}

View File

@@ -31,7 +31,7 @@
{
"name": "tags[]",
"in": "query",
"description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)",
"description": "A list of tags used to filter the result set. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)",
"required": false,
"schema": {
"type": "array",
@@ -48,10 +48,14 @@
"schema": {
"type": "string",
"enum": [
"longUrl",
"shortCode",
"dateCreated",
"visits"
"longUrl-ASC",
"longUrl-DESC",
"shortCode-ASC",
"shortCode-DESC",
"dateCreated-ASC",
"dateCreated-DESC",
"visits-ASC",
"visits-DESC"
]
}
},
@@ -187,7 +191,7 @@
"Short URLs"
],
"summary": "Create short URL",
"description": "Creates a new short URL.<br></br>**Param findIfExists:**: Starting with v1.16, this new param allows to force shlink to return existing short URLs when found based on provided params, instead of creating a new one. However, it might add complexity and have unexpected outputs.\n\nThese are the use cases:\n* Only the long URL is provided: It will return the newest match or create a new short URL if none is found.\n* Long url and custom slug are provided: It will return the short URL when both params match, return an error when the slug is in use for another long URL, or create a new short URL otherwise.\n* Any of the above but including other params (tags, validSince, validUntil, maxVisits): It will behave the same as the previous two cases, but it will try to exactly match existing results using all the params. If any of them does not match, it will try to create a new short URL.",
"description": "Creates a new short URL.<br></br>**Param findIfExists**: This new param allows to force shlink to return existing short URLs when found based on provided params, instead of creating a new one. However, it might add complexity and have unexpected outputs.\n\nThese are the use cases:\n* Only the long URL is provided: It will return the newest match or create a new short URL if none is found.\n* Long url and custom slug are provided: It will return the short URL when both params match, return an error when the slug is in use for another long URL, or create a new short URL otherwise.\n* Any of the above but including other params (tags, validSince, validUntil, maxVisits): It will behave the same as the previous two cases, but it will try to exactly match existing results using all the params. If any of them does not match, it will try to create a new short URL.",
"security": [
{
"ApiKey": []
@@ -247,6 +251,10 @@
"shortCodeLength": {
"description": "The length for generated short code. It has to be at least 4 and defaults to 5. It will be ignored when customSlug is provided",
"type": "number"
},
"validateUrl": {
"description": "Tells if the long URL should or should not be validated as a reachable URL. If not provided, it will fall back to app-level config",
"type": "boolean"
}
}
}

View File

@@ -127,6 +127,10 @@
"maxVisits": {
"description": "The maximum number of allowed visits for this short code",
"type": "number"
},
"validateUrl": {
"description": "Tells if the long URL (if provided) should or should not be validated as a reachable URL. If not provided, it will fall back to app-level config",
"type": "boolean"
}
}
}

View File

@@ -14,6 +14,19 @@
"parameters": [
{
"$ref": "../parameters/version.json"
},
{
"name": "withStats",
"description": "Whether you want to include also a list with general stats by tag or not.",
"in": "query",
"required": false,
"schema": {
"type": "string",
"enum": [
"true",
"false"
]
}
}
],
"responses": {
@@ -26,12 +39,20 @@
"properties": {
"tags": {
"type": "object",
"required": ["data"],
"properties": {
"data": {
"type": "array",
"items": {
"type": "string"
}
},
"stats": {
"description": "The tag stats will be returned only if the withStats param was provided with value 'true'",
"type": "array",
"items": {
"$ref": "../definitions/TagInfo.json"
}
}
}
}
@@ -66,12 +87,13 @@
},
"post": {
"deprecated": true,
"operationId": "createTags",
"tags": [
"Tags"
],
"summary": "Create tags",
"description": "Provided a list of tags, creates all that do not yet exist",
"description": "Provided a list of tags, creates all that do not yet exist<br />This endpoint is deprecated, as tags are automatically created while creating a short URL",
"security": [
{
"ApiKey": []
@@ -210,6 +232,16 @@
}
}
},
"403": {
"description": "The API key you used does not have permissions to rename tags.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"404": {
"description": "There's no tag found with the name provided in oldName param.",
"content": {
@@ -276,6 +308,16 @@
"204": {
"description": "Tags properly deleted"
},
"403": {
"description": "The API key you used does not have permissions to delete tags.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {

View File

@@ -0,0 +1,86 @@
{
"get": {
"operationId": "listDomains",
"tags": [
"Domains"
],
"summary": "List existing domains",
"description": "Returns the list of all domains ever used, with a flag that tells if they are the default domain",
"security": [
{
"ApiKey": []
}
],
"parameters": [
{
"$ref": "../parameters/version.json"
}
],
"responses": {
"200": {
"description": "The list of tags",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["domains"],
"properties": {
"domains": {
"type": "object",
"required": ["data"],
"properties": {
"data": {
"type": "array",
"items": {
"type": "object",
"required": ["domain", "isDefault"],
"properties": {
"domain": {
"type": "string"
},
"isDefault": {
"type": "boolean"
}
}
}
}
}
}
}
}
}
},
"examples": {
"application/json": {
"domains": {
"data": [
{
"domain": "example.com",
"isDefault": true
},
{
"domain": "aaa.com",
"isDefault": false
},
{
"domain": "bbb.com",
"isDefault": false
}
]
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,67 @@
{
"get": {
"operationId": "mercureInfo",
"tags": [
"Integrations"
],
"summary": "Get mercure integration info",
"description": "Returns information to consume updates published by Shlink on a mercure hub. https://mercure.rocks/",
"parameters": [
{
"$ref": "../parameters/version.json"
}
],
"security": [
{
"ApiKey": []
}
],
"responses": {
"200": {
"description": "The mercure integration info",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/MercureInfo.json"
}
}
},
"examples": {
"application/json": {
"mercureHubUrl": "https://example.com/.well-known/mercure",
"jwt": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTaGxpbmsiLCJpYXQiOjE1ODY2ODY3MzIsImV4cCI6MTU4Njk0NTkzMiwibWVyY3VyZSI6eyJzdWJzY3JpYmUiOltdfX0.P-519lgU7dFz0bbNlRG1CXyqugGbaHon4kw6fu4QBdQ",
"jwtExpiration": "2020-04-15T12:18:52+02:00"
}
}
},
"501": {
"description": "This Shlink instance is not integrated with a mercure hub",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
},
"examples": {
"application/json": {
"title": "Mercure integration not configured",
"type": "MERCURE_NOT_CONFIGURED",
"detail": "This Shlink instance is not integrated with a mercure hub.",
"status": 501
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,154 @@
{
"get": {
"operationId": "getTagVisits",
"tags": [
"Visits"
],
"summary": "List visits for tag",
"description": "Get the list of visits on any short URL which is tagged with provided tag.",
"parameters": [
{
"$ref": "../parameters/version.json"
},
{
"name": "tag",
"in": "path",
"description": "The tag from which we want to get the visits.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "startDate",
"in": "query",
"description": "The date (in ISO-8601 format) from which we want to get visits.",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "endDate",
"in": "query",
"description": "The date (in ISO-8601 format) until which we want to get visits.",
"required": false,
"schema": {
"type": "string"
}
},
{
"name": "page",
"in": "query",
"description": "The page to display. Defaults to 1",
"required": false,
"schema": {
"type": "number"
}
},
{
"name": "itemsPerPage",
"in": "query",
"description": "The amount of items to return on every page. Defaults to all the items",
"required": false,
"schema": {
"type": "number"
}
}
],
"security": [
{
"ApiKey": []
}
],
"responses": {
"200": {
"description": "List of visits.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"visits": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "../definitions/Visit.json"
}
},
"pagination": {
"$ref": "../definitions/Pagination.json"
}
}
}
}
}
}
},
"examples": {
"application/json": {
"visits": {
"data": [
{
"referer": "https://twitter.com",
"date": "2015-08-20T05:05:03+04:00",
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0",
"visitLocation": null
},
{
"referer": "https://t.co",
"date": "2015-08-20T05:05:03+04:00",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"visitLocation": {
"cityName": "Cupertino",
"countryCode": "US",
"countryName": "United States",
"latitude": 37.3042,
"longitude": -122.0946,
"regionName": "California",
"timezone": "America/Los_Angeles"
}
},
{
"referer": null,
"date": "2015-08-20T05:05:03+04:00",
"userAgent": "some_web_crawler/1.4",
"visitLocation": null
}
],
"pagination": {
"currentPage": 5,
"pagesCount": 12,
"itemsPerPage": 10,
"itemsInCurrentPage": 10,
"totalItems": 115
}
}
}
}
},
"404": {
"description": "The tag does not exist.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,54 @@
{
"get": {
"operationId": "getGlobalVisits",
"tags": [
"Visits"
],
"summary": "Get general visits stats",
"description": "Get general visits stats not linked to one specific short URL.",
"parameters": [
{
"$ref": "../parameters/version.json"
}
],
"security": [
{
"ApiKey": []
}
],
"responses": {
"200": {
"description": "Visits stats.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"visits": {
"$ref": "../definitions/VisitStats.json"
}
}
}
}
},
"examples": {
"application/json": {
"visits": {
"visitsCount": 1569874
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -18,7 +18,7 @@
},
{
"name": "size",
"in": "path",
"in": "query",
"description": "The size of the image to be returned.",
"required": false,
"schema": {
@@ -27,6 +27,19 @@
"maximum": 1000,
"default": 300
}
},
{
"name": "format",
"in": "query",
"description": "The format for the QR code image, being valid values png and svg. Not providing the param or providing any other value will fall back to png.",
"required": false,
"schema": {
"type": "string",
"enum": [
"png",
"svg"
]
}
}
],
"responses": {
@@ -38,6 +51,12 @@
"type": "string",
"format": "binary"
}
},
"image/svg+xml": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}

View File

@@ -0,0 +1,66 @@
{
"get": {
"operationId": "shortUrlQrCodeSize",
"deprecated": true,
"tags": [
"URL Shortener"
],
"summary": "Short URL QR code",
"description": "Generates a QR code image pointing to a short URL",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "size",
"in": "path",
"description": "The size of the image to be returned.",
"required": false,
"schema": {
"type": "integer",
"minimum": 50,
"maximum": 1000,
"default": 300
}
},
{
"name": "format",
"in": "query",
"description": "The format for the QR code image, being valid values png and svg. Not providing the param or providing any other value will fall back to png.",
"required": false,
"schema": {
"type": "string",
"enum": [
"png",
"svg"
]
}
}
],
"responses": {
"200": {
"description": "QR code in PNG format",
"content": {
"image/png": {
"schema": {
"type": "string",
"format": "binary"
}
},
"image/svg+xml": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
}
}

View File

@@ -50,6 +50,14 @@
"name": "Visits",
"description": "Operations to manage visits on short URLs"
},
{
"name": "Domains",
"description": "Operations to manage domains used on short URLs"
},
{
"name": "Integrations",
"description": "Handle services with which shlink is integrated"
},
{
"name": "Monitoring",
"description": "Public endpoints designed to monitor the service"
@@ -78,9 +86,23 @@
"$ref": "paths/v1_tags.json"
},
"/rest/v{version}/visits": {
"$ref": "paths/v2_visits.json"
},
"/rest/v{version}/short-urls/{shortCode}/visits": {
"$ref": "paths/v1_short-urls_{shortCode}_visits.json"
},
"/rest/v{version}/tags/{tag}/visits": {
"$ref": "paths/v2_tags_{tag}_visits.json"
},
"/rest/v{version}/domains": {
"$ref": "paths/v2_domains.json"
},
"/rest/v{version}/mercure-info": {
"$ref": "paths/v2_mercure-info.json"
},
"/rest/health": {
"$ref": "paths/health.json"
@@ -94,6 +116,9 @@
},
"/{shortCode}/qr-code": {
"$ref": "paths/{shortCode}_qr-code.json"
},
"/{shortCode}/qr-code/{size}": {
"$ref": "paths/{shortCode}_qr-code_{size}.json"
}
}
}

23
infection-db.json Normal file
View File

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

View File

@@ -6,11 +6,11 @@
},
"timeout": 5,
"logs": {
"text": "build/infection/infection-log.txt",
"summary": "build/infection/summary-log.txt",
"debug": "build/infection/debug-log.txt"
"text": "build/infection-unit/infection-log.txt",
"summary": "build/infection-unit/summary-log.txt",
"debug": "build/infection-unit/debug-log.txt"
},
"tmpDir": "build/infection/temp",
"tmpDir": "build/infection-unit/temp",
"phpUnit": {
"configDir": "."
},

View File

@@ -3,9 +3,13 @@
declare(strict_types=1);
return [
'name' => 'ShlinkMigrations',
'migrations_namespace' => 'ShlinkMigrations',
'table_name' => 'migrations',
'migrations_directory' => 'data/migrations',
'migrations_paths' => [
'ShlinkMigrations' => 'data/migrations',
],
'table_storage' => [
'table_name' => 'migrations',
],
'custom_template' => 'data/migrations_template.txt',
];

View File

@@ -25,6 +25,8 @@ return [
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class,
Command\Domain\ListDomainsCommand::NAME => Command\Domain\ListDomainsCommand::class,
Command\Db\CreateDatabaseCommand::NAME => Command\Db\CreateDatabaseCommand::class,
Command\Db\MigrateDatabaseCommand::NAME => Command\Db\MigrateDatabaseCommand::class,
],

View File

@@ -8,9 +8,10 @@ use Doctrine\DBAL\Connection;
use GeoIp2\Database\Reader;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Shlinkio\Shlink\CLI\Util\GeolocationDbUpdater;
use Shlinkio\Shlink\Common\Doctrine\NoDbNameConnectionFactory;
use Shlinkio\Shlink\Core\Domain\DomainService;
use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Core\Tag\TagService;
use Shlinkio\Shlink\Core\Visit;
use Shlinkio\Shlink\Installer\Factory\ProcessHelperFactory;
use Shlinkio\Shlink\IpGeolocation\GeoLite2\DbUpdater;
@@ -30,7 +31,8 @@ return [
SymfonyCli\Helper\ProcessHelper::class => ProcessHelperFactory::class,
PhpExecutableFinder::class => InvokableFactory::class,
GeolocationDbUpdater::class => ConfigAbstractFactory::class,
Util\GeolocationDbUpdater::class => ConfigAbstractFactory::class,
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,
Command\ShortUrl\GenerateShortUrlCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
@@ -51,11 +53,14 @@ return [
Command\Db\CreateDatabaseCommand::class => ConfigAbstractFactory::class,
Command\Db\MigrateDatabaseCommand::class => ConfigAbstractFactory::class,
Command\Domain\ListDomainsCommand::class => ConfigAbstractFactory::class,
],
],
ConfigAbstractFactory::class => [
GeolocationDbUpdater::class => [DbUpdater::class, Reader::class, LOCAL_LOCK_FACTORY],
Util\GeolocationDbUpdater::class => [DbUpdater::class, Reader::class, LOCAL_LOCK_FACTORY],
ApiKey\RoleResolver::class => [DomainService::class],
Command\ShortUrl\GenerateShortUrlCommand::class => [
Service\UrlShortener::class,
@@ -71,17 +76,19 @@ return [
Visit\VisitLocator::class,
IpLocationResolverInterface::class,
LockFactory::class,
GeolocationDbUpdater::class,
Util\GeolocationDbUpdater::class,
],
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class],
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, ApiKey\RoleResolver::class],
Command\Api\DisableKeyCommand::class => [ApiKeyService::class],
Command\Api\ListKeysCommand::class => [ApiKeyService::class],
Command\Tag\ListTagsCommand::class => [Service\Tag\TagService::class],
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class],
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class],
Command\Tag\DeleteTagsCommand::class => [Service\Tag\TagService::class],
Command\Tag\ListTagsCommand::class => [TagService::class],
Command\Tag\CreateTagCommand::class => [TagService::class],
Command\Tag\RenameTagCommand::class => [TagService::class],
Command\Tag\DeleteTagsCommand::class => [TagService::class],
Command\Domain\ListDomainsCommand::class => [DomainService::class],
Command\Db\CreateDatabaseCommand::class => [
LockFactory::class,

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\ApiKey;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
use Symfony\Component\Console\Input\InputInterface;
class RoleResolver implements RoleResolverInterface
{
private DomainServiceInterface $domainService;
public function __construct(DomainServiceInterface $domainService)
{
$this->domainService = $domainService;
}
public function determineRoles(InputInterface $input): array
{
$domainAuthority = $input->getOption('domain-only');
$author = $input->getOption('author-only');
$roleDefinitions = [];
if ($author) {
$roleDefinitions[] = RoleDefinition::forAuthoredShortUrls();
}
if ($domainAuthority !== null) {
$domain = $this->domainService->getOrCreate($domainAuthority);
$roleDefinitions[] = RoleDefinition::forDomain($domain);
}
return $roleDefinitions;
}
}

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\ApiKey;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
use Symfony\Component\Console\Input\InputInterface;
interface RoleResolverInterface
{
public const AUTHOR_ONLY_PARAM = 'author-only';
public const DOMAIN_ONLY_PARAM = 'domain-only';
/**
* @return RoleDefinition[]
*/
public function determineRoles(InputInterface $input): array;
}

View File

@@ -5,7 +5,10 @@ declare(strict_types=1);
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\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -13,6 +16,7 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function Shlinkio\Shlink\Core\arrayToString;
use function sprintf;
class GenerateKeyCommand extends Command
@@ -20,15 +24,35 @@ class GenerateKeyCommand extends Command
public const NAME = 'api-key:generate';
private ApiKeyServiceInterface $apiKeyService;
private RoleResolverInterface $roleResolver;
public function __construct(ApiKeyServiceInterface $apiKeyService)
public function __construct(ApiKeyServiceInterface $apiKeyService, RoleResolverInterface $roleResolver)
{
$this->apiKeyService = $apiKeyService;
parent::__construct();
$this->apiKeyService = $apiKeyService;
$this->roleResolver = $roleResolver;
}
protected function configure(): void
{
$authorOnly = RoleResolverInterface::AUTHOR_ONLY_PARAM;
$domainOnly = RoleResolverInterface::DOMAIN_ONLY_PARAM;
$help = <<<HELP
The <info>%command.name%</info> generates a new valid API key.
<info>%command.full_name%</info>
You can optionally set its expiration date with <comment>--expirationDate</comment> or <comment>-e</comment>:
<info>%command.full_name% --expirationDate 2020-01-01</info>
You can also set roles to the API key:
* 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>
HELP;
$this
->setName(self::NAME)
->setDescription('Generates a new valid API key.')
@@ -37,15 +61,42 @@ class GenerateKeyCommand extends Command
'e',
InputOption::VALUE_REQUIRED,
'The date in which the API key should expire. Use any valid PHP format.',
);
)
->addOption(
$authorOnly,
'a',
InputOption::VALUE_NONE,
sprintf('Adds the "%s" role to the new API key.', Role::AUTHORED_SHORT_URLS),
)
->addOption(
$domainOnly,
'd',
InputOption::VALUE_REQUIRED,
sprintf('Adds the "%s" role to the new API key, with the domain provided.', Role::DOMAIN_SPECIFIC),
)
->setHelp($help);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$expirationDate = $input->getOption('expirationDate');
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? Chronos::parse($expirationDate) : null);
$apiKey = $this->apiKeyService->create(
isset($expirationDate) ? Chronos::parse($expirationDate) : null,
...$this->roleResolver->determineRoles($input),
);
$io = new SymfonyStyle($input, $output);
$io->success(sprintf('Generated API key: "%s"', $apiKey->toString()));
if (! $apiKey->isAdmin()) {
ShlinkTable::fromOutput($io)->render(
['Role name', 'Role metadata'],
$apiKey->mapRoles(fn (string $name, array $meta) => [$name, arrayToString($meta, 0)]),
null,
'Roles',
);
}
(new SymfonyStyle($input, $output))->success(sprintf('Generated API key: "%s"', $apiKey));
return ExitCodes::EXIT_SUCCESS;
}
}

View File

@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
@@ -14,7 +15,8 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use function array_filter;
use function array_map;
use function Functional\map;
use function implode;
use function sprintf;
class ListKeysCommand extends Command
@@ -50,7 +52,7 @@ class ListKeysCommand extends Command
{
$enabledOnly = $input->getOption('enabledOnly');
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
$rows = map($this->apiKeyService->listKeys($enabledOnly), function (ApiKey $apiKey) use ($enabledOnly) {
$expiration = $apiKey->getExpirationDate();
$messagePattern = $this->determineMessagePattern($apiKey);
@@ -60,13 +62,21 @@ class ListKeysCommand extends Command
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
}
$rowData[] = $expiration !== null ? $expiration->toAtomString() : '-';
$rowData[] = $apiKey->isAdmin() ? 'Admin' : implode("\n", $apiKey->mapRoles(
fn (string $roleName, array $meta) =>
empty($meta)
? Role::toFriendlyName($roleName)
: sprintf('%s: %s', Role::toFriendlyName($roleName), Role::domainAuthorityFromMeta($meta)),
));
return $rowData;
}, $this->apiKeyService->listKeys($enabledOnly));
});
ShlinkTable::fromOutput($output)->render(array_filter([
'Key',
! $enabledOnly ? 'Is enabled' : null,
'Expiration date',
'Roles',
]), $rows);
return ExitCodes::EXIT_SUCCESS;
}
@@ -80,8 +90,6 @@ class ListKeysCommand extends Command
return $apiKey->isExpired() ? self::WARNING_STRING_PATTERN : self::SUCCESS_STRING_PATTERN;
}
/**
*/
private function getEnabledSymbol(ApiKey $apiKey): string
{
return ! $apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++';

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Domain;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function Functional\map;
class ListDomainsCommand extends Command
{
public const NAME = 'domain:list';
private DomainServiceInterface $domainService;
public function __construct(DomainServiceInterface $domainService)
{
parent::__construct();
$this->domainService = $domainService;
}
protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('List all domains that have been ever used for some short URL');
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$domains = $this->domainService->listDomains();
ShlinkTable::fromOutput($output)->render(
['Domain', 'Is default'],
map($domains, fn (DomainItem $domain) => [$domain->toString(), $domain->isDefault() ? 'Yes' : 'No']),
);
return ExitCodes::EXIT_SUCCESS;
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Laminas\Diactoros\Uri;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
@@ -22,7 +21,9 @@ use function array_map;
use function Functional\curry;
use function Functional\flatten;
use function Functional\unique;
use function method_exists;
use function sprintf;
use function strpos;
class GenerateShortUrlCommand extends Command
{
@@ -95,6 +96,18 @@ class GenerateShortUrlCommand extends Command
'l',
InputOption::VALUE_REQUIRED,
'The length for generated short code (it will be ignored if --customSlug was provided).',
)
->addOption(
'validate-url',
null,
InputOption::VALUE_NONE,
'Forces the long URL to be validated, regardless what is globally configured.',
)
->addOption(
'no-validate-url',
null,
InputOption::VALUE_NONE,
'Forces the long URL to not be validated, regardless what is globally configured.',
);
}
@@ -126,21 +139,19 @@ class GenerateShortUrlCommand extends Command
$customSlug = $input->getOption('customSlug');
$maxVisits = $input->getOption('maxVisits');
$shortCodeLength = $input->getOption('shortCodeLength') ?? $this->defaultShortCodeLength;
$doValidateUrl = $this->doValidateUrl($input);
try {
$shortUrl = $this->urlShortener->urlToShortCode(
new Uri($longUrl),
$tags,
ShortUrlMeta::fromRawData([
ShortUrlMetaInputFilter::VALID_SINCE => $input->getOption('validSince'),
ShortUrlMetaInputFilter::VALID_UNTIL => $input->getOption('validUntil'),
ShortUrlMetaInputFilter::CUSTOM_SLUG => $customSlug,
ShortUrlMetaInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null,
ShortUrlMetaInputFilter::FIND_IF_EXISTS => $input->getOption('findIfExists'),
ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'),
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
]),
);
$shortUrl = $this->urlShortener->shorten($longUrl, $tags, ShortUrlMeta::fromRawData([
ShortUrlMetaInputFilter::VALID_SINCE => $input->getOption('validSince'),
ShortUrlMetaInputFilter::VALID_UNTIL => $input->getOption('validUntil'),
ShortUrlMetaInputFilter::CUSTOM_SLUG => $customSlug,
ShortUrlMetaInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null,
ShortUrlMetaInputFilter::FIND_IF_EXISTS => $input->getOption('findIfExists'),
ShortUrlMetaInputFilter::DOMAIN => $input->getOption('domain'),
ShortUrlMetaInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
ShortUrlMetaInputFilter::VALIDATE_URL => $doValidateUrl,
]));
$io->writeln([
sprintf('Processed long URL: <info>%s</info>', $longUrl),
@@ -152,4 +163,18 @@ class GenerateShortUrlCommand extends Command
return ExitCodes::EXIT_FAILURE;
}
}
private function doValidateUrl(InputInterface $input): ?bool
{
$rawInput = method_exists($input, '__toString') ? $input->__toString() : '';
if (strpos($rawInput, '--no-validate-url') !== false) {
return false;
}
if (strpos($rawInput, '--validate-url') !== false) {
return true;
}
return null;
}
}

View File

@@ -11,7 +11,6 @@ use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering;
use Shlinkio\Shlink\Core\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
@@ -61,7 +60,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
'page',
'p',
InputOption::VALUE_REQUIRED,
sprintf('The first page to list (%s items per page)', ShortUrlRepositoryAdapter::ITEMS_PER_PAGE),
'The first page to list (10 items per page unless "--all" is provided)',
'1',
)
->addOption(
@@ -82,7 +81,14 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
InputOption::VALUE_REQUIRED,
'The field from which we want to order by. Pass ASC or DESC separated by a comma',
)
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not');
->addOption('showTags', null, InputOption::VALUE_NONE, 'Whether to display the tags or not')
->addOption(
'all',
'a',
InputOption::VALUE_NONE,
'Disables pagination and just displays all existing URLs. Caution! If the amount of short URLs is big,'
. ' this may end up failing due to memory usage.',
);
}
protected function getStartDateDesc(): string
@@ -104,24 +110,32 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$tags = $input->getOption('tags');
$tags = ! empty($tags) ? explode(',', $tags) : [];
$showTags = (bool) $input->getOption('showTags');
$all = (bool) $input->getOption('all');
$startDate = $this->getDateOption($input, $output, 'startDate');
$endDate = $this->getDateOption($input, $output, 'endDate');
$orderBy = $this->processOrderBy($input);
$data = [
ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
ShortUrlsParamsInputFilter::TAGS => $tags,
ShortUrlsOrdering::ORDER_BY => $orderBy,
ShortUrlsParamsInputFilter::START_DATE => $startDate !== null ? $startDate->toAtomString() : null,
ShortUrlsParamsInputFilter::END_DATE => $endDate !== null ? $endDate->toAtomString() : null,
];
if ($all) {
$data[ShortUrlsParamsInputFilter::ITEMS_PER_PAGE] = -1;
}
do {
$result = $this->renderPage($output, $showTags, ShortUrlsParams::fromRawData([
ShortUrlsParamsInputFilter::PAGE => $page,
ShortUrlsParamsInputFilter::SEARCH_TERM => $searchTerm,
ShortUrlsParamsInputFilter::TAGS => $tags,
ShortUrlsOrdering::ORDER_BY => $orderBy,
ShortUrlsParamsInputFilter::START_DATE => $startDate !== null ? $startDate->toAtomString() : null,
ShortUrlsParamsInputFilter::END_DATE => $endDate !== null ? $endDate->toAtomString() : null,
]));
$data[ShortUrlsParamsInputFilter::PAGE] = $page;
$result = $this->renderPage($output, $showTags, ShortUrlsParams::fromRawData($data), $all);
$page++;
$continue = $this->isLastPage($result)
? false
: $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
$continue = ! $this->isLastPage($result) && $io->confirm(
sprintf('Continue with page <options=bold>%s</>?', $page),
false,
);
} while ($continue);
$io->newLine();
@@ -130,7 +144,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
return ExitCodes::EXIT_SUCCESS;
}
private function renderPage(OutputInterface $output, bool $showTags, ShortUrlsParams $params): Paginator
private function renderPage(OutputInterface $output, bool $showTags, ShortUrlsParams $params, bool $all): Paginator
{
$result = $this->shortUrlService->listShortUrls($params);
@@ -151,7 +165,7 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$rows[] = array_values(array_intersect_key($shortUrl, array_flip(self::COLUMNS_WHITELIST)));
}
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
ShlinkTable::fromOutput($output)->render($headers, $rows, $all ? null : $this->formatCurrentPageMessage(
$result,
'Page %s of %s',
));

View File

@@ -5,13 +5,14 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/** @deprecated */
class CreateTagCommand extends Command
{
public const NAME = 'tag:create';
@@ -28,7 +29,7 @@ class CreateTagCommand extends Command
{
$this
->setName(self::NAME)
->setDescription('Creates one or more tags.')
->setDescription('[Deprecated] Creates one or more tags.')
->addOption(
'name',
't',

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;

View File

@@ -6,8 +6,8 @@ namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Shlinkio\Shlink\Core\Tag\Model\TagInfo;
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -35,17 +35,20 @@ class ListTagsCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
ShlinkTable::fromOutput($output)->render(['Name'], $this->getTagsRows());
ShlinkTable::fromOutput($output)->render(['Name', 'URLs amount', 'Visits amount'], $this->getTagsRows());
return ExitCodes::EXIT_SUCCESS;
}
private function getTagsRows(): array
{
$tags = $this->tagService->listTags();
$tags = $this->tagService->tagsInfo();
if (empty($tags)) {
return [['No tags yet']];
return [['No tags found', '-', '-']];
}
return map($tags, fn (Tag $tag) => [(string) $tag]);
return map(
$tags,
fn (TagInfo $tagInfo) => [(string) $tagInfo->tag(), $tagInfo->shortUrlsCount(), $tagInfo->visitsCount()],
);
}
}

View File

@@ -7,7 +7,8 @@ namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Exception\TagConflictException;
use Shlinkio\Shlink\Core\Exception\TagNotFoundException;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Shlinkio\Shlink\Core\Tag\Model\TagRenaming;
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -42,7 +43,7 @@ class RenameTagCommand extends Command
$newName = $input->getArgument('newName');
try {
$this->tagService->renameTag($oldName, $newName);
$this->tagService->renameTag(TagRenaming::fromNames($oldName, $newName));
$io->success('Tag properly renamed.');
return ExitCodes::EXIT_SUCCESS;
} catch (TagNotFoundException | TagConflictException $e) {

View File

@@ -52,7 +52,7 @@ class GeolocationDbUpdater implements GeolocationDbUpdaterInterface
}
$meta = $this->geoLiteDbReader->metadata();
if ($this->buildIsTooOld($meta->__get('buildEpoch'))) {
if ($this->buildIsTooOld($meta->buildEpoch)) {
$this->downloadNewDb(true, $mustBeUpdated, $handleProgress);
}
}

View File

@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\ApiKey;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\ApiKey\RoleResolver;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Core\Entity\Domain;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
use Symfony\Component\Console\Input\InputInterface;
class RoleResolverTest extends TestCase
{
use ProphecyTrait;
private RoleResolver $resolver;
private ObjectProphecy $domainService;
protected function setUp(): void
{
$this->domainService = $this->prophesize(DomainServiceInterface::class);
$this->resolver = new RoleResolver($this->domainService->reveal());
}
/**
* @test
* @dataProvider provideRoles
*/
public function properRolesAreResolvedBasedOnInput(
InputInterface $input,
array $expectedRoles,
int $expectedDomainCalls
): void {
$getDomain = $this->domainService->getOrCreate('example.com')->willReturn(
(new Domain('example.com'))->setId('1'),
);
$result = $this->resolver->determineRoles($input);
self::assertEquals($expectedRoles, $result);
$getDomain->shouldHaveBeenCalledTimes($expectedDomainCalls);
}
public function provideRoles(): iterable
{
$domain = (new Domain('example.com'))->setId('1');
$buildInput = function (array $definition): InputInterface {
$input = $this->prophesize(InputInterface::class);
foreach ($definition as $name => $value) {
$input->getOption($name)->willReturn($value);
}
return $input->reveal();
};
yield 'no roles' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => null, RoleResolver::AUTHOR_ONLY_PARAM => false]),
[],
0,
];
yield 'domain role only' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => 'example.com', RoleResolver::AUTHOR_ONLY_PARAM => false]),
[RoleDefinition::forDomain($domain)],
1,
];
yield 'author role only' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => null, RoleResolver::AUTHOR_ONLY_PARAM => true]),
[RoleDefinition::forAuthoredShortUrls()],
0,
];
yield 'both roles' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => 'example.com', RoleResolver::AUTHOR_ONLY_PARAM => true]),
[RoleDefinition::forAuthoredShortUrls(), RoleDefinition::forDomain($domain)],
1,
];
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
@@ -14,6 +15,8 @@ use Symfony\Component\Console\Tester\CommandTester;
class DisableKeyCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $apiKeyService;
@@ -37,7 +40,7 @@ class DisableKeyCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('API key "abcd1234" properly disabled', $output);
self::assertStringContainsString('API key "abcd1234" properly disabled', $output);
}
/** @test */
@@ -52,7 +55,7 @@ class DisableKeyCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString($expectedMessage, $output);
self::assertStringContainsString($expectedMessage, $output);
$disable->shouldHaveBeenCalledOnce();
}
}

View File

@@ -7,22 +7,31 @@ namespace ShlinkioTest\Shlink\CLI\Command\Api;
use Cake\Chronos\Chronos;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\ApiKey\RoleResolverInterface;
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Tester\CommandTester;
class GenerateKeyCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $apiKeyService;
private ObjectProphecy $roleResolver;
public function setUp(): void
{
$this->apiKeyService = $this->prophesize(ApiKeyServiceInterface::class);
$command = new GenerateKeyCommand($this->apiKeyService->reveal());
$this->roleResolver = $this->prophesize(RoleResolverInterface::class);
$this->roleResolver->determineRoles(Argument::type(InputInterface::class))->willReturn([]);
$command = new GenerateKeyCommand($this->apiKeyService->reveal(), $this->roleResolver->reveal());
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
@@ -36,7 +45,7 @@ class GenerateKeyCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Generated API key: ', $output);
self::assertStringContainsString('Generated API key: ', $output);
$create->shouldHaveBeenCalledOnce();
}

View File

@@ -5,8 +5,11 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
use Shlinkio\Shlink\Core\Entity\Domain;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Application;
@@ -14,6 +17,8 @@ use Symfony\Component\Console\Tester\CommandTester;
class ListKeysCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $apiKeyService;
@@ -26,42 +31,87 @@ class ListKeysCommandTest extends TestCase
$this->commandTester = new CommandTester($command);
}
/** @test */
public function everythingIsListedIfEnabledOnlyIsNotProvided(): void
/**
* @test
* @dataProvider provideKeysAndOutputs
*/
public function returnsExpectedOutput(array $keys, bool $enabledOnly, string $expected): void
{
$this->apiKeyService->listKeys(false)->willReturn([
new ApiKey(),
new ApiKey(),
new ApiKey(),
])->shouldBeCalledOnce();
$listKeys = $this->apiKeyService->listKeys($enabledOnly)->willReturn($keys);
$this->commandTester->execute([]);
$this->commandTester->execute(['--enabledOnly' => $enabledOnly]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Key', $output);
$this->assertStringContainsString('Is enabled', $output);
$this->assertStringContainsString(' +++ ', $output);
$this->assertStringNotContainsString(' --- ', $output);
$this->assertStringContainsString('Expiration date', $output);
self::assertEquals($expected, $output);
$listKeys->shouldHaveBeenCalledOnce();
}
/** @test */
public function onlyEnabledKeysAreListedIfEnabledOnlyIsProvided(): void
public function provideKeysAndOutputs(): iterable
{
$this->apiKeyService->listKeys(true)->willReturn([
(new ApiKey())->disable(),
new ApiKey(),
])->shouldBeCalledOnce();
yield 'all keys' => [
[ApiKey::withKey('foo'), ApiKey::withKey('bar'), ApiKey::withKey('baz')],
false,
<<<OUTPUT
+-----+------------+-----------------+-------+
| Key | Is enabled | Expiration date | Roles |
+-----+------------+-----------------+-------+
| foo | +++ | - | Admin |
| bar | +++ | - | Admin |
| baz | +++ | - | Admin |
+-----+------------+-----------------+-------+
$this->commandTester->execute([
'--enabledOnly' => true,
]);
$output = $this->commandTester->getDisplay();
OUTPUT,
];
yield 'enabled keys' => [
[ApiKey::withKey('foo')->disable(), ApiKey::withKey('bar')],
true,
<<<OUTPUT
+-----+-----------------+-------+
| Key | Expiration date | Roles |
+-----+-----------------+-------+
| foo | - | Admin |
| bar | - | Admin |
+-----+-----------------+-------+
$this->assertStringContainsString('Key', $output);
$this->assertStringNotContainsString('Is enabled', $output);
$this->assertStringNotContainsString(' +++ ', $output);
$this->assertStringNotContainsString(' --- ', $output);
$this->assertStringContainsString('Expiration date', $output);
OUTPUT,
];
yield 'with roles' => [
[
ApiKey::withKey('foo'),
$this->apiKeyWithRoles('bar', [RoleDefinition::forAuthoredShortUrls()]),
$this->apiKeyWithRoles('baz', [RoleDefinition::forDomain((new Domain('example.com'))->setId('1'))]),
ApiKey::withKey('foo2'),
$this->apiKeyWithRoles('baz2', [
RoleDefinition::forAuthoredShortUrls(),
RoleDefinition::forDomain((new Domain('example.com'))->setId('1')),
]),
ApiKey::withKey('foo3'),
],
true,
<<<OUTPUT
+------+-----------------+--------------------------+
| Key | Expiration date | Roles |
+------+-----------------+--------------------------+
| foo | - | Admin |
| bar | - | Author only |
| baz | - | Domain only: example.com |
| foo2 | - | Admin |
| baz2 | - | Author only |
| | | Domain only: example.com |
| foo3 | - | Admin |
+------+-----------------+--------------------------+
OUTPUT,
];
}
private function apiKeyWithRoles(string $key, array $roles): ApiKey
{
$apiKey = ApiKey::withKey($key);
foreach ($roles as $role) {
$apiKey->registerRole($role);
}
return $apiKey;
}
}

View File

@@ -9,6 +9,7 @@ use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Db\CreateDatabaseCommand;
use Symfony\Component\Console\Application;
@@ -22,10 +23,11 @@ use Symfony\Component\Process\Process;
class CreateDatabaseCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $processHelper;
private ObjectProphecy $regularConn;
private ObjectProphecy $noDbNameConn;
private ObjectProphecy $schemaManager;
private ObjectProphecy $databasePlatform;
@@ -48,15 +50,15 @@ class CreateDatabaseCommandTest extends TestCase
$this->regularConn = $this->prophesize(Connection::class);
$this->regularConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
$this->regularConn->getDatabasePlatform()->willReturn($this->databasePlatform->reveal());
$this->noDbNameConn = $this->prophesize(Connection::class);
$this->noDbNameConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
$noDbNameConn = $this->prophesize(Connection::class);
$noDbNameConn->getSchemaManager()->willReturn($this->schemaManager->reveal());
$command = new CreateDatabaseCommand(
$locker->reveal(),
$this->processHelper->reveal(),
$phpExecutableFinder->reveal(),
$this->regularConn->reveal(),
$this->noDbNameConn->reveal(),
$noDbNameConn->reveal(),
);
$app = new Application();
$app->add($command);
@@ -77,7 +79,7 @@ class CreateDatabaseCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Database already exists. Run "db:migrate" command', $output);
self::assertStringContainsString('Database already exists. Run "db:migrate" command', $output);
$getDatabase->shouldHaveBeenCalledOnce();
$listDatabases->shouldHaveBeenCalledOnce();
$createDatabase->shouldNotHaveBeenCalled();
@@ -121,8 +123,8 @@ class CreateDatabaseCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Creating database tables...', $output);
$this->assertStringContainsString('Database properly created!', $output);
self::assertStringContainsString('Creating database tables...', $output);
self::assertStringContainsString('Database properly created!', $output);
$getDatabase->shouldHaveBeenCalledOnce();
$listDatabases->shouldHaveBeenCalledOnce();
$createDatabase->shouldNotHaveBeenCalled();

View File

@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\Db;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Db\MigrateDatabaseCommand;
use Symfony\Component\Console\Application;
@@ -19,6 +20,8 @@ use Symfony\Component\Process\Process;
class MigrateDatabaseCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $processHelper;
@@ -60,8 +63,8 @@ class MigrateDatabaseCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Migrating database...', $output);
$this->assertStringContainsString('Database properly migrated!', $output);
self::assertStringContainsString('Migrating database...', $output);
self::assertStringContainsString('Database properly migrated!', $output);
$runCommand->shouldHaveBeenCalledOnce();
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Domain;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Domain\ListDomainsCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
class ListDomainsCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $domainService;
public function setUp(): void
{
$this->domainService = $this->prophesize(DomainServiceInterface::class);
$command = new ListDomainsCommand($this->domainService->reveal());
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/** @test */
public function allDomainsAreProperlyPrinted(): void
{
$expectedOutput = <<<OUTPUT
+---------+------------+
| Domain | Is default |
+---------+------------+
| foo.com | Yes |
| bar.com | No |
| baz.com | No |
+---------+------------+
OUTPUT;
$listDomains = $this->domainService->listDomains()->willReturn([
new DomainItem('foo.com', true),
new DomainItem('bar.com', false),
new DomainItem('baz.com', false),
]);
$this->commandTester->execute([]);
self::assertEquals($expectedOutput, $this->commandTester->getDisplay());
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
$listDomains->shouldHaveBeenCalledOnce();
}
}

View File

@@ -6,6 +6,7 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\DeleteShortUrlCommand;
use Shlinkio\Shlink\Core\Exception;
@@ -21,6 +22,8 @@ use const PHP_EOL;
class DeleteShortUrlCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $service;
@@ -47,7 +50,7 @@ class DeleteShortUrlCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString(
self::assertStringContainsString(
sprintf('Short URL with short code "%s" successfully deleted.', $shortCode),
$output,
);
@@ -66,7 +69,7 @@ class DeleteShortUrlCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
self::assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
$deleteByShortCode->shouldHaveBeenCalledOnce();
}
@@ -95,11 +98,11 @@ class DeleteShortUrlCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString(sprintf(
self::assertStringContainsString(sprintf(
'Impossible to delete short URL with short code "%s" since it has more than "10" visits.',
$shortCode,
), $output);
$this->assertStringContainsString($expectedMessage, $output);
self::assertStringContainsString($expectedMessage, $output);
$deleteByShortCode->shouldHaveBeenCalledTimes($expectedDeleteCalls);
}
@@ -122,11 +125,11 @@ class DeleteShortUrlCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString(sprintf(
self::assertStringContainsString(sprintf(
'Impossible to delete short URL with short code "%s" since it has more than "10" visits.',
$shortCode,
), $output);
$this->assertStringContainsString('Short URL was not deleted.', $output);
self::assertStringContainsString('Short URL was not deleted.', $output);
$deleteByShortCode->shouldHaveBeenCalledOnce();
}
}

View File

@@ -7,19 +7,22 @@ namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\UriInterface;
use Shlinkio\Shlink\CLI\Command\ShortUrl\GenerateShortUrlCommand;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
class GenerateShortUrlCommandTest extends TestCase
{
use ProphecyTrait;
private const DOMAIN_CONFIG = [
'schema' => 'http',
'hostname' => 'foo.com',
@@ -41,7 +44,7 @@ class GenerateShortUrlCommandTest extends TestCase
public function properShortCodeIsCreatedIfLongUrlIsCorrect(): void
{
$shortUrl = new ShortUrl('');
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn($shortUrl);
$urlToShortCode = $this->urlShortener->shorten(Argument::cetera())->willReturn($shortUrl);
$this->commandTester->execute([
'longUrl' => 'http://domain.com/foo/bar',
@@ -49,8 +52,8 @@ class GenerateShortUrlCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
$this->assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
self::assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
@@ -58,28 +61,28 @@ class GenerateShortUrlCommandTest extends TestCase
public function exceptionWhileParsingLongUrlOutputsError(): void
{
$url = 'http://domain.com/invalid';
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(InvalidUrlException::fromUrl($url))
$this->urlShortener->shorten(Argument::cetera())->willThrow(InvalidUrlException::fromUrl($url))
->shouldBeCalledOnce();
$this->commandTester->execute(['longUrl' => $url]);
$output = $this->commandTester->getDisplay();
$this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
$this->assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
self::assertStringContainsString('Provided URL http://domain.com/invalid is invalid.', $output);
}
/** @test */
public function providingNonUniqueSlugOutputsError(): void
{
$urlToShortCode = $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(
$urlToShortCode = $this->urlShortener->shorten(Argument::cetera())->willThrow(
NonUniqueSlugException::fromSlug('my-slug'),
);
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--customSlug' => 'my-slug']);
$output = $this->commandTester->getDisplay();
$this->assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
$this->assertStringContainsString('Provided slug "my-slug" is already in use', $output);
self::assertEquals(ExitCodes::EXIT_FAILURE, $this->commandTester->getStatusCode());
self::assertStringContainsString('Provided slug "my-slug" is already in use', $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
@@ -87,8 +90,8 @@ class GenerateShortUrlCommandTest extends TestCase
public function properlyProcessesProvidedTags(): void
{
$shortUrl = new ShortUrl('');
$urlToShortCode = $this->urlShortener->urlToShortCode(
Argument::type(UriInterface::class),
$urlToShortCode = $this->urlShortener->shorten(
Argument::type('string'),
Argument::that(function (array $tags) {
Assert::assertEquals(['foo', 'bar', 'baz', 'boo', 'zar'], $tags);
return $tags;
@@ -102,8 +105,38 @@ class GenerateShortUrlCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
$this->assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
self::assertEquals(ExitCodes::EXIT_SUCCESS, $this->commandTester->getStatusCode());
self::assertStringContainsString($shortUrl->toString(self::DOMAIN_CONFIG), $output);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
/**
* @test
* @dataProvider provideFlags
*/
public function urlValidationHasExpectedValueBasedOnProvidedTags(array $options, ?bool $expectedValidateUrl): void
{
$shortUrl = new ShortUrl('');
$urlToShortCode = $this->urlShortener->shorten(
Argument::type('string'),
Argument::type('array'),
Argument::that(function (ShortUrlMeta $meta) use ($expectedValidateUrl) {
Assert::assertEquals($expectedValidateUrl, $meta->doValidateUrl());
return $meta;
}),
)->willReturn($shortUrl);
$options['longUrl'] = 'http://domain.com/foo/bar';
$this->commandTester->execute($options);
$urlToShortCode->shouldHaveBeenCalledOnce();
}
public function provideFlags(): iterable
{
yield 'no flags' => [[], null];
yield 'no-validate-url only' => [['--no-validate-url' => true], false];
yield 'validate-url' => [['--validate-url' => true], true];
yield 'both flags' => [['--validate-url' => true, '--no-validate-url' => true], false];
}
}

View File

@@ -9,6 +9,7 @@ use Laminas\Paginator\Adapter\ArrayAdapter;
use Laminas\Paginator\Paginator;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\GetVisitsCommand;
use Shlinkio\Shlink\Common\Util\DateRange;
@@ -27,6 +28,8 @@ use function sprintf;
class GetVisitsCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $visitsTracker;
@@ -88,7 +91,7 @@ class GetVisitsCommandTest extends TestCase
$output = $this->commandTester->getDisplay();
$info->shouldHaveBeenCalledOnce();
$this->assertStringContainsString(
self::assertStringContainsString(
sprintf('Ignored provided "startDate" since its value "%s" is not a valid date', $startDate),
$output,
);
@@ -108,8 +111,8 @@ class GetVisitsCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('foo', $output);
$this->assertStringContainsString('Spain', $output);
$this->assertStringContainsString('bar', $output);
self::assertStringContainsString('foo', $output);
self::assertStringContainsString('Spain', $output);
self::assertStringContainsString('bar', $output);
}
}

View File

@@ -9,6 +9,7 @@ use Laminas\Paginator\Adapter\ArrayAdapter;
use Laminas\Paginator\Paginator;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
@@ -21,6 +22,8 @@ use function explode;
class ListShortUrlsCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $shortUrlService;
@@ -50,9 +53,9 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Continue with page 2?', $output);
$this->assertStringContainsString('Continue with page 3?', $output);
$this->assertStringContainsString('Continue with page 4?', $output);
self::assertStringContainsString('Continue with page 2?', $output);
self::assertStringContainsString('Continue with page 3?', $output);
self::assertStringContainsString('Continue with page 4?', $output);
}
/** @test */
@@ -72,13 +75,13 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('url_1', $output);
$this->assertStringContainsString('url_9', $output);
$this->assertStringNotContainsString('url_10', $output);
$this->assertStringNotContainsString('url_20', $output);
$this->assertStringNotContainsString('url_30', $output);
$this->assertStringContainsString('Continue with page 2?', $output);
$this->assertStringNotContainsString('Continue with page 3?', $output);
self::assertStringContainsString('url_1', $output);
self::assertStringContainsString('url_9', $output);
self::assertStringNotContainsString('url_10', $output);
self::assertStringNotContainsString('url_20', $output);
self::assertStringNotContainsString('url_30', $output);
self::assertStringContainsString('Continue with page 2?', $output);
self::assertStringNotContainsString('Continue with page 3?', $output);
}
/** @test */
@@ -103,7 +106,7 @@ class ListShortUrlsCommandTest extends TestCase
$this->commandTester->setInputs(['y']);
$this->commandTester->execute(['--showTags' => true]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Tags', $output);
self::assertStringContainsString('Tags', $output);
}
/**
@@ -192,4 +195,22 @@ class ListShortUrlsCommandTest extends TestCase
yield [['--orderBy' => 'foo,ASC'], ['foo' => 'ASC']];
yield [['--orderBy' => 'bar,DESC'], ['bar' => 'DESC']];
}
/** @test */
public function requestingAllElementsWillSetItemsPerPage(): void
{
$listShortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData([
'page' => 1,
'searchTerm' => null,
'tags' => [],
'startDate' => null,
'endDate' => null,
'orderBy' => null,
'itemsPerPage' => -1,
]))->willReturn(new Paginator(new ArrayAdapter()));
$this->commandTester->execute(['--all' => true]);
$listShortUrls->shouldHaveBeenCalledOnce();
}
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\ShortUrl;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ResolveUrlCommand;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
@@ -20,6 +21,8 @@ use const PHP_EOL;
class ResolveUrlCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $urlResolver;
@@ -44,7 +47,7 @@ class ResolveUrlCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
self::assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
}
/** @test */
@@ -59,6 +62,6 @@ class ResolveUrlCommandTest extends TestCase
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
self::assertStringContainsString(sprintf('No URL found with short code "%s"', $shortCode), $output);
}
}

View File

@@ -6,14 +6,17 @@ namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use Doctrine\Common\Collections\ArrayCollection;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
class CreateTagCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $tagService;
@@ -34,7 +37,7 @@ class CreateTagCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('You have to provide at least one tag name', $output);
self::assertStringContainsString('You have to provide at least one tag name', $output);
}
/** @test */
@@ -48,7 +51,7 @@ class CreateTagCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Tags properly created', $output);
self::assertStringContainsString('Tags properly created', $output);
$createTags->shouldHaveBeenCalled();
}
}

View File

@@ -5,14 +5,17 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Shlinkio\Shlink\Core\Tag\TagServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
class DeleteTagsCommandTest extends TestCase
{
use ProphecyTrait;
private CommandTester $commandTester;
private ObjectProphecy $tagService;
@@ -33,7 +36,7 @@ class DeleteTagsCommandTest extends TestCase
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('You have to provide at least one tag name', $output);
self::assertStringContainsString('You have to provide at least one tag name', $output);
}
/** @test */
@@ -48,7 +51,7 @@ class DeleteTagsCommandTest extends TestCase
]);
$output = $this->commandTester->getDisplay();
$this->assertStringContainsString('Tags properly deleted', $output);
self::assertStringContainsString('Tags properly deleted', $output);
$deleteTags->shouldHaveBeenCalled();
}
}

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