Compare commits

...

525 Commits

Author SHA1 Message Date
Alejandro Celaya
f60c217fae Merge pull request #136 from acelaya/feature/1.7.2
Feature/1.7.2
2018-03-26 18:13:48 +02:00
Alejandro Celaya
d3fc7d543a Updated changelog 2018-03-26 18:13:08 +02:00
Alejandro Celaya
4d0fc1da07 Fixed PathVersionMiddleware not being properly propagated 2018-03-26 17:53:22 +02:00
Alejandro Celaya
ee2233c6dd Updated PathVersionMiddleware to single-pass middleware 2018-03-26 17:36:58 +02:00
Alejandro Celaya
ea6e0d7c7f Merge branch 'develop' 2018-03-21 16:31:27 +01:00
Alejandro Celaya
d9d599eab4 Updated changelog 2018-03-21 16:31:00 +01:00
Alejandro Celaya
d1ba44e1b3 Merge pull request #128 from weirdan/upgrade-to-expressive-2.2
Upgrade to expressive 2.2
2018-03-21 16:27:09 +01:00
Bruce Weirdan
dff2ad3740 define property to please scrutinizer 2018-03-21 12:13:03 +02:00
Bruce Weirdan
f7e63710e4 updated tests to fix deprecations
also fixed cs errors in middleware-pipeline
2018-03-21 02:05:55 +02:00
Bruce Weirdan
d3b5cd5c57 fixed middleware deprecations 2018-03-21 01:46:26 +02:00
Alejandro Celaya
86ed83d25e Merge branch 'develop' 2018-02-03 10:23:18 +01:00
Alejandro Celaya
f96d0fe30a Merge pull request #124 from acelaya/feature/o-a-s-3
Feature/o a s 3
2018-02-03 10:14:32 +01:00
Alejandro Celaya
be406bd676 Removed no-longer used Authorization parameter 2018-02-03 10:13:10 +01:00
Alejandro Celaya
044278752b Fixed server 2018-02-03 10:09:42 +01:00
Alejandro Celaya
343d2ab44a Added domain 2018-02-03 10:07:37 +01:00
Alejandro Celaya
66992f644e Added default value for server 2018-02-03 10:06:04 +01:00
Alejandro Celaya
cf245524dd Added missing base path in server 2018-02-03 10:01:16 +01:00
Alejandro Celaya
ad520811a3 Fixed dynamic host 2018-02-03 09:55:53 +01:00
Alejandro Celaya
ee1e1d5688 Updated swagger docs to OAS3 2018-02-03 09:53:40 +01:00
Alejandro Celaya
8ef0e7c25b Merge pull request #121 from shlinkio/develop
Develop
2018-01-21 10:11:12 +01:00
Alejandro Celaya
c3d555ef3c Added missing null coalescing operator 2018-01-21 10:01:18 +01:00
Alejandro Celaya
bf8e14708b Merge pull request #116 from acelaya/feature/1.7.0
Feature/1.7.0
2018-01-21 10:00:11 +01:00
Alejandro Celaya
6ea59b1e4d Updated changelog 2018-01-21 09:43:01 +01:00
Alejandro Celaya
cf8b778711 Updated language files 2018-01-21 09:40:38 +01:00
Alejandro Celaya
1e79969c3b Made visits not to be tracked if query param has been provided 2018-01-14 09:24:33 +01:00
Alejandro Celaya
5fd34e03fc Added new app config param to allow disabling short URL visits tracking 2018-01-14 09:13:49 +01:00
Alejandro Celaya
ce9d6642d4 Fixed edit short code action not being properly registered 2018-01-07 21:13:06 +01:00
Alejandro Celaya
ecebdbbfa8 Updated API docs including new endpoint and updating params for short code creation 2018-01-07 20:54:02 +01:00
Alejandro Celaya
6f7ce709ca Fixed PhpStan error 2018-01-07 20:46:28 +01:00
Alejandro Celaya
84094a51a2 Implemented EditShortCodeAction 2018-01-07 20:45:05 +01:00
Alejandro Celaya
7ba9eb8e2c Fixed coding styles 2018-01-07 20:08:07 +01:00
Alejandro Celaya
e8a0c5484c Added test for ShortUrlMeta 2018-01-07 20:07:12 +01:00
Alejandro Celaya
0521227127 Tested new method to update short URLs metadata 2018-01-07 20:00:21 +01:00
Alejandro Celaya
fac9455a1e Created method to updated already created short URLs 2018-01-07 19:51:25 +01:00
Alejandro Celaya
3243ade4fd Improved error message when installation fails 2017-12-31 19:31:35 +01:00
Alejandro Celaya
da21eb4a5c Removed return type incompatible with PHP 7.0 2017-12-31 19:24:22 +01:00
Alejandro Celaya
5ec6d538db Improved and simplified ProcessVisitsCommand thanks to SymfonyStyle 2017-12-31 19:13:42 +01:00
Alejandro Celaya
08228d9d98 Improved and simplified RenameTagCommand thanks to SymfonyStyle 2017-12-31 19:10:27 +01:00
Alejandro Celaya
7856d64299 Improved and simplified ListTagsCommand thanks to SymfonyStyle 2017-12-31 19:08:10 +01:00
Alejandro Celaya
057bbae729 Improved and simplified DeleteTagCommand thanks to SymfonyStyle 2017-12-31 19:06:04 +01:00
Alejandro Celaya
09b161304c Improved and simplified CreateTagCommand thanks to SymfonyStyle 2017-12-31 19:03:41 +01:00
Alejandro Celaya
a60c45ca4d Simplified and improved ResolveUrlCommand with SymfonyStyle 2017-12-31 18:58:11 +01:00
Alejandro Celaya
89ed84ce28 Removed unused use statements 2017-12-31 18:38:25 +01:00
Alejandro Celaya
a6c547c4da Improved and simplified ListShortcodesCommand with SymfonyStyle 2017-12-31 18:37:39 +01:00
Alejandro Celaya
3e2c5abaa4 Improved GetVisitsCommand by using SymfonyStyle 2017-12-31 18:17:58 +01:00
Alejandro Celaya
c202b3e518 Improved GenerateShortcodeCommand by using SymfonyStyle 2017-12-31 18:12:43 +01:00
Alejandro Celaya
e15b67b5dc Improved GeneratePreviewCommand using SymfonyStyle 2017-12-31 18:04:11 +01:00
Alejandro Celaya
7ddc180487 Simplified InstallCommand 2017-12-31 17:59:50 +01:00
Alejandro Celaya
f3fbfc3692 Fixed phpstan error 2017-12-31 17:54:01 +01:00
Alejandro Celaya
b289e3bac2 Applied more improvements on InstallCommand with SymfonyStyle 2017-12-31 17:52:17 +01:00
Alejandro Celaya
4d4aafa6db Fixed config customizer tests 2017-12-31 17:45:27 +01:00
Alejandro Celaya
2705070063 Renamed tests 2017-12-31 17:22:25 +01:00
Alejandro Celaya
5e3770c105 Renamed ConfigCustomizerPluginManager to CongigCustomizerManager 2017-12-31 17:20:03 +01:00
Alejandro Celaya
0f0213aa87 Removed plugin suffix on config ustomizers 2017-12-31 17:18:54 +01:00
Alejandro Celaya
0e2ad0dbca Updated ConfigCustomizer api to expect a SymfonyStyle object instead of a set of input and output 2017-12-31 17:14:01 +01:00
Alejandro Celaya
d275316acd Applied SymfonyStyle to all installation config customizers 2017-12-31 17:07:39 +01:00
Alejandro Celaya
0a681f0efa Simplified UrlShortenerConfigCustomizerPlugin thanks to SymfonyStyle 2017-12-31 17:00:26 +01:00
Alejandro Celaya
b17f96043a Simplified and standardized DatabaseConfigCustomizerPlugin thanks to SymfonyStyle 2017-12-31 16:53:18 +01:00
Alejandro Celaya
6f9b727673 Merge branch 'feature/1.7.0' of github.com:acelaya/shlink into feature/1.7.0 2017-12-31 16:29:55 +01:00
Alejandro Celaya
79427d08d7 Added phpstan config file to export-ignore list 2017-12-30 21:39:18 +01:00
Alejandro Celaya
2ec807ba70 Increased phpstan required level 2017-12-30 21:36:33 +01:00
Alejandro Celaya
ede4525332 Refactored exceptions to properly use package exceptions 2017-12-30 21:35:26 +01:00
Alejandro Celaya
4dffc9f0c1 Improved and simplified all installation process thanks to symfony style 2017-12-28 15:52:10 +01:00
Alejandro Celaya
5de845c258 Improved GenerateSecretCommand by using SymfonyStyle 2017-12-28 15:17:12 +01:00
Alejandro Celaya
745ff51150 Ensured phpstan config is properly loaded in Ci envs 2017-12-28 15:07:14 +01:00
Alejandro Celaya
88b9f9fc56 Fixed GenerateCharsetCommandTest 2017-12-28 14:59:52 +01:00
Alejandro Celaya
fdbe076bf2 Added phpstan config file excluding a file that fails 2017-12-28 14:55:55 +01:00
Alejandro Celaya
0760550767 Removed unnecessary type hints 2017-12-28 09:48:34 +01:00
Alejandro Celaya
1b94083188 Improved GenerateCharsetCommand by using SymfonyStyle 2017-12-28 09:48:17 +01:00
Alejandro Celaya
1993d01110 Dimplified GenerateKeyCommand by using SymfonyStyle 2017-12-27 17:36:07 +01:00
Alejandro Celaya
37fb7e76d9 Simplified DisableKeyCommand using SymfonyStyle 2017-12-27 17:32:39 +01:00
Alejandro Celaya
cc3362837b Simplified ListKeysCommand using SymfonyStyle 2017-12-27 17:28:51 +01:00
Alejandro Celaya
2012cc453c Fixed PHPStan errors due to API inconsistency in EntityManager and EntityManagerInterface 2017-12-27 17:22:51 +01:00
Alejandro Celaya
ea80b6d48a Replaced vlucas/phpdotenv package by symfony/dotenv 2017-12-27 16:33:06 +01:00
Alejandro Celaya
db956a1f40 Fixed all possible PHPStan errors 2017-12-27 16:23:54 +01:00
Alejandro Celaya
4f3995ea80 Fixed phpstan errors in ListKeysCommand 2017-12-27 15:56:26 +01:00
Alejandro Celaya
e024ba5d94 Added phpstan to build matrix 2017-12-27 15:43:59 +01:00
Alejandro Celaya
af0ff0f65b Console commands are now lazy loaded 2017-12-27 15:37:26 +01:00
Alejandro Celaya
a9094dc0f6 Updated dependency constraints 2017-12-27 15:25:59 +01:00
Alejandro Celaya
aca90ef907 Merge pull request #111 from shlinkio/develop
Develop
2017-10-25 16:25:39 +02:00
Alejandro Celaya
ddfccea901 Updated changelog 2017-10-25 16:21:34 +02:00
Alejandro Celaya
6c6cfb4fc3 Fixed typo 2017-10-25 16:19:08 +02:00
Alejandro Celaya
482b792adf Merge branch 'develop' 2017-10-24 17:45:54 +02:00
Alejandro Celaya
d3a86f4fae Added v1.6.1 to CHANGELOG 2017-10-24 17:45:19 +02:00
Alejandro Celaya
6de1d1057b Created gitattributes file 2017-10-24 17:43:19 +02:00
Alejandro Celaya
d7e68d29de Merge branch 'develop' 2017-10-23 14:01:22 +02:00
Alejandro Celaya
b8f24c3584 Merge pull request #107 from acelaya/feature/1.6
Feature/1.6
2017-10-23 14:00:24 +02:00
Alejandro Celaya
26c455616b Ensured validate_url does not fail when importing config, and instead, it gets a default BC value 2017-10-23 13:56:07 +02:00
Alejandro Celaya
909ecc2387 Ensured build does not fail when trying to delete missing optional files 2017-10-23 13:44:50 +02:00
Alejandro Celaya
a9dff56a92 Updated config aggregator version 2017-10-23 13:33:23 +02:00
Alejandro Celaya
ef0dd416f9 Updated CHANGELOG 2017-10-23 13:30:26 +02:00
Alejandro Celaya
781ca39938 Fixed merge conflicts 2017-10-23 13:28:09 +02:00
Alejandro Celaya
ccb9d5e83a Merge pull request #105 from gogus/feature/url-validation-option
Added option for enable/disable URL Validation by response status code.
2017-10-23 13:18:30 +02:00
Alejandro Celaya
91f08c9ead Updated CHANGELOG 2017-10-23 13:09:10 +02:00
Alejandro Celaya
433a5a923d Improved ShortUrlRepositoryTest 2017-10-23 13:06:29 +02:00
Alejandro Celaya
501a933d2e Created ShortUrlRepositoryTest 2017-10-23 13:03:23 +02:00
Alejandro Celaya
633f3b728f Created composer command to generate merge pretty code coverage 2017-10-23 12:54:54 +02:00
Alejandro Celaya
be34600494 Updated CI process to generate a merged coverage file 2017-10-23 12:51:06 +02:00
Alejandro Celaya
9577a4da4b Refactored ShortUrlRepository to make it more readable 2017-10-23 12:36:03 +02:00
Alejandro Celaya
a24688b92a Created VisitRepositoryTest 2017-10-23 12:33:46 +02:00
Alejandro Celaya
91442a3379 Ensured same testing database is used to generate with entities and to run functional tests 2017-10-23 12:19:28 +02:00
Alejandro Celaya
0bd9f1e19f Removed wrong extension for travis 2017-10-23 12:09:07 +02:00
Alejandro Celaya
c233e807c2 Added sqlite extensions to PDO 2017-10-23 11:42:13 +02:00
Alejandro Celaya
a002c60183 Updated travis config so that it loads apcu extension 2017-10-23 11:37:16 +02:00
Alejandro Celaya
c522879c64 Updated composer check to ru functional tests too 2017-10-23 11:29:37 +02:00
Mikolaj Gogula
d04abd1f75 Added validate_url config key for development. 2017-10-23 11:28:04 +02:00
Alejandro Celaya
c2feffa50c First version of functional tests working 2017-10-23 11:21:00 +02:00
Alejandro Celaya
e282521040 Updated migrations to prevent duplication when running after orm schema tool 2017-10-23 11:11:26 +02:00
Alejandro Celaya
d7b7db670f Created first common elements for functional tests 2017-10-22 18:03:35 +02:00
Alejandro Celaya
5a500a00d7 Added another feature to CHANGELOG 2017-10-22 09:18:28 +02:00
Alejandro Celaya
9fb07f4039 Fixed tests 2017-10-22 09:17:19 +02:00
Alejandro Celaya
cb23d38b38 Used maxVisits field when creating or fetching a ShortUrl 2017-10-22 09:15:37 +02:00
Alejandro Celaya
af7c11665c Added max_visits field to short_urls 2017-10-22 09:00:32 +02:00
Alejandro Celaya
7f4678261e Added first tasks to the CHANGELOG 2017-10-21 20:27:19 +02:00
Alejandro Celaya
a1c8c51f70 Updated translations 2017-10-21 20:21:08 +02:00
Alejandro Celaya
6bbe66e8f1 Improved CreateShortcodeActiontest 2017-10-21 20:16:39 +02:00
Alejandro Celaya
5f0d281255 Updated create shortcode action to accept the custom slug 2017-10-21 20:09:30 +02:00
Alejandro Celaya
fd468cd4e9 Added support for custom slug in shortcode command 2017-10-21 17:32:05 +02:00
Alejandro Celaya
1f7a94794d Added option to provide custom slug when creating a short url 2017-10-21 17:19:02 +02:00
Alejandro Celaya
0232f68b91 Updated action and command to create short urls so that it accepts validity dates 2017-10-21 12:24:53 +02:00
Alejandro Celaya
070055a8b9 Fixed type hints 2017-10-21 11:59:31 +02:00
Alejandro Celaya
a3bbd06fe3 Updated UrlShortener so that it does not match a short code which is out of the validity dat erange 2017-10-21 11:58:20 +02:00
Alejandro Celaya
68b4cfbae0 Added valid_since and valid_until columns to shoirt_urls table 2017-10-21 11:39:27 +02:00
Alejandro Celaya
97a54aef06 Imported function in config file 2017-10-21 11:28:44 +02:00
Mikolaj Gogula
297c88c334 Change variable name for validation enabled. 2017-10-17 11:44:30 +02:00
Mikolaj Gogula
fef5390a62 Codestyle fixes. 2017-10-17 11:35:32 +02:00
Mikolaj Gogula
08d18b1dc1 Codestyle fixes. 2017-10-17 11:33:11 +02:00
Mikolaj Gogula
16a2349d86 Composer fixes. 2017-10-17 11:28:05 +02:00
Mikolaj Gogula
18d9815e88 Added option for enable/disable URL Validation by response status code. 2017-10-17 11:03:12 +02:00
Alejandro Celaya
c8346bc5f8 Added target php platform in composer.json to prevent building versions that cannot be executed in older versions 2017-10-16 18:23:13 +02:00
Alejandro Celaya
2d85a207d1 Removed comented translations 2017-10-13 12:31:44 +02:00
Alejandro Celaya
0df8f17e7b Added new translations 2017-10-13 12:30:54 +02:00
Alejandro Celaya
29645e77cf Created DottedAccessConfigAbstractFactory 2017-10-13 12:27:20 +02:00
Alejandro Celaya
ea76092681 Ensured a generic template is used to render generic 404 errors, and a more specific one to render 'invalid short url' errors 2017-10-13 12:22:19 +02:00
Alejandro Celaya
c12e13dfd7 Created NotFoundDelegateTest 2017-10-13 12:02:00 +02:00
Alejandro Celaya
566940349f Created default delegate that returns a JSON response when accepted type is json 2017-10-13 11:55:14 +02:00
Alejandro Celaya
391ef5c323 Added return typehint 2017-10-12 11:59:22 +02:00
Alejandro Celaya
70264be8e7 Fixed coding styles 2017-10-12 11:29:11 +02:00
Alejandro Celaya
6208f6f0d5 Improved Exception management to be more specific 2017-10-12 11:28:45 +02:00
Alejandro Celaya
c422a14c5c Improved coding styles 2017-10-12 10:13:20 +02:00
Alejandro Celaya
fbeb959317 Fixed tests 2017-10-12 10:03:43 +02:00
Alejandro Celaya
e53ffc8d43 Migrated templates to plates 2017-10-12 10:03:20 +02:00
Alejandro Celaya
453ca1728e Updated system to use plates instead of twig 2017-10-12 09:40:42 +02:00
Alejandro Celaya
d10583c7c5 Udated minimum PHP version and docker stuff 2017-10-12 09:24:47 +02:00
Alejandro Celaya
a9d213990e Merge branch 'feature/1.6' of github.com:acelaya/shlink into feature/1.6 2017-09-30 12:14:05 +02:00
Alejandro Celaya
6321cc0f2d Added security-advisories dependency 2017-09-30 12:13:47 +02:00
Alejandro Celaya
3e5f0b2451 Ensured a doctrine/cache version which requires PHP 7.1 is not installed 2017-09-30 12:13:47 +02:00
Alejandro Celaya
4ad167eb30 Ensured all doctrine components are not updated to latest releases, which require PHP 7.1 2017-09-30 12:13:47 +02:00
Alejandro Celaya
01a4f9f867 Removed any remaining reference to AnnotatedFactory 2017-09-30 12:13:47 +02:00
Alejandro Celaya
b93d65ddc1 Replaced more ussages of AnnotatedFactory by ConfigAbstractFactory 2017-09-30 12:13:47 +02:00
Alejandro Celaya
9ef9da0870 Replaced more ussages of AnnotatedFactory by ConfigAbstractFactory 2017-09-30 12:13:47 +02:00
Alejandro Celaya
ba2053bd3a Dropped AnnotatedFactory in commands and replaced by ConfigAbstractFactory 2017-09-30 12:13:47 +02:00
Alejandro Celaya
1260da85a7 Added PHP 7.2 to the build matrix 2017-09-30 12:13:12 +02:00
Alejandro Celaya
960c7a0835 Added security-advisories dependency 2017-08-19 19:41:07 +02:00
Alejandro Celaya
574f407a90 Ensured a doctrine/cache version which requires PHP 7.1 is not installed 2017-07-23 11:51:47 +02:00
Alejandro Celaya
abf802093c Ensured all doctrine components are not updated to latest releases, which require PHP 7.1 2017-07-23 08:42:57 +02:00
Alejandro Celaya
7ca22f8629 Removed any remaining reference to AnnotatedFactory 2017-07-22 14:20:40 +02:00
Alejandro Celaya
6300982b07 Replaced more ussages of AnnotatedFactory by ConfigAbstractFactory 2017-07-22 13:48:30 +02:00
Alejandro Celaya
54cb40f6ed Replaced more ussages of AnnotatedFactory by ConfigAbstractFactory 2017-07-22 13:41:41 +02:00
Alejandro Celaya
8256f0c757 Dropped AnnotatedFactory in commands and replaced by ConfigAbstractFactory 2017-07-22 13:33:32 +02:00
Alejandro Celaya
1009f9e7c6 Merge pull request #98 from shlinkio/develop
Develop
2017-07-16 10:08:38 +02:00
Alejandro Celaya
9c8eef12ba Merge pull request #97 from acelaya/feature/1.5
Feature/1.5
2017-07-16 10:01:50 +02:00
Alejandro Celaya
9260b3ac6b Fixed coding styles 2017-07-16 09:58:03 +02:00
Alejandro Celaya
ff98e1fb3d Added v1.5.0 to CHANGELOG 2017-07-16 09:43:35 +02:00
Alejandro Celaya
f3389d3738 Updated language files 2017-07-16 09:40:34 +02:00
Alejandro Celaya
a138f4153d Created DeleteTagsCommand 2017-07-16 09:35:24 +02:00
Alejandro Celaya
602e11d5e7 Added namespace to functions 2017-07-16 09:28:40 +02:00
Alejandro Celaya
3cd14153ca Created command to rename tag 2017-07-16 09:24:21 +02:00
Alejandro Celaya
095d8e73b8 Created ListTagsCommand 2017-07-16 09:13:25 +02:00
Alejandro Celaya
b37f303e76 Created CreateTagCommand 2017-07-16 09:09:11 +02:00
Alejandro Celaya
c8368c9098 Updated language files 2017-07-15 12:16:15 +02:00
Alejandro Celaya
8d0bac9478 Documented delete and edit tags endpoints 2017-07-15 12:13:59 +02:00
Alejandro Celaya
286c24f8c0 Improved TagServiceTest 2017-07-15 12:09:25 +02:00
Alejandro Celaya
963d26f59b Created UpdateTagAction 2017-07-15 12:04:12 +02:00
Alejandro Celaya
e07c464de8 Removed strict declarations 2017-07-15 09:15:45 +02:00
Alejandro Celaya
575509c45b Created CreateTagsActiontest 2017-07-15 09:12:07 +02:00
Alejandro Celaya
563e654b99 Created DeleteTagsActionTest 2017-07-15 09:10:09 +02:00
Alejandro Celaya
3e268e2012 Improved TagServiceTest 2017-07-15 09:05:02 +02:00
Alejandro Celaya
b2d9f2fc01 Added Create and Delete tag actions 2017-07-15 09:00:53 +02:00
Alejandro Celaya
6717102dd2 Updated tag actions namespace 2017-07-15 08:31:21 +02:00
Alejandro Celaya
1ba7fc81ac Created ListTagsCommand 2017-07-08 13:17:46 +02:00
Alejandro Celaya
caf4fa7fdd Documented list tags endpoint 2017-07-08 12:55:38 +02:00
Alejandro Celaya
5c7962966d Created ListTagsActionTest 2017-07-07 13:28:58 +02:00
Alejandro Celaya
95ec7e0afa Registered action to list tags 2017-07-07 13:12:45 +02:00
Alejandro Celaya
c37660f763 Created TagService 2017-07-07 12:49:41 +02:00
Alejandro Celaya
486ea10c3c Renamed EditTagsAction to EditShortcodeTagsAction 2017-07-07 11:45:20 +02:00
Alejandro Celaya
e0f18f8d1f Created InstallApplicationFactoryTest 2017-07-06 18:06:11 +02:00
Alejandro Celaya
a66f116d66 Created DatabaseConfigCustomizerPluginTest 2017-07-06 18:00:38 +02:00
Alejandro Celaya
dd099dc39c Removed declare strict types added by mistake 2017-07-06 17:49:05 +02:00
Alejandro Celaya
c05aeabdee Improved if statements reducing indentation 2017-07-06 17:38:16 +02:00
Alejandro Celaya
23922f6c7b Created UrlShortenerConfigCustomizerPluginTest 2017-07-06 17:28:32 +02:00
Alejandro Celaya
69a99949e1 Created LanguageConfigCustomizerPluginTest 2017-07-06 17:22:03 +02:00
Alejandro Celaya
d56cde72a3 Created ApplicationConfigCustomizerPluginTest 2017-07-06 17:12:32 +02:00
Alejandro Celaya
99ffff11c7 Created DefaultConfigCustomizerPluginFactoryTest 2017-07-06 13:43:36 +02:00
Alejandro Celaya
bb050cc1b6 Improved InstallCommandTest coverage 2017-07-06 13:38:15 +02:00
Alejandro Celaya
3547889ad5 Fixed InstallCommandTest 2017-07-06 10:04:35 +02:00
Alejandro Celaya
479e694478 Moved all configuration customization steps to individual plugins 2017-07-05 20:04:44 +02:00
Alejandro Celaya
2368b634e3 Moved command and app creation logic to a factory for install scripts 2017-07-05 18:12:03 +02:00
Alejandro Celaya
dcc09975a9 Abstracted filesystem manipulation in InstallCommand 2017-07-04 20:14:22 +02:00
Alejandro Celaya
102f5c4e12 Updated instalation script to import sqlte database file when importing the rets of the config 2017-07-04 20:01:42 +02:00
Alejandro Celaya
cc688fa3ce Implemented method to deserialize customizable config 2017-07-04 19:48:53 +02:00
Alejandro Celaya
e7f7cbcaac Improved installation command, reducing duplication and moving serialization logic to specific model 2017-07-03 20:46:35 +02:00
Alejandro Celaya
f9c56d7cb1 Added process to import previous configuration when updating shlink 2017-07-03 13:43:53 +02:00
Alejandro Celaya
1fe2e6f6bd Improved check on update and install commands 2017-07-03 13:17:44 +02:00
Alejandro Celaya
c3cc88f03e Fixed inspections 2017-07-03 13:11:45 +02:00
Alejandro Celaya
584e1f5643 Created common abstract command for update and install 2017-07-03 13:10:16 +02:00
Alejandro Celaya
04c479148a Updated build script so that generated zip contains one folder 2017-07-03 12:54:50 +02:00
Alejandro Celaya
c45cb7bacb Updated build script 2017-07-03 12:49:32 +02:00
Alejandro Celaya
17be221920 Added response examples to swagger docs 2017-04-16 10:45:52 +02:00
Alejandro Celaya
10da57572f Fixed date format returned by the API 2017-04-16 10:27:27 +02:00
Alejandro Celaya
52478ca60a Returned all allowed methods until fast route router is fixed 2017-04-14 13:27:41 +02:00
Alejandro Celaya
62b49dcb19 Set cross domain allow-methods header with the same value as the allow header 2017-04-14 12:55:59 +02:00
Alejandro Celaya
a365faef9c Removed requirement of OPTIONS on every route 2017-04-14 12:52:24 +02:00
Alejandro Celaya
5d2698e8a1 Created EmptyResponseImplicitOptionsMiddlewareFactoryTest 2017-04-13 09:52:17 +02:00
Alejandro Celaya
ec4a413a5b Removed options bypass in actions in favor of implicit options middleware 2017-04-13 09:45:31 +02:00
Alejandro Celaya
596d1ee797 Registered implicit options middleware 2017-04-13 09:43:11 +02:00
Alejandro Celaya
d117f82bcb Installed expressive tooling 2017-04-13 09:39:35 +02:00
Alejandro Celaya
18abe9d0f9 Merge branch 'develop' of github.com:shlinkio/shlink into develop 2017-04-13 09:34:52 +02:00
Alejandro Celaya
b53e51de33 Merge branch 'develop' 2017-03-25 10:13:11 +01:00
Alejandro Celaya
9478c71af1 Merge pull request #90 from acelaya/feature/expressive-2
Feature/expressive 2
2017-03-25 10:11:46 +01:00
Alejandro Celaya
a2c4eebec8 Updated CHANGELOG 2017-03-25 10:09:30 +01:00
Alejandro Celaya
2e5a7d76df Migrated rest actions to psr-15 middleware 2017-03-25 10:04:48 +01:00
Alejandro Celaya
288249d0b8 Renamed JsonErrorHandler to JsonErrorResponseGenerator 2017-03-25 09:46:29 +01:00
Alejandro Celaya
cd47aae902 Migrated CrossDomainMiddleware to psr-15 middleware 2017-03-25 09:44:34 +01:00
Alejandro Celaya
9bd18ee041 Migrated CheckAuthenticationMiddleware to psr-15 middleware 2017-03-25 09:37:13 +01:00
Alejandro Celaya
22c76df8e6 Migrated BodyParserMiddleware to psr-15 middleware 2017-03-25 09:22:00 +01:00
Alejandro Celaya
6c87436a96 Migrated QrCodeCacheMiddleware to psr-15 middleware 2017-03-24 23:34:17 +01:00
Alejandro Celaya
734dac9456 Migrated RedirectAction to psr-15 middleware 2017-03-24 23:24:11 +01:00
Alejandro Celaya
85ca366893 Migrated QrCodeAction to psr-15 middleware 2017-03-24 23:19:42 +01:00
Alejandro Celaya
46db736af8 Migrated PreviewAction to psr-15 middleware 2017-03-24 22:07:28 +01:00
Alejandro Celaya
7530048fbd Removed exception catch that used to return a 500, and now returns a 404 due to a behavior change 2017-03-24 21:59:45 +01:00
Alejandro Celaya
c3c03a3a3b Migrated LocaleMiddleware to psr-15 middleware 2017-03-24 21:49:31 +01:00
Alejandro Celaya
d1018b6da7 Fixed tests 2017-03-24 21:38:43 +01:00
Alejandro Celaya
fe7928ae0e Fixed JsonErrorHandler and prevented AuthorizationMiddleware to eat exceptions 2017-03-24 21:31:55 +01:00
Alejandro Celaya
f6c39285c9 Updated to expressive 2 and used new error handling system 2017-03-24 21:10:25 +01:00
Alejandro Celaya
0e2a289f9f Updated to phpunit 6 2017-03-24 20:34:18 +01:00
Alejandro Celaya
f7424da16b Merge branch 'develop' 2017-01-22 11:37:35 +01:00
Alejandro Celaya
2e10ee66b7 Merge pull request #87 from acelaya/feature/1.3.1
Feature/1.3.1
2017-01-22 11:36:04 +01:00
Alejandro Celaya
7e442671c3 Updated build script 2017-01-22 11:27:56 +01:00
Alejandro Celaya
ca646ec2b7 Updated changelog including v1.3.1 2017-01-22 11:17:44 +01:00
Alejandro Celaya
4df1af5fd8 Fixed searching short URLs list not querying tag names 2017-01-22 11:14:25 +01:00
Alejandro Celaya
b4548f3401 Added configs to enable fastroute cache 2017-01-22 11:07:18 +01:00
Alejandro Celaya
1819481710 Updated license year 2017-01-22 10:59:35 +01:00
Alejandro Celaya
e59ae654c0 Increased number of followed redirects to 15 2017-01-22 10:53:41 +01:00
Alejandro Celaya
a8bf699f2d Fixed error with non-registered service on latest expressive-twig-renderer version 2017-01-21 20:19:30 +01:00
Alejandro Celaya
de9d9d8667 Updated PathVersionMiddleware so that it is only applied to rest routes 2017-01-21 20:12:12 +01:00
Alejandro Celaya
869865f22a Added option to customize database hostname and port 2017-01-21 13:45:28 +01:00
Alejandro Celaya
29fd313337 Added memcached to php docker image 2017-01-21 13:33:51 +01:00
Alejandro Celaya
7781f07352 Added local entity manager config that allows db host name to be set 2017-01-21 09:16:00 +01:00
Alejandro Celaya
072371d459 Created docker-related files 2017-01-19 22:56:45 +01:00
Alejandro Celaya
51bf948458 Fixed schema definition on order by argument 2016-10-29 12:42:36 +02:00
Alejandro Celaya
48acded6ed Merge branch 'develop' 2016-10-23 10:30:36 +02:00
Alejandro Celaya
6821f5cf97 Merge branch 'develop' of https://github.com/shlinkio/shlink into develop 2016-10-23 10:30:11 +02:00
Alejandro Celaya
a15b17e08b Fixed regression bug while processing versionning for rest paths 2016-10-23 10:29:54 +02:00
Alejandro Celaya
275b17e1e8 Merge pull request #74 from acelaya/develop
Develop
2016-10-23 00:18:22 +02:00
Alejandro Celaya
27b08ff47b Merge branch 'develop' 2016-10-23 00:17:00 +02:00
Alejandro Celaya
bf7c760ca9 Updated changelog with last versions 2016-10-23 00:13:00 +02:00
Alejandro Celaya
8af9b0ee02 Tagged and summarized all endpoints in swagger docs 2016-10-23 00:07:31 +02:00
Alejandro Celaya
b225c03ef1 Improved swagger definition 2016-10-23 00:02:13 +02:00
Alejandro Celaya
8a12ed6b8c Separated swagger specification into multiple files 2016-10-22 23:44:14 +02:00
Alejandro Celaya
c3fd433446 Merge branch 'feature/67' into develop 2016-10-22 23:15:57 +02:00
Alejandro Celaya
0b9753582d Documented how to order results 2016-10-22 23:13:54 +02:00
Alejandro Celaya
9ac48bfbc5 Added support for ordering in shortcode:list command 2016-10-22 23:10:30 +02:00
Alejandro Celaya
85146e5676 Added support to order short URL lists 2016-10-22 23:02:12 +02:00
Alejandro Celaya
18ae541c93 Improved body parsing on BodyParserMiddleware 2016-10-22 22:17:04 +02:00
Alejandro Celaya
7a665ec26f Merge branch 'feature/72' into develop 2016-10-22 22:11:45 +02:00
Alejandro Celaya
e3cbac38ce Improved output on api-key:list command 2016-10-22 22:11:36 +02:00
Alejandro Celaya
31594d47b3 Created PathVersionMiddlewareTest 2016-10-22 18:52:40 +02:00
Alejandro Celaya
42f86a4a24 Added versioning to API endpoints, allowing not to pass the version which will default to v1 2016-10-22 18:46:53 +02:00
Alejandro Celaya
850ce152cd Merge branch 'feature/58' into develop 2016-10-22 13:16:13 +02:00
Alejandro Celaya
c22bbecdc5 Updated languages 2016-10-22 13:15:35 +02:00
Alejandro Celaya
230f2d155b Documented tags param in GET /short-codes endpoint 2016-10-22 13:13:50 +02:00
Alejandro Celaya
47a2c18c7e Added the ability to filter by tag in shotcodes:list command 2016-10-22 13:11:24 +02:00
Alejandro Celaya
52bb14bd66 Implemented filtering by tags in ListShortcodesAction 2016-10-22 13:04:17 +02:00
Alejandro Celaya
8b9caf02d2 Added tags param to paginable repository adapter 2016-10-22 12:57:24 +02:00
Alejandro Celaya
4580d11d32 Noticed in swagger docs that the searchTerm param is only available from v 1.3.0 of shlink 2016-10-22 12:50:35 +02:00
Alejandro Celaya
8610a158d4 Added searchTerm param to shortcode:list command 2016-10-22 12:48:24 +02:00
Alejandro Celaya
0a6030b35d Documented searchTerm query param for GET /short-codes endpoint 2016-10-22 12:43:22 +02:00
Alejandro Celaya
543c0e62d0 Added search term filtering to short codes list 2016-10-22 12:40:51 +02:00
Alejandro Celaya
4c76e17178 Changed swagger file format from yaml to json 2016-10-22 12:11:31 +02:00
Alejandro Celaya
611a314cdf Merge branch 'develop' 2016-08-29 13:07:25 +02:00
Alejandro Celaya
e7b4d24e5d Merge pull request #68 from acelaya/develop
Develop
2016-08-29 13:07:01 +02:00
Alejandro Celaya
cf60440288 Fixed possible PHP errors being missed while checking REST auth 2016-08-29 12:43:02 +02:00
Alejandro Celaya
15896045f3 Removed logic making visits to be returned for 2 days only if no start or end date were provided 2016-08-28 19:32:07 +02:00
Alejandro Celaya
a9f480ca99 Fixed error while checking an API key that doesn't exist 2016-08-28 09:46:11 +02:00
Alejandro Celaya
4bd67d5f98 Fixed cross domain middleware not exposing the Authorization header 2016-08-27 13:00:41 +02:00
Alejandro Celaya
924ba58f73 Added swagger documentation file 2016-08-26 11:51:51 +02:00
Alejandro Celaya
6eef694315 Merge branch 'develop' 2016-08-21 21:24:31 +02:00
Alejandro Celaya
fe4a4aef34 Updated changelog 2016-08-21 21:24:00 +02:00
Alejandro Celaya
b13c95cf1a Placed cross domain middleware as the first one for rest requests 2016-08-21 21:21:31 +02:00
Alejandro Celaya
c1c325588e Merge branch 'develop' 2016-08-21 19:16:36 +02:00
Alejandro Celaya
faad60f79e Merge branch 'develop' of https://github.com/shlinkio/shlink into develop 2016-08-21 19:16:21 +02:00
Alejandro Celaya
cbb5a02b95 Kept symlincs while generating the dist file 2016-08-21 19:16:06 +02:00
Alejandro Celaya
96faafd31e Merge branch 'develop' 2016-08-21 18:31:45 +02:00
Alejandro Celaya
7aa42ada54 Merge pull request #61 from acelaya/develop
Develop
2016-08-21 18:31:32 +02:00
Alejandro Celaya
3b1545a761 Added v1.2.0 to changelog 2016-08-21 18:26:28 +02:00
Alejandro Celaya
536309afb6 Merge pull request #4 from acelaya/feature/45
Feature/45
2016-08-21 18:20:56 +02:00
Alejandro Celaya
ad6ef22b72 Updated languages 2016-08-21 18:17:39 +02:00
Alejandro Celaya
4185015bef Improved tests 2016-08-21 18:15:25 +02:00
Alejandro Celaya
3376725152 Created EditTagsActiontest 2016-08-21 18:06:34 +02:00
Alejandro Celaya
d7b18776f1 Improved error response in edit tag request 2016-08-21 17:55:26 +02:00
Alejandro Celaya
1da285a63a Created action to set the togas for a short url 2016-08-21 16:52:26 +02:00
Alejandro Celaya
372488cbb4 Created middleware to parse PUT request bodies in rest requests 2016-08-21 13:52:15 +02:00
Alejandro Celaya
b6fee0ebaf Added option to set tags while creating short code from rest API 2016-08-21 13:07:12 +02:00
Alejandro Celaya
322180bde4 Added tag property to json serialization of ShortUrl 2016-08-21 12:48:31 +02:00
Alejandro Celaya
1cf6c93007 Added option to pass tags when creating a short code from the command line 2016-08-21 10:39:00 +02:00
Alejandro Celaya
2b89556c09 Allowed to display tags in the shortcode:list command 2016-08-21 10:17:17 +02:00
Alejandro Celaya
e3021120e3 Checked tables do not exist before creating them 2016-08-21 09:45:36 +02:00
Alejandro Celaya
2ca7ab4ccf Created new entity Tag and migration to create new tables 2016-08-21 09:41:21 +02:00
Alejandro Celaya
a2c2bc166c Removed wrong spaces 2016-08-20 19:11:20 +02:00
Alejandro Celaya
5f0baab2ac Created script to update an already existing system 2016-08-19 15:50:08 +02:00
Alejandro Celaya
ea083e30b6 Improved InstallCommand, adding migrations and removing code duplication 2016-08-19 15:15:53 +02:00
Alejandro Celaya
7d49c1760c Added doctrine migrations and remove platform specific code from entities 2016-08-19 14:51:34 +02:00
Alejandro Celaya
7c42835cc1 Improved error management on install command 2016-08-18 18:02:24 +02:00
Alejandro Celaya
f77273ef93 Merge pull request #3 from acelaya/feature/7
Feature/7
2016-08-18 17:30:57 +02:00
Alejandro Celaya
e52983c5d9 Updated translations 2016-08-18 17:28:04 +02:00
Alejandro Celaya
4615bbaaf7 CreatedPreviewActionTest 2016-08-18 17:23:40 +02:00
Alejandro Celaya
b432ed2c1d Created GeneratePreviewCommandTest 2016-08-18 17:10:40 +02:00
Alejandro Celaya
34174b2fbe Added tests for Image classes 2016-08-18 16:37:50 +02:00
Alejandro Celaya
fac519699a Used filesystem check instead of cache check for preview generation 2016-08-18 13:20:57 +02:00
Alejandro Celaya
2c91ded514 Improved PreviewGenerator by composing an ImageBuilder that creates new Image objects fore each URL 2016-08-18 12:21:26 +02:00
Alejandro Celaya
15247e832e Improved QR code generation route 2016-08-18 11:31:04 +02:00
Alejandro Celaya
60c68c914b Managed error while generating URL previews by throwing an exception 2016-08-18 11:17:17 +02:00
Alejandro Celaya
277406c3b8 Created action to return preview images 2016-08-18 11:10:15 +02:00
Alejandro Celaya
26adf48b48 Added wkhtmltopdf stuff and created preview generator service 2016-08-18 10:19:33 +02:00
Alejandro Celaya
20e43aac90 Added bin dir to code sniffer checks 2016-08-17 11:47:08 +02:00
Alejandro Celaya
56c9abcfd0 Fixed build script to generate the distributable file just with project files from the project root 2016-08-16 11:51:05 +02:00
Alejandro Celaya
5ca4bc928d Created specific factory for AppOptions to prevent circular dependency with cache 2016-08-15 23:40:49 +02:00
Alejandro Celaya
ffa6c0d2ca Merge branch 'feature/install' into develop 2016-08-15 11:34:48 +02:00
Alejandro Celaya
395311eaad Added InstallCommandTest 2016-08-15 11:34:35 +02:00
Alejandro Celaya
5bbc7de4af Fixed namespace in some tests 2016-08-15 11:01:55 +02:00
Alejandro Celaya
d5516c7269 Finished build script to compress dist project file 2016-08-15 10:18:13 +02:00
Alejandro Celaya
f852f9d398 Created htaccess 2016-08-15 10:03:39 +02:00
Alejandro Celaya
0d9f964687 Fixed bug on build script 2016-08-15 10:03:17 +02:00
Alejandro Celaya
00c56ca594 Fixed tests 2016-08-15 09:52:44 +02:00
Alejandro Celaya
9e4fb68265 Created build script 2016-08-15 09:45:09 +02:00
Alejandro Celaya
804d99ebf7 Created CLI scripts for Windows OS 2016-08-15 09:21:14 +02:00
Alejandro Celaya
4bbdccf981 Added symfony process to run initialization commands 2016-08-14 23:41:42 +02:00
Alejandro Celaya
1f3e31d100 Fixed working directory and paths in InstallCommand 2016-08-14 23:25:04 +02:00
Alejandro Celaya
2617ef1547 Added cli locale to installation generated config file 2016-08-14 23:10:07 +02:00
Alejandro Celaya
25380e4727 Moved console Application creation to factory 2016-08-14 23:08:26 +02:00
Alejandro Celaya
12322b7368 Improved config loading by using only one config provider object 2016-08-14 22:56:22 +02:00
Alejandro Celaya
a608f7d0f4 Minor name change 2016-08-14 21:28:21 +02:00
Alejandro Celaya
9a42d70604 Added custom config params to merged confg 2016-08-14 18:15:10 +02:00
Alejandro Celaya
fe708333b1 Fixed path while generating config file 2016-08-14 10:43:16 +02:00
Alejandro Celaya
566ee7ef6f Finished custom config command 2016-08-14 10:30:43 +02:00
Alejandro Celaya
56af58fcb8 Created installation script and installation command 2016-08-14 09:11:46 +02:00
Alejandro Celaya
cffa43a155 Created installation script and installation command 2016-08-14 09:09:23 +02:00
Alejandro Celaya
2b2c0b7c13 Merge branch 'feature/29' into develop 2016-08-12 18:01:38 +02:00
Alejandro Celaya
faa8019fc5 Redefined logger services so that the error handler uses Shlink's logger 2016-08-12 18:01:09 +02:00
Alejandro Celaya
a8ea458649 Fixed distributable local config file 2016-08-12 17:55:07 +02:00
Alejandro Celaya
8f4d305982 Added ErrorHandler package dependency and remove local files 2016-08-12 17:54:32 +02:00
Alejandro Celaya
065cddc4b1 Merge branch 'develop' 2016-08-09 19:05:39 +02:00
Alejandro Celaya
3a3a16f46f Added changelog for v1.1.0 2016-08-09 19:05:13 +02:00
Alejandro Celaya
ba5bd6d98c Merge branch 'develop' 2016-08-09 19:00:42 +02:00
Alejandro Celaya
a1aa9c2031 Merge pull request #49 from acelaya/develop
v1.1.0
2016-08-09 19:00:22 +02:00
Alejandro Celaya
43c6b56e42 Fixed memcached test while comparing servers 2016-08-09 18:56:01 +02:00
Alejandro Celaya
39d2f5a38f Created travis config file to enable memcached extension 2016-08-09 18:50:14 +02:00
Alejandro Celaya
69cc30bce7 Allowed failures on PHP 7.1 environments 2016-08-09 18:38:48 +02:00
Alejandro Celaya
5913550eec Fixed build when memcached is not enabled in PHP 7.1 2016-08-09 18:29:47 +02:00
Alejandro Celaya
3140ab2ad7 Updated shlink website link to use https 2016-08-09 18:17:28 +02:00
Alejandro Celaya
090479fa62 Improved CacheFactory supporting more adapters 2016-08-09 17:58:47 +02:00
Alejandro Celaya
9ee2064ba1 Merge pull request #2 from acelaya/feature/46
Feature/46
2016-08-09 14:21:43 +02:00
Alejandro Celaya
90cef7d4d9 Removed unused import 2016-08-09 14:19:46 +02:00
Alejandro Celaya
12410e82d8 Created tests for QrCode middlewares 2016-08-09 14:18:20 +02:00
Alejandro Celaya
18084433c7 Created middleware to cache generated QR codes 2016-08-09 13:41:30 +02:00
Alejandro Celaya
8eb279fd28 Updated UrlShortener to namespace the cache entries 2016-08-09 13:32:33 +02:00
Alejandro Celaya
99b7c77997 Created action to generate QR codes 2016-08-09 10:25:30 +02:00
Alejandro Celaya
166c94cac7 Merge branch 'feature/40' into develop 2016-08-09 09:13:48 +02:00
Alejandro Celaya
7c5d8cf244 Fixed VisitsTracker to take into account the X-Forwarded-For header in case the server is behind a load balabncer or proxy 2016-08-09 09:13:39 +02:00
Alejandro Celaya
73a236b3d0 Updated VisitsTracker so that the track method expects a Request object to be provided 2016-08-09 08:52:06 +02:00
Alejandro Celaya
34753ca7d3 Added logger to classes that catch errors in order to log them 2016-08-08 12:33:58 +02:00
Alejandro Celaya
fff058f44b Created LoggerFactoryTest 2016-08-08 12:07:04 +02:00
Alejandro Celaya
b7f3c332e4 Created Logger factory and logger config, and added logger dependencies 2016-08-08 11:56:19 +02:00
Alejandro Celaya
cff9b7c0b5 Deleted docs which are now in Shlink's website 2016-08-08 11:17:14 +02:00
Alejandro Celaya
63e867cf4b Updated vendor name on error pages 2016-08-08 10:08:34 +02:00
Alejandro Celaya
f49e9064cd Added cache adapter to the UrlShortener service to cache shortcode-url maps 2016-08-08 10:02:52 +02:00
Alejandro Celaya
3bd4f506e0 Updated status returned in REST endpoints to be 404 when something is not found 2016-08-08 09:46:40 +02:00
Alejandro Celaya
93713689d7 Merge branch 'feature/35' into develop 2016-08-08 09:39:15 +02:00
Alejandro Celaya
ecd2e6e759 Updated namespace for Visit CLI commands 2016-08-08 09:38:50 +02:00
Alejandro Celaya
a65003803b Updated namespace for Shortcode CLI commands 2016-08-08 09:36:52 +02:00
Alejandro Celaya
0a4f8c3b0a Merge pull request #1 from acelaya/feature/13
Feature/13
2016-08-07 21:15:59 +02:00
Alejandro Celaya
80d8c32881 Removed rest auth env vars 2016-08-07 20:47:43 +02:00
Alejandro Celaya
57bc681b9e Created command to generate a random secret key string 2016-08-07 20:30:19 +02:00
Alejandro Celaya
2a089f05b1 Updated languages 2016-08-07 20:21:38 +02:00
Alejandro Celaya
258f954a38 Deleted rest token related classes 2016-08-07 19:57:23 +02:00
Alejandro Celaya
7b0beb3b8c Updated CheckAuthenticationMiddleware to work with JWT and the Authorization header 2016-08-07 19:53:14 +02:00
Alejandro Celaya
9573e9f4ef Updated AuthenticateAction to generate and return a JWT 2016-08-07 19:13:40 +02:00
Alejandro Celaya
a60080b1ce Created JWTService and related classes 2016-08-07 14:44:33 +02:00
Alejandro Celaya
1d92e87d50 Updated AuthenticateAction to use the APiKeyService instead of the RestTokenService 2016-08-07 10:26:34 +02:00
Alejandro Celaya
289db45f27 Created ListKeysCommand 2016-08-06 18:50:50 +02:00
Alejandro Celaya
c5382b2a7f Created DisableKeyCommand 2016-08-06 18:26:07 +02:00
Alejandro Celaya
dd1bc49b79 Added method to ApiKeyService to list api keys 2016-08-06 18:08:09 +02:00
Alejandro Celaya
74777c2234 Created command to generate a new api key 2016-08-06 18:07:48 +02:00
Alejandro Celaya
99d7e6dd7d Fixed AuthenticateAction not working with only one group of params 2016-08-06 13:24:06 +02:00
Alejandro Celaya
7b746f76b0 Created APiKeyService and tests 2016-08-06 13:18:27 +02:00
Alejandro Celaya
2767a14101 Created ApiKey entity 2016-08-06 12:50:44 +02:00
Alejandro Celaya
270dbc6028 Created new entity_manager configuration, dropping old database first level config key 2016-08-06 12:40:31 +02:00
Alejandro Celaya
7b1b00901a Created phpstorm meta fle to get ContainerInterop typehint based on service name 2016-08-05 07:20:40 +02:00
Alejandro Celaya
9ab4b9ab43 Merge branch 'develop' 2016-08-01 21:30:49 +02:00
Alejandro Celaya
5e493b435a Added badges to readme file 2016-08-01 21:26:47 +02:00
Alejandro Celaya
21dc9ac5a0 Updated changelog for version 1 2016-08-01 21:25:15 +02:00
Alejandro Celaya
40a3ba6203 Improved rest documentation 2016-08-01 21:15:30 +02:00
Alejandro Celaya
307dfc64b4 Created installation steps doc 2016-08-01 21:13:17 +02:00
Alejandro Celaya
9b9b1415fe Created GenerateCharsetCommand 2016-08-01 21:11:42 +02:00
Alejandro Celaya
ce2b28a0b4 Improved cli entry point so that the language is set to the translator based on the CLI_LANGUAGE env var 2016-08-01 20:44:45 +02:00
Alejandro Celaya
7b98527f2e Improved CacheFactory so that adapter can be set in config 2016-08-01 20:16:13 +02:00
Alejandro Celaya
30988b10d1 Added Laravel's env helper 2016-08-01 14:36:43 +02:00
Alejandro Celaya
d73d3049b7 Removed dependency on expressive-helpers package 2016-07-31 16:42:19 +02:00
Alejandro Celaya
7b1e855e7f Renamed services config files to dependencies 2016-07-31 16:32:27 +02:00
Alejandro Celaya
a957f66ed0 Renamed services first level config key to dependencies 2016-07-31 16:30:05 +02:00
Alejandro Celaya
ce00874dd1 Merge branch 'feature/12' into develop 2016-07-31 16:25:11 +02:00
Alejandro Celaya
3d5e5d5df9 Created ResolveUrlActionTest 2016-07-31 16:24:00 +02:00
Alejandro Celaya
08f6d2de78 Created ListShortcodesActionTest 2016-07-31 16:16:26 +02:00
Alejandro Celaya
c6b7515285 Created GetVisitsActionTest 2016-07-31 16:10:16 +02:00
Alejandro Celaya
04e0a192ad Created CreateShortcodeActionTest 2016-07-31 15:58:18 +02:00
Alejandro Celaya
878518ced7 Created AuthenticateActionTest 2016-07-31 13:33:55 +02:00
Alejandro Celaya
6f7e4f7e7f Created RestUtilsTest 2016-07-31 13:18:36 +02:00
Alejandro Celaya
f701e65f75 Created RestTokenServiceTest 2016-07-31 13:14:06 +02:00
Alejandro Celaya
ef6f4fba66 Improved CheckAuthenticationMiddlewareTest 2016-07-31 13:01:08 +02:00
Alejandro Celaya
f904f79c18 Created CheckAuthenticationMiddlewareTest 2016-07-30 23:26:49 +02:00
Alejandro Celaya
41939b790d Added more Rest module tests 2016-07-30 23:17:13 +02:00
Alejandro Celaya
8c446f0f3b Created Core\ConfigProviderTest 2016-07-30 23:07:44 +02:00
Alejandro Celaya
9c6420fe26 Created VisitServiceTest 2016-07-30 23:01:07 +02:00
Alejandro Celaya
ce4877d4ac Improved VisitsTrackerTest 2016-07-30 22:55:28 +02:00
Alejandro Celaya
ebeaa3c64a Created RedirectActionTest 2016-07-30 20:02:48 +02:00
Alejandro Celaya
fcdcfde04f Added missing tests for Common module 2016-07-30 17:45:48 +02:00
Alejandro Celaya
2ce6c1f44b Added more tests to Common module 2016-07-30 17:17:21 +02:00
Alejandro Celaya
00db8a7ea5 Created Common\ErrorHandler tests 2016-07-30 17:07:35 +02:00
Alejandro Celaya
4c6cc9cd11 Created ResolveUrlCommandTest 2016-07-30 16:48:02 +02:00
Alejandro Celaya
50f1549457 Created ProcessVisitsCommand 2016-07-30 14:42:09 +02:00
Alejandro Celaya
3923bf0604 Created GetVisitsCommandTest 2016-07-30 14:30:30 +02:00
Alejandro Celaya
bb8404040a Created GenerateShortcodeCommandTest 2016-07-30 14:13:28 +02:00
Alejandro Celaya
5cdd782ce1 Removed whiteline 2016-07-30 13:54:00 +02:00
Alejandro Celaya
e345c2bbfe Moved error handler classes from Expressive namespace to ErrorHandler namespace 2016-07-30 13:51:52 +02:00
Alejandro Celaya
2a018f5415 Fixed ContentBasedErrorHandler fatching error handlers from the composed plugin manager 2016-07-30 10:47:29 +02:00
Alejandro Celaya
7394424a43 Merge branch 'develop' into feature/12 2016-07-30 10:41:05 +02:00
Alejandro Celaya
2f5119d0b3 Split ContentBasedErrorHandler responsibilities into two separated classes 2016-07-30 10:08:34 +02:00
Alejandro Celaya
c569cef239 Fixed ContentBased error handler not using the default content if accepted contents are not valid 2016-07-29 11:25:53 +02:00
Alejandro Celaya
ab6aa99a6d Created more tests on CLI module 2016-07-28 20:49:27 +02:00
Alejandro Celaya
f4532c3015 Replaced exclusive ifs by if-else 2016-07-28 09:56:44 +02:00
Alejandro Celaya
af9193f721 Removed duplicated error handling for 404 errors 2016-07-28 09:40:36 +02:00
Alejandro Celaya
36259588db Fixed Action prefix on routable middlewares 2016-07-27 20:22:50 +02:00
Alejandro Celaya
a3554db1c3 Merge branch 'feature/15' into develop 2016-07-27 20:18:28 +02:00
Alejandro Celaya
75e744838c Created content based error handler which allows managing errors in a different way depending on the Accepted content type from the client 2016-07-27 20:17:23 +02:00
Alejandro Celaya
f3d2cf5e15 Deleted ResponseTypeMiddleware which is not ussable anymore 2016-07-26 19:10:43 +02:00
Alejandro Celaya
a81dba58bd Defined custom NotFoundMiddleware for rest routes 2016-07-26 19:10:01 +02:00
Alejandro Celaya
83f29080c6 Improved the way rest errors are catched 2016-07-26 11:05:17 +02:00
Alejandro Celaya
0ef1e416c6 Created middleware to catch rest errors and return JSON responses 2016-07-26 09:54:13 +02:00
Alejandro Celaya
57d81115de Added PHP 7.1 to the CI matrix 2016-07-21 18:46:52 +02:00
Alejandro Celaya
5c8353da02 Set collation to utf8_bin in shortCode column of ShortUrl so that the UNIQUE key is case sensitive 2016-07-21 18:46:15 +02:00
Alejandro Celaya
95346ebb7e Merge branch 'feature/23' into develop 2016-07-21 17:00:42 +02:00
Alejandro Celaya
cd5bbcd60a Reused middleware to check Accept-Language header on any HTTP related middleware 2016-07-21 16:59:27 +02:00
Alejandro Celaya
fb9c7f8eec Used twig extension to load translations on twig templates 2016-07-21 16:54:00 +02:00
Alejandro Celaya
e42469b090 Added translations for error messages returned by the REST API 2016-07-21 16:41:16 +02:00
Alejandro Celaya
06868f782b Created middleware for rest that reads the language from the Accept-Language header 2016-07-21 16:20:27 +02:00
Alejandro Celaya
73a35a8f44 Added translator and translations to ResolveUrlCommand 2016-07-21 16:01:16 +02:00
Alejandro Celaya
6a05265a48 Added translator and translations to ProcessVisitsCommand 2016-07-21 15:50:27 +02:00
Alejandro Celaya
8e51b51cae Added translator and translations to ListShortcodesCommand 2016-07-21 15:43:19 +02:00
Alejandro Celaya
545fe7da70 Added translator and translations to GetVisitsCommand 2016-07-21 15:35:52 +02:00
Alejandro Celaya
cb99130c1e Created translator and used inside one of the commands 2016-07-21 15:08:46 +02:00
Alejandro Celaya
b413b16c86 Merge branch 'feature/21' into develop 2016-07-21 10:16:52 +02:00
Alejandro Celaya
3ba51c5390 Improved visits REST endpoint path 2016-07-21 10:16:36 +02:00
Alejandro Celaya
0ef9db0bdf Added option to filter by date in visits REST endpoint 2016-07-21 10:13:09 +02:00
Alejandro Celaya
bdd2d6f8b2 Improved DateRange to check if both wrapped dates are empty 2016-07-21 10:03:37 +02:00
Alejandro Celaya
45d194aced Added option to filter by date in shortcode:views CLI command 2016-07-21 09:58:33 +02:00
Alejandro Celaya
0a57f52309 Fixed typo 2016-07-21 09:46:12 +02:00
Alejandro Celaya
d97287ab0c Added option to filter by date the visits list 2016-07-21 09:36:38 +02:00
Alejandro Celaya
84c4021b24 Merge branch 'feature/22' into develop 2016-07-20 19:04:48 +02:00
Alejandro Celaya
d39a949992 Fixed wrong exception name 2016-07-20 19:04:38 +02:00
Alejandro Celaya
dbe1281d2a Created services and command to process visits 2016-07-20 19:00:23 +02:00
Alejandro Celaya
d3c2f4ed2a Created service to resolve IP locations 2016-07-20 12:37:48 +02:00
Alejandro Celaya
06fa33877b Moved some exceptions from core to common 2016-07-20 10:13:53 +02:00
Alejandro Celaya
c290bed354 Created VisitLocation entity 2016-07-20 09:35:46 +02:00
Alejandro Celaya
31af0eea04 Improved main config file and fixed tests whitelist 2016-07-19 23:35:47 +02:00
Alejandro Celaya
aaf4f1dfe5 Improved config loading so that autoloaded overrides module-specific 2016-07-19 23:30:23 +02:00
Alejandro Celaya
e28e984278 Improved CrossDomainMiddleware by allowing the same origin that was requested 2016-07-19 22:38:14 +02:00
Alejandro Celaya
839329d627 Improved CrossDomainMiddleware preventing headers to be injected on non-CORS requests 2016-07-19 20:20:18 +02:00
Alejandro Celaya
0daa24739d Merge branch 'feature/11' into develop 2016-07-19 18:34:02 +02:00
Alejandro Celaya
7ca52ecff9 Removed wrong use statement from old namespace 2016-07-19 18:33:45 +02:00
Alejandro Celaya
39598d8608 Moved templates and templates config to Core module 2016-07-19 18:32:59 +02:00
Alejandro Celaya
923abdf4d2 Added first tests for CLI module 2016-07-19 18:28:21 +02:00
Alejandro Celaya
f917697b8e Added first tests to Rest module 2016-07-19 18:19:05 +02:00
Alejandro Celaya
fe8ef2030f Fixed elements broken on module separation 2016-07-19 18:05:06 +02:00
Alejandro Celaya
ab9c2f728a Created Core module 2016-07-19 18:01:39 +02:00
Alejandro Celaya
ba06ad44bd Created Common module 2016-07-19 17:38:41 +02:00
Alejandro Celaya
8fc88171ee Moved AuthenticationException to Rest module 2016-07-19 17:27:55 +02:00
Alejandro Celaya
7efb3b3a86 Created cli-specific services config file 2016-07-19 17:17:37 +02:00
Alejandro Celaya
170c96b8ef Fixed config files names 2016-07-19 17:12:50 +02:00
Alejandro Celaya
55f954f50f Created Rest module 2016-07-19 17:07:59 +02:00
Alejandro Celaya
95d0beea3c Created CLI module 2016-07-19 16:50:02 +02:00
Alejandro Celaya
5eefaf3071 Added config manager package 2016-07-19 16:30:48 +02:00
Alejandro Celaya
5536d05e99 Added idea folder to gitignore 2016-07-19 16:14:57 +02:00
Alejandro Celaya
a229d2903b Merge branch 'develop' 2016-07-06 20:33:42 +02:00
Alejandro Celaya
e3ad9eb03a Merge pull request #17 from acelaya/feature/10
feature/10
2016-07-06 20:33:04 +02:00
Alejandro Celaya
cdeffe9cc7 Added missing issue to changelog 2016-07-06 20:23:38 +02:00
Alejandro Celaya
2e00a8dec6 Created command to list visits for a shortcode 2016-07-06 20:21:34 +02:00
Alejandro Celaya
43f1f790dd Improved ListShortciodesCommand 2016-07-06 20:10:19 +02:00
Alejandro Celaya
60f5e5290e Created new command to list short urls 2016-07-06 19:41:24 +02:00
Alejandro Celaya
96478f3400 Created the console application via a factory 2016-07-05 23:25:39 +02:00
Alejandro Celaya
9ce5e255f1 Created new CLI command to parse a shortcode 2016-07-05 23:16:23 +02:00
Alejandro Celaya
490b72539e Created CHANGELOG 2016-07-05 20:09:45 +02:00
Alejandro Celaya
86d877b565 Merge pull request #16 from acelaya/feature/8
Feature/8
2016-07-05 20:05:02 +02:00
Alejandro Celaya
371e264ebe Removed PHP5.5 from travis environments 2016-07-05 19:54:16 +02:00
Alejandro Celaya
f691bb00d1 Created rest documentation 2016-07-05 19:28:47 +02:00
Alejandro Celaya
baf5936cf1 More cross-domain improvements 2016-07-05 19:19:23 +02:00
Alejandro Celaya
bd36c65a73 Fixed some cross-origin issues 2016-07-05 19:08:34 +02:00
Alejandro Celaya
431169eb8c Created middleware that checks authentication 2016-07-04 17:54:24 +02:00
Alejandro Celaya
dfc5bfd0f2 Created rest route to perform authentication 2016-07-04 14:45:18 +02:00
Alejandro Celaya
56b2bd3d56 Created entity to persist rest tokens 2016-07-04 14:04:19 +02:00
Alejandro Celaya
bbef3444c2 Added errorhanler local config distributable file 2016-07-04 13:14:01 +02:00
Alejandro Celaya
b4e6fe7135 Created trait to serialize paginators 2016-07-04 12:50:06 +02:00
Alejandro Celaya
30773c66d0 Fixed ShortUrlServiceTest 2016-07-04 09:18:10 +02:00
Alejandro Celaya
cc1829f9ed Added pagination to ShortUrls list 2016-07-04 09:15:50 +02:00
Alejandro Celaya
35f1a4b672 Created stuff to handle pagination on list results 2016-07-04 08:57:37 +02:00
Alejandro Celaya
67ef171262 Improved middleware pipeline and added cross-domain headers for ajax requests 2016-07-04 08:33:31 +02:00
Alejandro Celaya
4129d35447 Added list short URLs endpoint to rest api 2016-07-04 08:33:31 +02:00
Alejandro Celaya
305df3a95b Implemented rest endpoint to return shortcode visits 2016-07-04 08:33:31 +02:00
Alejandro Celaya
ab8ccd7df1 Added get URL rest endpoint 2016-07-04 08:33:31 +02:00
Alejandro Celaya
1fbefbbd15 Created shortcode creation rest endpoint 2016-07-04 08:33:31 +02:00
Alejandro Celaya
8a7d5a499e Dropped unused middleware 2016-07-04 08:33:22 +02:00
Alejandro Celaya
32d0e2d091 Merge branch 'feature/9' into develop 2016-07-03 09:17:33 +02:00
Alejandro Celaya
e09915dd21 Removed cli-related middlewares and factories 2016-07-03 09:16:56 +02:00
Alejandro Celaya
35147fecb2 Replaced cli execution using expressive middleware by symfony/console 2016-07-03 09:14:27 +02:00
Alejandro Celaya
1c6250618a Removed unique index from too long field 2016-05-02 17:52:53 +02:00
323 changed files with 16592 additions and 1153 deletions

View File

@@ -1,9 +1,14 @@
# Application
APP_ENV=
SECRET_KEY=
SHORTENED_URL_SCHEMA=
SHORTENED_URL_HOSTNAME=
SHORTCODE_CHARS=
# Language
DEFAULT_LOCALE=
CLI_LOCALE=
# Database
DB_USER=
DB_PASSWORD=

25
.gitattributes vendored Normal file
View File

@@ -0,0 +1,25 @@
/data/infra export-ignore
/docs export-ignore
/module/CLI/test export-ignore
/module/CLI/test-resources export-ignore
/module/Common/test export-ignore
/module/Common/test-func export-ignore
/module/Core/test export-ignore
/module/Core/test-func export-ignore
/module/Rest/test export-ignore
.env.dist export-ignore
.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
docker-compose.override.yml.dist export-ignore
docker-compose.yml export-ignore
func_tests_bootstrap.php export-ignore
indocker export-ignore
phpcs.xml export-ignore
phpunit.xml.dist export-ignore
phpunit-func.xml export-ignore
phpstan.neon

4
.gitignore vendored
View File

@@ -1,4 +1,8 @@
.idea
build
composer.lock
vendor/
.env
data/database.sqlite
docs/swagger-ui
docker-compose.override.yml

23
.phpstorm.meta.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace PHPSTORM_META;
use Psr\Container\ContainerInterface;
use Zend\ServiceManager\ServiceManager;
/**
* PhpStorm Container Interop code completion
*
* Add code completion for container-interop.
*
* \App\ClassName::class will automatically resolve to it's own name.
*
* Custom strings like ``"cache"`` or ``"logger"`` need to be added manually.
*/
$STATIC_METHOD_TYPES = [
ContainerInterface::get('') => [
'' == '@',
],
ServiceManager::build('') => [
'' == '@',
],
];

View File

@@ -2,24 +2,29 @@ language: php
branches:
only:
- master
- develop
- /.*/
php:
- 5.5
- 5.6
- 7
- hhvm
- 7.1
- 7.2
before_install:
- phpenv config-add data/infra/travis-php/memcached.ini
- phpenv config-add data/infra/travis-php/apcu.ini
before_script:
- composer self-update
- composer install --no-interaction
- if [[ $TRAVIS_PHP_VERSION = 7.1 ]] || [[ $TRAVIS_PHP_VERSION = 7.2 ]]; then composer global require --dev phpstan/phpstan:0.9.*; fi
script:
- mkdir build
- composer check
- if [[ $TRAVIS_PHP_VERSION = 7.1 ]] || [[ $TRAVIS_PHP_VERSION = 7.2 ]]; then ~/.composer/vendor/bin/phpstan analyse module/*/src/ --level=6 -c phpstan.neon; fi
after_script:
- vendor/bin/phpcov 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

227
CHANGELOG.md Normal file
View File

@@ -0,0 +1,227 @@
## CHANGELOG
### 1.7.2
**Bugs:**
* [135: Fix PathVersionMiddleware being ignored when using expressive 2.2](https://github.com/shlinkio/shlink/issues/135)
### 1.7.1
**Enhancements:**
* [128: Upgrade to expressive 2.2](https://github.com/shlinkio/shlink/issues/128)
**Bugs**
* [126: Expressive 2.2 causes failures by triggering E_USER_DEPRECATED errors](https://github.com/shlinkio/shlink/issues/126)
### 1.7.0
**Features**
* [88: Allow to disable tracking of the short URL by including a configurable query param](https://github.com/shlinkio/shlink/issues/88)
* [108: Allow to edit metadata in created shortcodes](https://github.com/shlinkio/shlink/issues/108)
**Enhancements:**
* [113: Update CLI commands to use SymfonyStyle](https://github.com/shlinkio/shlink/issues/113)
* [112: Configure cli commands lazy loading](https://github.com/shlinkio/shlink/issues/112)
**Tasks**
* [117: Make every module which throws exceptions have its own ExceptionInterface, and make them all extend Throwable](https://github.com/shlinkio/shlink/issues/117)
* [115: Add phpstan to build matrix on PHP >=7.1 envs](https://github.com/shlinkio/shlink/issues/115)
* [114: Replace vlucas/phpdotenv dev requirement by symfony/env](https://github.com/shlinkio/shlink/issues/114)
### 1.6.2
**Bugs**
* [109: Fix installation error due to typo in latest migration](https://github.com/shlinkio/shlink/issues/109)
### 1.6.1
**Tasks**
* [110: Create gitattributes file to define files to be excluded from distributable package](https://github.com/shlinkio/shlink/issues/110)
### 1.6.0
**Features**
* [44: Consider allowing to set custom slugs instead of generating a short code](https://github.com/shlinkio/shlink/issues/44)
* [47: Allow to limit short codes availability by date range](https://github.com/shlinkio/shlink/issues/47)
* [48: Allow to limit the number of visits to a short code](https://github.com/shlinkio/shlink/issues/48)
* [105: Added option to enable/disable URL validation by response status code.](https://github.com/shlinkio/shlink/pull/105)
**Enhancements:**
* [27: Add repository functional tests with dbunit](https://github.com/shlinkio/shlink/issues/27)
* [86: Drop support for PHP 5](https://github.com/shlinkio/shlink/issues/86)
* [101: Make actions just capture very specific exceptions, and let the ErrorHandler catch any other exception](https://github.com/shlinkio/shlink/issues/101)
* [104: Use different templates for requested-short-code-does-not-exist and route-could-not-be-match](https://github.com/shlinkio/shlink/issues/104)
**Tasks**
* [99: Replace AnnotatedFactory by ConfigAbstractFactory](https://github.com/shlinkio/shlink/issues/99)
* [100: Replace twig by plates](https://github.com/shlinkio/shlink/issues/100)
* [102: Improve coding standards strictness](https://github.com/shlinkio/shlink/issues/102)
**Bugs**
* [103: Make NotFoundDelegate return proper content types based on accepted content](https://github.com/shlinkio/shlink/issues/103)
### 1.5.0
**Enhancements:**
* [95: Add tags CRUD to CLI](https://github.com/shlinkio/shlink/issues/95)
* [59: Add tags CRUD to REST](https://github.com/shlinkio/shlink/issues/59)
* [66: Allow to import certain information from older app directory when updating](https://github.com/shlinkio/shlink/issues/66)
**Tasks**
* [96: Add namespace to functions](https://github.com/shlinkio/shlink/issues/96)
* [76: Add response examples to swagger docs](https://github.com/shlinkio/shlink/issues/76)
* [93: Improve cross domain management by using the ImplicitOptionsMiddleware](https://github.com/shlinkio/shlink/issues/93)
**Bugs**
* [92: Fix formatted dates, using an ISO compliant format](https://github.com/shlinkio/shlink/issues/92)
### 1.4.0
**Enhancements:**
* [89: Update to expressive 2](https://github.com/shlinkio/shlink/issues/89)
### 1.3.1
**Tasks**
* [82: Enable FastRoute routes cache](https://github.com/shlinkio/shlink/issues/82)
* [85: Update year in license file](https://github.com/shlinkio/shlink/issues/85)
* [81: Add docker containers config](https://github.com/shlinkio/shlink/issues/81)
**Bugs**
* [83: Short codes list: search in tags when filtering by query string](https://github.com/shlinkio/shlink/issues/83)
* [79: Increase the number of followed redirects](https://github.com/shlinkio/shlink/issues/79)
* [75: Apply PathVersionMiddleware only to rest routes defining it by configuration instead of code](https://github.com/shlinkio/shlink/issues/75)
* [77: Allow defining database server hostname and port](https://github.com/shlinkio/shlink/issues/77)
### 1.3.0
**Enhancements:**
* [67: Allow to order the short codes list](https://github.com/shlinkio/shlink/issues/67)
* [60: Accept JSON requests in REST and use a body parser middleware to set the parsedBody](https://github.com/shlinkio/shlink/issues/60)
* [72: When listing API keys from CLI, display in yellow color enabled keys that have expired](https://github.com/shlinkio/shlink/issues/72)
* [58: Allow to filter short URLs by tag](https://github.com/shlinkio/shlink/issues/58)
* [69: Allow to filter short codes by text query](https://github.com/shlinkio/shlink/issues/69)
**Tasks**
* [73: Tag endpoints in swagger file](https://github.com/shlinkio/shlink/issues/73)
* [71: Separate swagger docs into multiple files](https://github.com/shlinkio/shlink/issues/71)
* [63: Add path versioning to REST API routes](https://github.com/shlinkio/shlink/issues/63)
### 1.2.2
**Bugs**
* Fixed minor bugs on CORS requests
### 1.2.1
**Bugs**
* [62: Fix cross-domain requests in REST API](https://github.com/shlinkio/shlink/issues/62)
### 1.2.0
**Features**
* [45: Allow to define tags on short codes, to improve filtering and classification](https://github.com/shlinkio/shlink/issues/45)
* [7: Add website previews while listing available URLs](https://github.com/shlinkio/shlink/issues/7)
**Enhancements:**
* [57: Add database migrations system to improve updating between versions](https://github.com/shlinkio/shlink/issues/57)
* [31: Add support for other database management systems by improving the EntityManager factory](https://github.com/shlinkio/shlink/issues/31)
* [51: Generate build process to paquetize the app and ease distribution](https://github.com/shlinkio/shlink/issues/51)
* [38: Define installation script. It will request dynamic data on the fly so that there is no need to define env vars](https://github.com/shlinkio/shlink/issues/38)
**Tasks**
* [55: Create update script which does not try to create a new database](https://github.com/shlinkio/shlink/issues/55)
* [54: Add cache namespace to prevent name collisions with other apps in the same environment](https://github.com/shlinkio/shlink/issues/54)
* [29: Use the acelaya/ze-content-based-error-handler package instead of custom error handler implementation](https://github.com/shlinkio/shlink/issues/29)
**Bugs**
* [53: Fix entities database interoperability](https://github.com/shlinkio/shlink/issues/53)
* [52: Add missing htaccess file for apache environments](https://github.com/shlinkio/shlink/issues/52)
### 1.1.0
**Features**
* [46: Define a route that returns a QR code representing the shortened URL](https://github.com/shlinkio/shlink/issues/46)
**Enhancements:**
* [32: Add support for other cache adapters by improving the Cache factory](https://github.com/shlinkio/shlink/issues/32)
* [14: https://github.com/shlinkio/shlink/issues/14](https://github.com/shlinkio/shlink/issues/14)
* [41: Cache the "short code" => "URL" map to prevent extra DB hits](https://github.com/shlinkio/shlink/issues/41)
* [13: Improve REST authentication](https://github.com/shlinkio/shlink/issues/13)
**Tasks**
* [39: Change copyright from "Alejandro Celaya" to "Shlink" in error pages](https://github.com/shlinkio/shlink/issues/39)
* [42: Make REST endpoints that need to find something return a 404 when "something" is not found](https://github.com/shlinkio/shlink/issues/42)
* [35: Make CLI commands to use the same PHP namespace as the one used for the command name](https://github.com/shlinkio/shlink/issues/35)
**Bugs**
* [40: Take into account the X-Forwarded-For header in order to get the visitor information, in case the server is behind a load balancer or proxy](https://github.com/shlinkio/shlink/issues/40)
### 1.0.0
**Enhancements:**
* [33: Create a command to generate a short code charset by randomizing the default one](https://github.com/shlinkio/shlink/issues/33)
* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/shlinkio/shlink/issues/15)
* [23: Translate application literals](https://github.com/shlinkio/shlink/issues/23)
* [21: Allow to filter visits by date range](https://github.com/shlinkio/shlink/issues/21)
* [22: Save visits locations data on a visit_locations table](https://github.com/shlinkio/shlink/issues/22)
* [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/shlinkio/shlink/issues/20)
* [11: Separate code into multiple modules](https://github.com/shlinkio/shlink/issues/11)
* [18: Group routable middleware in an Action namespace](https://github.com/shlinkio/shlink/issues/18)
**Tasks**
* [36: Remove hhvm from the CI matrix since it doesn't support array constants and will fail](https://github.com/shlinkio/shlink/issues/36)
* [4: Installation steps](https://github.com/shlinkio/shlink/issues/4)
* [6: Remove dependency on expressive helpers package](https://github.com/shlinkio/shlink/issues/6)
* [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/shlinkio/shlink/issues/30)
* [12: Improve code coverage](https://github.com/shlinkio/shlink/issues/12)
* [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/shlinkio/shlink/issues/25)
* [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/shlinkio/shlink/issues/19)
**Bugs**
* [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/shlinkio/shlink/issues/24)
### 0.2.0
**Enhancements:**
* [9: Use symfony/console to dispatch console requests, instead of trying to integrate the process with expressive](https://github.com/shlinkio/shlink/issues/9)
* [8: Create a REST API](https://github.com/shlinkio/shlink/issues/8)
* [10: Add more CLI functionality](https://github.com/shlinkio/shlink/issues/10)
**Tasks**
* [5: Create CHANGELOG file](https://github.com/shlinkio/shlink/issues/5)

View File

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

View File

@@ -1,2 +1,9 @@
# url-shortener
# Shlink
[![Build Status](https://travis-ci.org/shlinkio/shlink.svg?branch=master)](https://travis-ci.org/shlinkio/shlink)
[![Code Coverage](https://scrutinizer-ci.com/g/shlinkio/shlink/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/shlinkio/shlink/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/shlinkio/shlink/?branch=master)
[![Latest Stable Version](https://poser.pugx.org/shlinkio/shlink/v/stable.png)](https://packagist.org/packages/shlinkio/shlink)
[![License](https://poser.pugx.org/shlinkio/shlink/license.png)](https://packagist.org/packages/shlinkio/shlink)
A PHP-based URL shortener application with analytics and management

15
bin/cli
View File

@@ -1,17 +1,10 @@
#!/usr/bin/env php
<?php
declare(strict_types=1);
use Interop\Container\ContainerInterface;
use Zend\Diactoros\ServerRequestFactory;
use Zend\Diactoros\Uri;
use Zend\Expressive\Application;
use Symfony\Component\Console\Application as CliApp;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
/** @var Application $app */
$app = $container->get(Application::class);
$command = count($_SERVER['argv']) > 1 ? $_SERVER['argv'][1] : '';
$request = ServerRequestFactory::fromGlobals()
->withMethod('CLI')
->withUri(new Uri(sprintf('/%s', $command)));
$app->run($request);
$container->get(CliApp::class)->run();

29
bin/install Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env php
<?php
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizer;
use Symfony\Component\Console\Application;
use Symfony\Component\Filesystem\Filesystem;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ServiceManager\ServiceManager;
chdir(dirname(__DIR__));
require __DIR__ . '/../vendor/autoload.php';
$container = new ServiceManager([
'factories' => [
Application::class => InstallApplicationFactory::class,
Filesystem::class => InvokableFactory::class,
],
'services' => [
'config' => [
ConfigAbstractFactory::class => [
DatabaseConfigCustomizer::class => [Filesystem::class]
],
],
],
]);
$container->build(Application::class)->run();

29
bin/update Executable file
View File

@@ -0,0 +1,29 @@
#!/usr/bin/env php
<?php
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizer;
use Symfony\Component\Console\Application;
use Symfony\Component\Filesystem\Filesystem;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ServiceManager\ServiceManager;
chdir(dirname(__DIR__));
require __DIR__ . '/../vendor/autoload.php';
$container = new ServiceManager([
'factories' => [
Application::class => InstallApplicationFactory::class,
Filesystem::class => InvokableFactory::class,
],
'services' => [
'config' => [
ConfigAbstractFactory::class => [
DatabaseConfigCustomizer::class => [Filesystem::class]
],
],
],
]);
$container->build(Application::class, ['isUpdate' => true])->run();

BIN
bin/wkhtmltoimage Executable file

Binary file not shown.

48
build.sh Executable file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -e
if [ "$#" -ne 1 ]; then
echo "Usage:" >&2
echo " $0 {version}" >&2
exit 1
fi
version=$1
builtcontent=$(readlink -f "../shlink_${version}_dist")
projectdir=$(pwd)
# Copy project content to temp dir
echo 'Copying project files...'
rm -rf "${builtcontent}"
mkdir "${builtcontent}"
sudo chmod -R 777 "${projectdir}"/data/infra/{database,nginx}
cp -R "${projectdir}"/* "${builtcontent}"
cd "${builtcontent}"
# Install dependencies
rm -rf vendor
rm -f composer.lock
composer self-update
composer install --no-dev --optimize-autoloader --no-progress --no-interaction
# Delete development files
echo 'Deleting dev files...'
rm build.sh
rm CHANGELOG.md
rm composer.*
rm LICENSE
rm indocker
rm docker-compose.yml
rm php*
rm README.md
rm -rf build
rm -ff data/database.sqlite
rm -rf data/infra
rm -rf data/{cache,log,proxies}/{*,.gitignore}
rm -rf config/params/{*,.gitignore}
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}
# Compressing file
rm -f "${projectdir}"/build/shlink_${version}_dist.zip
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip "../shlink_${version}_dist"
rm -rf "${builtcontent}"

View File

@@ -1,11 +0,0 @@
<?php
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Interop\Container\ContainerInterface;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/config/container.php';
/** @var EntityManager $em */
$em = $container->get(EntityManager::class);
return ConsoleRunner::createHelperSet($em);

View File

@@ -1,54 +1,109 @@
{
"name": "acelaya/url-shortener",
"name": "shlinkio/shlink",
"type": "project",
"homepage": "https://github.com/acelaya/url-shortener",
"homepage": "https://shlink.io",
"description": "A self-hosted and PHP-based URL shortener application with CLI and REST interfaces",
"license": "MIT",
"authors": [
{
"name": "Alejandro Celaya ALastrué",
"homepage": "http://www.alejandrocelaya.com",
"name": "Alejandro Celaya Alastrué",
"homepage": "https://www.alejandrocelaya.com",
"email": "alejandro@alejandrocelaya.com"
}
],
"require": {
"php": "^5.5 || ^7.0",
"zendframework/zend-expressive": "^1.0",
"zendframework/zend-expressive-helpers": "^2.0",
"zendframework/zend-expressive-fastroute": "^1.1",
"zendframework/zend-expressive-twigrenderer": "^1.0",
"zendframework/zend-stdlib": "^2.7",
"zendframework/zend-servicemanager": "^3.0",
"php": "^7.0",
"acelaya/ze-content-based-error-handler": "^2.0",
"cocur/slugify": "^3.0",
"doctrine/annotations": "^1.4",
"doctrine/cache": "^1.6",
"doctrine/collections": "^1.4",
"doctrine/common": "^2.7",
"doctrine/dbal": "^2.5",
"doctrine/migrations": "^1.4",
"doctrine/orm": "^2.5",
"endroid/qrcode": "^1.7",
"firebase/php-jwt": "^4.0",
"guzzlehttp/guzzle": "^6.2",
"acelaya/zsm-annotated-services": "^0.2.0"
"http-interop/http-middleware": "^0.4.1",
"mikehaertl/phpwkhtmltopdf": "^2.2",
"monolog/monolog": "^1.21",
"roave/security-advisories": "dev-master",
"symfony/console": "^3.4",
"symfony/filesystem": "^3.0",
"symfony/process": "^3.0",
"theorchard/monolog-cascade": "^0.4",
"zendframework/zend-config": "^3.0",
"zendframework/zend-config-aggregator": "^1.0",
"zendframework/zend-expressive": "^2.0",
"zendframework/zend-expressive-fastroute": "^2.0",
"zendframework/zend-expressive-helpers": "^4.2",
"zendframework/zend-expressive-platesrenderer": "^1.3",
"zendframework/zend-i18n": "^2.7",
"zendframework/zend-inputfilter": "^2.8",
"zendframework/zend-paginator": "^2.6",
"zendframework/zend-servicemanager": "^3.2",
"zendframework/zend-stdlib": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8",
"squizlabs/php_codesniffer": "^2.3",
"roave/security-advisories": "dev-master",
"filp/whoops": "^2.0",
"phpunit/dbunit": "^3.0",
"phpunit/phpcov": "^4.0",
"phpunit/phpunit": "^6.0",
"slevomat/coding-standard": "^4.0",
"squizlabs/php_codesniffer": "^3.1 <3.2",
"symfony/dotenv": "^3.4",
"symfony/var-dumper": "^3.0",
"vlucas/phpdotenv": "^2.2"
"zendframework/zend-expressive-tooling": "^0.4"
},
"autoload": {
"psr-4": {
"Acelaya\\UrlShortener\\": "src"
}
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
"Shlinkio\\Shlink\\Common\\": "module/Common/src"
},
"files": [
"module/Common/functions/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"AcelayaTest\\UrlShortener\\": "tests"
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
"ShlinkioTest\\Shlink\\Core\\": [
"module/Core/test",
"module/Core/test-func"
],
"ShlinkioTest\\Shlink\\Common\\": [
"module/Common/test",
"module/Common/test-func"
]
}
},
"scripts": {
"check": [
"@cs",
"@test"
"@test",
"@func-test"
],
"cs": "phpcs",
"cs-fix": "phpcbf",
"serve": "php -S 0.0.0.0:8000 -t public/",
"test": "phpunit --coverage-clover build/clover.xml",
"pretty-test": "phpunit --coverage-html build/coverage"
"test": "phpunit --coverage-php build/coverage-unit.cov",
"pretty-test": "phpunit --coverage-html build/coverage",
"func-test": "phpunit -c phpunit-func.xml --coverage-php build/coverage-func.cov",
"complete-pretty-test": [
"@test",
"@func-test",
"phpcov merge build --html build/html"
]
},
"config": {
"process-timeout": 0,
"sort-packages": true,
"platform": {
"php": "7.0.8"
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
use function Shlinkio\Shlink\Common\env;
return [
'app_options' => [
'name' => 'Shlink',
'version' => '1.7.0',
'secret_key' => env('SECRET_KEY'),
],
];

View File

@@ -1,15 +0,0 @@
<?php
use Acelaya\UrlShortener\Middleware\CliRoutable;
return [
'routes' => [
[
'name' => 'cli-generate-shortcode',
'path' => '/generate-shortcode',
'middleware' => CliRoutable\GenerateShortcodeMiddleware::class,
'allowed_methods' => ['CLI'],
],
],
];

View File

@@ -1,15 +0,0 @@
<?php
return [
'database' => [
'driver' => 'pdo_mysql',
'user' => getenv('DB_USER'),
'password' => getenv('DB_PASSWORD'),
'dbname' => getenv('DB_NAME') ?: 'acelaya_url_shortener',
'charset' => 'utf8',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'
],
],
];

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
use Zend\Expressive;
use Zend\Expressive\Container;
use Zend\Expressive\Helper;
use Zend\Expressive\Middleware;
use Zend\Expressive\Plates;
use Zend\Expressive\Router;
use Zend\Expressive\Router\Middleware\ImplicitOptionsMiddleware;
use Zend\Expressive\Template;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\Stratigility\Middleware\ErrorHandler;
return [
'dependencies' => [
'factories' => [
Expressive\Application::class => Container\ApplicationFactory::class,
Template\TemplateRendererInterface::class => Plates\PlatesRendererFactory::class,
Router\RouterInterface::class => Router\FastRouteRouterFactory::class,
ErrorHandler::class => Container\ErrorHandlerFactory::class,
ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
Helper\ServerUrlHelper::class => InvokableFactory::class,
],
'aliases' => [
Middleware\ImplicitOptionsMiddleware::class => ImplicitOptionsMiddleware::class,
],
],
];

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Common;
return [
'entity_manager' => [
'orm' => [
'proxies_dir' => 'data/proxies',
],
'connection' => [
'user' => Common\env('DB_USER'),
'password' => Common\env('DB_PASSWORD'),
'dbname' => Common\env('DB_NAME', 'shlink'),
'charset' => 'utf8',
],
],
];

View File

@@ -0,0 +1,14 @@
<?php
return [
'entity_manager' => [
'connection' => [
'driver' => 'pdo_mysql',
'host' => 'shlink_db',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
],
],
],
];

View File

@@ -0,0 +1,27 @@
<?php
use Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory;
return [
'dependencies' => [
'invokables' => [
'Zend\Expressive\Whoops' => Whoops\Run::class,
'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class,
],
],
'whoops' => [
'json_exceptions' => [
'display' => true,
'show_trace' => true,
'ajax_only' => true,
],
],
'error_handler' => [
'plugins' => [
'factories' => [
'text/html' => WhoopsErrorResponseGeneratorFactory::class,
],
],
],
];

View File

@@ -1,7 +1,8 @@
<?php
return [
'debug' => true,
'debug' => true,
'config_cache_enabled' => false,
];

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;
return [
'logger' => [
'formatters' => [
'dashed' => [
'format' => '[%datetime%] %channel%.%level_name% - %message% %context%' . PHP_EOL,
'include_stacktraces' => true,
],
],
'handlers' => [
'rotating_file_handler' => [
'class' => RotatingFileHandler::class,
'level' => Logger::INFO,
'filename' => 'data/log/shlink_log.log',
'max_files' => 30,
'formatter' => 'dashed',
],
],
'loggers' => [
'Shlink' => [
'handlers' => ['rotating_file_handler'],
],
],
],
];

View File

@@ -0,0 +1,14 @@
<?php
use Monolog\Logger;
return [
'logger' => [
'handlers' => [
'rotating_file_handler' => [
'level' => Logger::DEBUG,
],
],
],
];

View File

@@ -1,32 +1,55 @@
<?php
use Acelaya\UrlShortener\Middleware\CliParamsMiddleware;
use Zend\Expressive\Container\ApplicationFactory;
use Zend\Expressive\Helper;
declare(strict_types=1);
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware;
use Zend\Expressive;
use Zend\Stratigility\Middleware\ErrorHandler;
return [
'middleware_pipeline' => [
'always' => [
'pre-routing' => [
'middleware' => [
Helper\ServerUrlMiddleware::class,
ErrorHandler::class,
LocaleMiddleware::class,
],
'priority' => 10000,
'priority' => 11,
],
'pre-routing-rest' => [
// 'path' => '/rest',
'middleware' => [
PathVersionMiddleware::class,
],
'priority' => 11,
],
'routing' => [
'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE,
CliParamsMiddleware::class,
Helper\UrlHelperMiddleware::class,
ApplicationFactory::DISPATCH_MIDDLEWARE,
Expressive\Router\Middleware\RouteMiddleware::class,
],
'priority' => 10,
],
'rest' => [
'path' => '/rest',
'middleware' => [
CrossDomainMiddleware::class,
Expressive\Router\Middleware\ImplicitOptionsMiddleware::class,
BodyParserMiddleware::class,
CheckAuthenticationMiddleware::class,
],
'priority' => 5,
],
'post-routing' => [
'middleware' => [
Expressive\Router\Middleware\DispatchMiddleware::class,
],
'priority' => 1,
],
'error' => [
'middleware' => [],
'error' => true,
'priority' => -10000,
],
],
];

View File

@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
return [
'phpwkhtmltopdf' => [
'images' => [
'binary' => 'bin/wkhtmltoimage',
'type' => 'jpg',
],
],
];

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
return [
'preview_generation' => [
'files_location' => 'data/cache',
],
];

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
use Zend\Expressive\Router\FastRouteRouter;
return [
'router' => [
'fastroute' => [
FastRouteRouter::CONFIG_CACHE_ENABLED => true,
FastRouteRouter::CONFIG_CACHE_FILE => 'data/cache/fastroute_cached_routes.php',
],
],
];

View File

@@ -0,0 +1,12 @@
<?php
use Zend\Expressive\Router\FastRouteRouter;
return [
'router' => [
'fastroute' => [
FastRouteRouter::CONFIG_CACHE_ENABLED => false,
],
],
];

View File

@@ -1,15 +0,0 @@
<?php
use Acelaya\UrlShortener\Middleware\Routable;
return [
'routes' => [
[
'name' => 'long-url-redirect',
'path' => '/{shortCode}',
'middleware' => Routable\RedirectMiddleware::class,
'allowed_methods' => ['GET'],
],
],
];

View File

@@ -1,53 +0,0 @@
<?php
use Acelaya\UrlShortener\Factory\CacheFactory;
use Acelaya\UrlShortener\Factory\EntityManagerFactory;
use Acelaya\UrlShortener\Middleware;
use Acelaya\UrlShortener\Service;
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
use Zend\Expressive\Application;
use Zend\Expressive\Container;
use Zend\Expressive\Helper;
use Zend\Expressive\Router;
use Zend\Expressive\Template;
use Zend\Expressive\Twig;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
'services' => [
'factories' => [
Application::class => Container\ApplicationFactory::class,
// Url helpers
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class,
Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
Helper\ServerUrlHelper::class => InvokableFactory::class,
Router\FastRouteRouter::class => InvokableFactory::class,
// View
'Zend\Expressive\FinalHandler' => Container\TemplatedErrorHandlerFactory::class,
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
// Services
EntityManager::class => EntityManagerFactory::class,
GuzzleHttp\Client::class => InvokableFactory::class,
Service\UrlShortener::class => AnnotatedFactory::class,
Service\VisitsTracker::class => AnnotatedFactory::class,
Cache::class => CacheFactory::class,
// Middleware
Middleware\CliRoutable\GenerateShortcodeMiddleware::class => AnnotatedFactory::class,
Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class,
Middleware\CliParamsMiddleware::class => Middleware\Factory\CliParamsMiddlewareFactory::class,
],
'aliases' => [
'em' => EntityManager::class,
'httpClient' => GuzzleHttp\Client::class,
Router\RouterInterface::class => Router\FastRouteRouter::class,
]
],
];

View File

@@ -1,15 +1,13 @@
<?php
declare(strict_types=1);
return [
'templates' => [
'paths' => [
'templates'
],
'extension' => 'phtml',
],
'twig' => [
'cache_dir' => 'data/cache/twig',
'plates' => [
'extensions' => [
// extension service names or instances
],

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Common;
return [
'translator' => [
'locale' => Common\env('DEFAULT_LOCALE', 'en'),
],
];

View File

@@ -1,12 +1,18 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Core\Service\UrlShortener;
use function Shlinkio\Shlink\Common\env;
return [
'url_shortener' => [
'domain' => [
'schema' => getenv('SHORTENED_URL_SCHEMA') ?: 'http',
'hostname' => getenv('SHORTENED_URL_HOSTNAME'),
'schema' => env('SHORTENED_URL_SCHEMA', 'http'),
'hostname' => env('SHORTENED_URL_HOSTNAME'),
],
'shortcode_chars' => getenv('SHORTCODE_CHARS'),
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
'validate_url' => true,
],
];

View File

@@ -1,14 +1,9 @@
<?php
declare(strict_types=1);
return [
'debug' => false,
'config_cache_enabled' => true,
'config_cache_enabled' => false,
'zend-expressive' => [
'error_handler' => [
'template_404' => 'error/404.html.twig',
'template_error' => 'error/error.html.twig',
],
],
];

35
config/cli-config.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Interop\Container\ContainerInterface;
use Zend\ServiceManager\ServiceManager;
$isTest = false;
foreach ($_SERVER['argv'] as $i => $arg) {
if ($arg === '--test') {
unset($_SERVER['argv'][$i]);
$isTest = true;
break;
}
}
/** @var ContainerInterface|ServiceManager $container */
$container = include __DIR__ . '/container.php';
// If in testing env, override DB connection to use an in-memory sqlite database
if ($isTest) {
$container->setAllowOverride(true);
$config = $container->get('config');
$config['entity_manager']['connection'] = [
'driver' => 'pdo_sqlite',
'path' => realpath(sys_get_temp_dir()) . '/shlink-tests.db',
];
$container->setService('config', $config);
}
/** @var EntityManager $em */
$em = $container->get(EntityManager::class);
return ConsoleRunner::createHelperSet($em);

View File

@@ -1,6 +1,12 @@
<?php
use Zend\Stdlib\ArrayUtils;
use Zend\Stdlib\Glob;
declare(strict_types=1);
use Acelaya\ExpressiveErrorHandler;
use Shlinkio\Shlink\CLI;
use Shlinkio\Shlink\Common;
use Shlinkio\Shlink\Core;
use Shlinkio\Shlink\Rest;
use Zend\ConfigAggregator;
/**
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
@@ -11,22 +17,13 @@ use Zend\Stdlib\Glob;
* Obviously, if you use closures in your config you can't cache it.
*/
$cachedConfigFile = 'data/cache/app_config.php';
$config = [];
if (is_file($cachedConfigFile)) {
// Try to load the cached config
$config = include $cachedConfigFile;
} else {
// Load configuration from autoload path
foreach (Glob::glob('config/autoload/{{,*.}global,{,*.}local}.php', Glob::GLOB_BRACE) as $file) {
$config = ArrayUtils::merge($config, include $file);
}
// Cache config if enabled
if (isset($config['config_cache_enabled']) && $config['config_cache_enabled'] === true) {
file_put_contents($cachedConfigFile, '<?php return ' . var_export($config, true) . ';');
}
}
return $config;
return (new ConfigAggregator\ConfigAggregator([
Zend\Expressive\ConfigProvider::class,
Zend\Expressive\Router\ConfigProvider::class,
ExpressiveErrorHandler\ConfigProvider::class,
Common\ConfigProvider::class,
Core\ConfigProvider::class,
CLI\ConfigProvider::class,
Rest\ConfigProvider::class,
new ConfigAggregator\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
], 'data/cache/app_config.php'))->getMergedConfig();

View File

@@ -1,5 +1,7 @@
<?php
use Dotenv\Dotenv;
declare(strict_types=1);
use Symfony\Component\Dotenv\Dotenv;
use Zend\ServiceManager\ServiceManager;
chdir(dirname(__DIR__));
@@ -9,13 +11,13 @@ require 'vendor/autoload.php';
// If the Dotenv class exists, load env vars and enable errors
if (class_exists(Dotenv::class)) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
$dotenv = new Dotenv(__DIR__ . '/..');
$dotenv->load();
ini_set('display_errors', '1');
$dotenv = new Dotenv();
$dotenv->load(__DIR__ . '/../.env');
}
// Build container
$config = require __DIR__ . '/config.php';
$container = new ServiceManager($config['services']);
$container = new ServiceManager($config['dependencies']);
$container->setService('config', $config);
return $container;

2
config/params/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

2
data/infra/database/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

6
data/infra/db.Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM mysql:5.7
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
# Enable remote access (default is localhost only, we change this
# otherwise our database would not be reachable from outside the container)
RUN sed -i -e"s/^bind-address\s*=\s*127.0.0.1/bind-address = 0.0.0.0/" /etc/mysql/my.cnf

View File

@@ -0,0 +1,5 @@
FROM nginx:1.11.6-alpine
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
# Delete default nginx vhost
RUN rm /etc/nginx/conf.d/default.conf

2
data/infra/nginx/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

90
data/infra/php.Dockerfile Normal file
View File

@@ -0,0 +1,90 @@
FROM php:7.1-fpm-alpine
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
RUN apk update
# Install common php extensions
RUN docker-php-ext-install pdo_mysql
RUN docker-php-ext-install iconv
RUN docker-php-ext-install mbstring
RUN docker-php-ext-install calendar
RUN apk add --no-cache --virtual sqlite-libs
RUN apk add --no-cache --virtual sqlite-dev
RUN docker-php-ext-install pdo_sqlite
RUN apk add --no-cache --virtual icu-dev
RUN docker-php-ext-install intl
RUN apk add --no-cache --virtual zlib-dev
RUN docker-php-ext-install zip
RUN apk add --no-cache --virtual libmcrypt-dev
RUN docker-php-ext-install mcrypt
RUN apk add --no-cache --virtual libpng-dev
RUN docker-php-ext-install gd
# Install redis extension
ADD https://github.com/phpredis/phpredis/archive/3.1.4.tar.gz /tmp/phpredis.tar.gz
RUN mkdir -p /usr/src/php/ext/redis\
&& tar xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1
# configure and install
RUN docker-php-ext-configure redis\
&& docker-php-ext-install redis
# cleanup
RUN rm /tmp/phpredis.tar.gz
# Install memcached extension
RUN apk add --no-cache --virtual cyrus-sasl-dev
RUN apk add --no-cache --virtual libmemcached-dev
ADD https://github.com/php-memcached-dev/php-memcached/archive/php7.tar.gz /tmp/memcached.tar.gz
RUN mkdir -p /usr/src/php/ext/memcached\
&& tar xf /tmp/memcached.tar.gz -C /usr/src/php/ext/memcached --strip-components=1
# configure and install
RUN docker-php-ext-configure memcached\
&& docker-php-ext-install memcached
# cleanup
RUN rm /tmp/memcached.tar.gz
# Install APCu extension
ADD https://pecl.php.net/get/apcu-5.1.3.tgz /tmp/apcu.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu\
&& tar xf /tmp/apcu.tar.gz -C /usr/src/php/ext/apcu --strip-components=1
# configure and install
RUN docker-php-ext-configure apcu\
&& docker-php-ext-install apcu
# cleanup
RUN rm /tmp/apcu.tar.gz
# Install APCu-BC extension
ADD https://pecl.php.net/get/apcu_bc-1.0.3.tgz /tmp/apcu_bc.tar.gz
RUN mkdir -p /usr/src/php/ext/apcu-bc\
&& tar xf /tmp/apcu_bc.tar.gz -C /usr/src/php/ext/apcu-bc --strip-components=1
# configure and install
RUN docker-php-ext-configure apcu-bc\
&& docker-php-ext-install apcu-bc
# cleanup
RUN rm /tmp/apcu_bc.tar.gz
# Load APCU.ini before APC.ini
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-2.5.0 /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 composer
RUN php -r "readfile('https://getcomposer.org/installer');" | php
RUN chmod +x composer.phar
RUN mv composer.phar /usr/local/bin/composer
# Make home directory writable by anyone
RUN chmod 777 /home

1
data/infra/php.ini Normal file
View File

@@ -0,0 +1 @@
date.timezone = Europe/Madrid

View File

@@ -0,0 +1 @@
extension="apcu.so"

View File

@@ -0,0 +1 @@
extension="memcached.so"

21
data/infra/vhost.conf Normal file
View File

@@ -0,0 +1,21 @@
server {
listen 80 default_server;
server_name shlink.local;
root /home/shlink/www/public;
index index.php;
charset utf-8;
error_log /home/shlink/www/data/infra/nginx/shlink.error.log;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php$ {
root /home/shlink/www/public;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass shlink_php:9000;
fastcgi_index index.php;
include fastcgi.conf;
}
}

2
data/log/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20160819142757 extends AbstractMigration
{
const MYSQL = 'mysql';
const SQLITE = 'sqlite';
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
$db = $this->connection->getDatabasePlatform()->getName();
$table = $schema->getTable('short_urls');
$column = $table->getColumn('short_code');
if ($db === self::MYSQL) {
$column->setPlatformOption('collation', 'utf8_bin');
} elseif ($db === self::SQLITE) {
$column->setPlatformOption('collate', 'BINARY');
}
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
$db = $this->connection->getDatabasePlatform()->getName();
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\Type;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20160820191203 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
// Check if the tables already exist
$tables = $schema->getTables();
foreach ($tables as $table) {
if ($table->getName() === 'tags') {
return;
}
}
$this->createTagsTable($schema);
$this->createShortUrlsInTagsTable($schema);
}
protected function createTagsTable(Schema $schema)
{
$table = $schema->createTable('tags');
$table->addColumn('id', Type::BIGINT, [
'unsigned' => true,
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('name', Type::STRING, [
'length' => 255,
'notnull' => true,
]);
$table->addUniqueIndex(['name']);
$table->setPrimaryKey(['id']);
}
protected function createShortUrlsInTagsTable(Schema $schema)
{
$table = $schema->createTable('short_urls_in_tags');
$table->addColumn('short_url_id', Type::BIGINT, [
'unsigned' => true,
'notnull' => true,
]);
$table->addColumn('tag_id', Type::BIGINT, [
'unsigned' => true,
'notnull' => true,
]);
$table->addForeignKeyConstraint('tags', ['tag_id'], ['id'], [
'onDelete' => 'CASCADE',
'onUpdate' => 'RESTRICT',
]);
$table->addForeignKeyConstraint('short_urls', ['short_url_id'], ['id'], [
'onDelete' => 'CASCADE',
'onUpdate' => 'RESTRICT',
]);
$table->setPrimaryKey(['short_url_id', 'tag_id']);
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
$schema->dropTable('short_urls_in_tags');
$schema->dropTable('tags');
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Type;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20171021093246 extends AbstractMigration
{
/**
* @param Schema $schema
* @throws SchemaException
*/
public function up(Schema $schema)
{
$shortUrls = $schema->getTable('short_urls');
if ($shortUrls->hasColumn('valid_since')) {
return;
}
$shortUrls->addColumn('valid_since', Type::DATETIME, [
'notnull' => false,
]);
$shortUrls->addColumn('valid_until', Type::DATETIME, [
'notnull' => false,
]);
}
/**
* @param Schema $schema
* @throws SchemaException
*/
public function down(Schema $schema)
{
$shortUrls = $schema->getTable('short_urls');
if (! $shortUrls->hasColumn('valid_since')) {
return;
}
$shortUrls->dropColumn('valid_since');
$shortUrls->dropColumn('valid_until');
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\SchemaException;
use Doctrine\DBAL\Types\Type;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20171022064541 extends AbstractMigration
{
/**
* @param Schema $schema
* @throws SchemaException
*/
public function up(Schema $schema)
{
$shortUrls = $schema->getTable('short_urls');
if ($shortUrls->hasColumn('max_visits')) {
return;
}
$shortUrls->addColumn('max_visits', Type::INTEGER, [
'unsigned' => true,
'notnull' => false,
]);
}
/**
* @param Schema $schema
* @throws SchemaException
*/
public function down(Schema $schema)
{
$shortUrls = $schema->getTable('short_urls');
if (! $shortUrls->hasColumn('max_visits')) {
return;
}
$shortUrls->dropColumn('max_visits');
}
}

0
data/proxies/.gitignore vendored Normal file → Executable file
View File

View File

@@ -0,0 +1,8 @@
version: '2'
services:
shlink_php:
user: 1000:1000
volumes:
- /etc/passwd:/etc/passwd:ro
- /etc/group:/etc/group:ro

41
docker-compose.yml Normal file
View File

@@ -0,0 +1,41 @@
version: '2'
services:
shlink_nginx:
container_name: shlink_nginx
build:
context: .
dockerfile: ./data/infra/nginx.Dockerfile
ports:
- "8000:80"
volumes:
- ./:/home/shlink/www
- ./docs:/home/shlink/www/public/docs
- ./data/infra/vhost.conf:/etc/nginx/conf.d/shlink-vhost.conf
links:
- shlink_php
shlink_php:
container_name: shlink_php
build:
context: .
dockerfile: ./data/infra/php.Dockerfile
volumes:
- ./:/home/shlink/www
- ./data/infra/php.ini:/usr/local/etc/php/php.ini
links:
- shlink_db
shlink_db:
container_name: shlink_db
build:
context: .
dockerfile: ./data/infra/db.Dockerfile
ports:
- "3307:3306"
volumes:
- ./:/home/shlink/www
- ./data/infra/database:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: shlink

View File

@@ -0,0 +1,13 @@
{
"type": "object",
"properties": {
"code": {
"type": "string",
"description": "A machine unique code"
},
"message": {
"type": "string",
"description": "A human-friendly error message"
}
}
}

View File

@@ -0,0 +1,13 @@
{
"type": "object",
"properties": {
"currentPage": {
"type": "integer",
"description": "The number of current page being displayed."
},
"pagesCount": {
"type": "integer",
"description": "The total number of pages that can be displayed."
}
}
}

View File

@@ -0,0 +1,29 @@
{
"type": "object",
"properties": {
"shortCode": {
"type": "string",
"description": "The short code for this short URL."
},
"originalUrl": {
"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"
}
}
}

View File

@@ -0,0 +1,18 @@
{
"type": "object",
"properties": {
"referer": {
"type": "string"
},
"date": {
"type": "string",
"format": "date-time"
},
"remoteAddr": {
"type": "string"
},
"userAgent": {
"type": "string"
}
}
}

View File

@@ -0,0 +1,82 @@
{
"post": {
"tags": [
"Authentication"
],
"summary": "Perform authentication",
"description": "Performs an authentication",
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"apiKey"
],
"properties": {
"apiKey": {
"description": "The API key to authenticate with",
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "The authentication worked.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"token": {
"type": "string",
"description": "The authentication token that needs to be sent in the Authorization header"
}
}
}
}
},
"examples": {
"application/json": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
}
}
},
"400": {
"description": "An API key was not provided.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"401": {
"description": "The API key is incorrect, is disabled or has expired.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,239 @@
{
"get": {
"tags": [
"ShortCodes"
],
"summary": "List short URLs",
"description": "Returns the list of short codes",
"parameters": [
{
"name": "page",
"in": "query",
"description": "The page to be displayed. Defaults to 1",
"required": false,
"schema": {
"type": "integer"
}
},
{
"name": "searchTerm",
"in": "query",
"description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (Since v1.3.0)",
"required": false,
"schema": {
"type": "string"
}
},
{
"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)",
"required": false,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "orderBy",
"in": "query",
"description": "The field from which you want to order the result. (Since v1.3.0)",
"required": false,
"schema": {
"type": "string",
"enum": [
"originalUrl",
"shortCode",
"dateCreated",
"visits"
]
}
}
],
"security": [
{
"Bearer": []
}
],
"responses": {
"200": {
"description": "The list of short URLs",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"shortUrls": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "../definitions/ShortUrl.json"
}
},
"pagination": {
"$ref": "../definitions/Pagination.json"
}
}
}
}
}
}
},
"examples": {
"application/json": {
"shortUrls": {
"data": [
{
"shortCode": "12C18",
"originalUrl": "https://store.steampowered.com",
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsCount": 328,
"tags": [
"games",
"tech"
]
},
{
"shortCode": "12Kb3",
"originalUrl": "https://shlink.io",
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsCount": 1029,
"tags": [
"shlink"
]
},
{
"shortCode": "123bA",
"originalUrl": "https://www.google.com",
"dateCreated": "2015-10-01T20:34:16+02:00",
"visitsCount": 25,
"tags": []
}
],
"pagination": {
"currentPage": 5,
"pagesCount": 12
}
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
},
"post": {
"tags": [
"ShortCodes"
],
"summary": "Create short URL",
"description": "Creates a new short code",
"security": [
{
"Bearer": []
}
],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"longUrl"
],
"properties": {
"longUrl": {
"description": "The URL to parse",
"type": "string"
},
"tags": {
"description": "The URL to parse",
"type": "array",
"items": {
"type": "string"
}
},
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": "string"
},
"validUntil": {
"description": "The date (in ISO-8601 format) until which this short code will be valid",
"type": "string"
},
"customSlug": {
"description": "A unique custom slug to be used instead of the generated short code",
"type": "string"
},
"maxVisits": {
"description": "The maximum number of allowed visits for this short code",
"type": "number"
}
}
}
}
}
},
"responses": {
"200": {
"description": "The result of parsing the long URL",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"longUrl": {
"type": "string",
"description": "The original long URL that has been parsed"
},
"shortUrl": {
"type": "string",
"description": "The generated short URL"
},
"shortCode": {
"type": "string",
"description": "the short code that is being used in the short URL"
}
}
}
}
}
},
"400": {
"description": "The long URL was not provided or is invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,162 @@
{
"get": {
"tags": [
"ShortCodes"
],
"summary": "Parse short code",
"description": "Get the long URL behind a short code.",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
}
],
"security": [
{
"Bearer": []
}
],
"responses": {
"200": {
"description": "The long URL behind a short code.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"longUrl": {
"type": "string",
"description": "The original long URL behind the short code."
}
}
}
}
},
"examples": {
"application/json": {
"longUrl": "https://shlink.io"
}
}
},
"400": {
"description": "Provided shortCode does not match the character set currently used by the app to generate short codes.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"404": {
"description": "No URL was found for provided short code.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
},
"put": {
"tags": [
"ShortCodes"
],
"summary": "Edit short code",
"description": "Update certain meta arguments from an existing short URL.",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to edit.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"validSince": {
"description": "The date (in ISO-8601 format) from which this short code will be valid",
"type": "string"
},
"validUntil": {
"description": "The date (in ISO-8601 format) until which this short code will be valid",
"type": "string"
},
"maxVisits": {
"description": "The maximum number of allowed visits for this short code",
"type": "number"
}
}
}
}
}
},
"security": [
{
"Bearer": []
}
],
"responses": {
"204": {
"description": "The short code has been properly updated."
},
"400": {
"description": "Provided meta arguments are invalid.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"404": {
"description": "No short URL was found for provided short code.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,107 @@
{
"put": {
"tags": [
"ShortCodes",
"Tags"
],
"summary": "Edit tags on short URL",
"description": "Edit the tags on provided short code.",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The shortCode in which we want to edit tags.",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"tags"
],
"properties": {
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "The list of tags to set to the short URL."
}
}
}
}
}
},
"security": [
{
"Bearer": []
}
],
"responses": {
"200": {
"description": "List of tags.",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},
"examples": {
"application/json": {
"tags": [
"games",
"tech"
]
}
}
},
"400": {
"description": "The request body does not contain a \"tags\" param with array type.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"404": {
"description": "No short URL was found for provided short code.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,97 @@
{
"get": {
"tags": [
"ShortCodes",
"Visits"
],
"summary": "List visits for short URL",
"description": "Get the list of visits on provided short code.",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The shortCode from which we want to get the visits.",
"required": true,
"schema": {
"type": "string"
}
}
],
"security": [
{
"Bearer": []
}
],
"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"
}
}
}
}
}
}
}
},
"examples": {
"application/json": {
"visits": {
"data": [
{
"referer": "https://twitter.com",
"date": "2015-08-20T05:05:03+04:00",
"remoteAddr": "10.20.30.40",
"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"
},
{
"referer": "https://t.co",
"date": "2015-08-20T05:05:03+04:00",
"remoteAddr": "11.22.33.44",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"
},
{
"referer": null,
"date": "2015-08-20T05:05:03+04:00",
"remoteAddr": "110.220.5.6",
"userAgent": "some_web_crawler/1.4"
}
]
}
}
}
},
"404": {
"description": "The short code does not belong to any short URL.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

View File

@@ -0,0 +1,259 @@
{
"get": {
"tags": [
"Tags"
],
"summary": "List existing tags",
"description": "Returns the list of all tags used in any short URL, ordered by name",
"security": [
{
"Bearer": []
}
],
"responses": {
"200": {
"description": "The list of tags",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tags": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
},
"examples": {
"application/json": {
"tags": {
"data": [
"games",
"php",
"shlink",
"tech"
]
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
},
"post": {
"tags": [
"Tags"
],
"summary": "Create tags",
"description": "Provided a list of tags, creates all that do not yet exist",
"security": [
{
"Bearer": []
}
],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"tags"
],
"properties": {
"tags": {
"description": "The list of tag names to create",
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "The list of tags",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"tags": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}
}
},
"examples": {
"application/json": {
"tags": {
"data": [
"games",
"php",
"shlink",
"tech"
]
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
},
"put": {
"tags": [
"Tags"
],
"summary": "Rename tag",
"description": "Renames one existing tag",
"security": [
{
"Bearer": []
}
],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"oldName",
"newName"
],
"properties": {
"oldName": {
"description": "Current name of the tag",
"type": "string"
},
"newName": {
"description": "New name of the tag",
"type": "string"
}
}
}
}
}
},
"responses": {
"204": {
"description": "The tag has been properly renamed"
},
"400": {
"description": "You have not provided either the oldName or the newName params.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"404": {
"description": "There's no tag found with the name provided in oldName param.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
},
"delete": {
"tags": [
"Tags"
],
"summary": "Delete tags",
"description": "Deletes provided list of tags",
"parameters": [
{
"name": "tags[]",
"in": "query",
"description": "The names of the tags to delete",
"required": true,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
],
"security": [
{
"Bearer": []
}
],
"responses": {
"204": {
"description": "Tags properly deleted"
},
"500": {
"description": "Unexpected error.",
"content": {
"application/json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}

58
docs/swagger/swagger.json Normal file
View File

@@ -0,0 +1,58 @@
{
"openapi": "3.0.0",
"info": {
"title": "Shlink",
"description": "Shlink, the self-hosted URL shortener",
"version": "1.0"
},
"servers": [
{
"url": "{schema}://{server}/rest",
"variables": {
"schema": {
"default": "https",
"enum": ["https", "http"]
},
"server": {
"default": ""
}
}
}
],
"components": {
"securitySchemes": {
"Bearer": {
"description": "The JWT identifying a previously logged API key",
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
}
},
"paths": {
"/v1/authenticate": {
"$ref": "paths/v1_authenticate.json"
},
"/v1/short-codes": {
"$ref": "paths/v1_short-codes.json"
},
"/v1/short-codes/{shortCode}": {
"$ref": "paths/v1_short-codes_{shortCode}.json"
},
"/v1/short-codes/{shortCode}/tags": {
"$ref": "paths/v1_short-codes_{shortCode}_tags.json"
},
"/v1/tags": {
"$ref": "paths/v1_tags.json"
},
"/v1/short-codes/{shortCode}/visits": {
"$ref": "paths/v1_short-codes_{shortCode}_visits.json"
}
}
}

33
func_tests_bootstrap.php Normal file
View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
use ShlinkioTest\Shlink\Common\DbUnit\DatabaseTestCase;
use Symfony\Component\Process\Process;
use Zend\ServiceManager\ServiceManager;
// Create an empty .env file
if (! file_exists('.env')) {
touch('.env');
}
$shlinkDbPath = realpath(sys_get_temp_dir()) . '/shlink-tests.db';
if (file_exists($shlinkDbPath)) {
unlink($shlinkDbPath);
}
/** @var ServiceManager $sm */
$sm = require __DIR__ . '/config/container.php';
$sm->setAllowOverride(true);
$config = $sm->get('config');
$config['entity_manager']['connection'] = [
'driver' => 'pdo_sqlite',
'path' => $shlinkDbPath,
];
$sm->setService('config', $config);
// Create database
$process = new Process('vendor/bin/doctrine orm:schema-tool:create --no-interaction -q --test', __DIR__);
$process->inheritEnvironmentVariables()
->mustRun();
DatabaseTestCase::$em = $sm->get('em');

2
indocker Executable file
View File

@@ -0,0 +1,2 @@
#!/usr/bin/env bash
docker exec -it shlink_php /bin/sh -c "cd /home/shlink/www && $*"

4
migrations.yml Normal file
View File

@@ -0,0 +1,4 @@
name: ShlinkMigrations
migrations_namespace: ShlinkMigrations
table_name: migrations
migrations_directory: data/migrations

View File

@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\CLI\Command;
use Shlinkio\Shlink\Common;
return [
'cli' => [
'locale' => Common\env('CLI_LOCALE', 'en'),
'commands' => [
Command\Shortcode\GenerateShortcodeCommand::NAME => Command\Shortcode\GenerateShortcodeCommand::class,
Command\Shortcode\ResolveUrlCommand::NAME => Command\Shortcode\ResolveUrlCommand::class,
Command\Shortcode\ListShortcodesCommand::NAME => Command\Shortcode\ListShortcodesCommand::class,
Command\Shortcode\GetVisitsCommand::NAME => Command\Shortcode\GetVisitsCommand::class,
Command\Shortcode\GeneratePreviewCommand::NAME => Command\Shortcode\GeneratePreviewCommand::class,
Command\Visit\ProcessVisitsCommand::NAME => Command\Visit\ProcessVisitsCommand::class,
Command\Config\GenerateCharsetCommand::NAME => Command\Config\GenerateCharsetCommand::class,
Command\Config\GenerateSecretCommand::NAME => Command\Config\GenerateSecretCommand::class,
Command\Api\GenerateKeyCommand::NAME => Command\Api\GenerateKeyCommand::class,
Command\Api\DisableKeyCommand::NAME => Command\Api\DisableKeyCommand::class,
Command\Api\ListKeysCommand::NAME => Command\Api\ListKeysCommand::class,
Command\Tag\ListTagsCommand::NAME => Command\Tag\ListTagsCommand::class,
Command\Tag\CreateTagCommand::NAME => Command\Tag\CreateTagCommand::class,
Command\Tag\RenameTagCommand::NAME => Command\Tag\RenameTagCommand::class,
Command\Tag\DeleteTagsCommand::NAME => Command\Tag\DeleteTagsCommand::class,
],
],
];

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\CLI\Command;
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Core\Service;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console\Application;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
return [
'dependencies' => [
'factories' => [
Application::class => ApplicationFactory::class,
Command\Shortcode\GenerateShortcodeCommand::class => ConfigAbstractFactory::class,
Command\Shortcode\ResolveUrlCommand::class => ConfigAbstractFactory::class,
Command\Shortcode\ListShortcodesCommand::class => ConfigAbstractFactory::class,
Command\Shortcode\GetVisitsCommand::class => ConfigAbstractFactory::class,
Command\Shortcode\GeneratePreviewCommand::class => ConfigAbstractFactory::class,
Command\Visit\ProcessVisitsCommand::class => ConfigAbstractFactory::class,
Command\Config\GenerateCharsetCommand::class => ConfigAbstractFactory::class,
Command\Config\GenerateSecretCommand::class => ConfigAbstractFactory::class,
Command\Api\GenerateKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\DisableKeyCommand::class => ConfigAbstractFactory::class,
Command\Api\ListKeysCommand::class => ConfigAbstractFactory::class,
Command\Tag\ListTagsCommand::class => ConfigAbstractFactory::class,
Command\Tag\CreateTagCommand::class => ConfigAbstractFactory::class,
Command\Tag\RenameTagCommand::class => ConfigAbstractFactory::class,
Command\Tag\DeleteTagsCommand::class => ConfigAbstractFactory::class,
],
],
ConfigAbstractFactory::class => [
Command\Shortcode\GenerateShortcodeCommand::class => [
Service\UrlShortener::class,
'translator',
'config.url_shortener.domain',
],
Command\Shortcode\ResolveUrlCommand::class => [Service\UrlShortener::class, 'translator'],
Command\Shortcode\ListShortcodesCommand::class => [Service\ShortUrlService::class, 'translator'],
Command\Shortcode\GetVisitsCommand::class => [Service\VisitsTracker::class, 'translator'],
Command\Shortcode\GeneratePreviewCommand::class => [
Service\ShortUrlService::class,
PreviewGenerator::class,
'translator',
],
Command\Visit\ProcessVisitsCommand::class => [
Service\VisitService::class,
IpLocationResolver::class,
'translator',
],
Command\Config\GenerateCharsetCommand::class => ['translator'],
Command\Config\GenerateSecretCommand::class => ['translator'],
Command\Api\GenerateKeyCommand::class => [ApiKeyService::class, 'translator'],
Command\Api\DisableKeyCommand::class => [ApiKeyService::class, 'translator'],
Command\Api\ListKeysCommand::class => [ApiKeyService::class, 'translator'],
Command\Tag\ListTagsCommand::class => [Service\Tag\TagService::class, Translator::class],
Command\Tag\CreateTagCommand::class => [Service\Tag\TagService::class, Translator::class],
Command\Tag\RenameTagCommand::class => [Service\Tag\TagService::class, Translator::class],
Command\Tag\DeleteTagsCommand::class => [Service\Tag\TagService::class, Translator::class],
],
];

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
return [
'translator' => [
'translation_file_patterns' => [
[
'type' => 'gettext',
'base_dir' => __DIR__ . '/../lang',
'pattern' => '%s.mo',
],
],
],
];

BIN
module/CLI/lang/es.mo Normal file

Binary file not shown.

333
module/CLI/lang/es.po Normal file
View File

@@ -0,0 +1,333 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2018-01-21 09:36+0100\n"
"PO-Revision-Date: 2018-01-21 09:39+0100\n"
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
"Language-Team: \n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.4\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: translate;translatePlural\n"
"X-Poedit-SearchPath-0: src\n"
"X-Poedit-SearchPath-1: config\n"
msgid "Disables an API key."
msgstr "Desahbilita una clave de API."
msgid "The API key to disable"
msgstr "La clave de API a deshabilitar"
#, php-format
msgid "API key \"%s\" properly disabled"
msgstr "Clave de API \"%s\" deshabilitada correctamente"
#, php-format
msgid "API key \"%s\" does not exist."
msgstr "La clave de API \"%s\" no existe."
msgid "Generates a new valid API key."
msgstr "Genera una nueva clave de API válida."
msgid "The date in which the API key should expire. Use any valid PHP format."
msgstr ""
"La fecha en la que la clave de API debe expirar. Utiliza cualquier valor "
"válido en PHP."
#, php-format
msgid "Generated API key: \"%s\""
msgstr "Generada clave de API. \"%s\""
msgid "Lists all the available API keys."
msgstr "Lista todas las claves de API disponibles."
msgid "Tells if only enabled API keys should be returned."
msgstr "Define si sólo las claves de API habilitadas deben ser devueltas."
msgid "Key"
msgstr "Clave"
msgid "Is enabled"
msgstr "Está habilitada"
msgid "Expiration date"
msgstr "Fecha de caducidad"
#, php-format
msgid ""
"Generates a character set sample just by shuffling the default one, \"%s\". "
"Then it can be set in the SHORTCODE_CHARS environment variable"
msgstr ""
"Genera un grupo de caracteres simplemente mexclando el grupo por defecto \"%s"
"\". Después puede ser utilizado en la variable de entrono SHORTCODE_CHARS"
#, php-format
msgid "Character set: \"%s\""
msgstr "Grupo de caracteres: \"%s\""
msgid ""
"Generates a random secret string that can be used for JWT token encryption"
msgstr ""
"Genera una cadena de caracteres aleatoria que puede ser usada para cifrar "
"tokens JWT"
#, php-format
msgid "Secret key: \"%s\""
msgstr "Clave secreta: \"%s\""
msgid ""
"Processes and generates the previews for every URL, improving performance "
"for later web requests."
msgstr ""
"Procesa y genera las vistas previas para cada URL, mejorando el rendimiento "
"para peticiones web posteriores."
msgid "Finished processing all URLs"
msgstr "Finalizado el procesado de todas las URLs"
#, php-format
msgid "Processing URL %s..."
msgstr "Procesando URL %s..."
msgid " <info>Success!</info>"
msgstr " <info>¡Correcto!</info>"
msgid "Error"
msgstr "Error"
msgid "Generates a short code for provided URL and returns the short URL"
msgstr ""
"Genera un código corto para la URL proporcionada y devuelve la URL acortada"
msgid "The long URL to parse"
msgstr "La URL larga a procesar"
msgid "Tags to apply to the new short URL"
msgstr "Etiquetas a aplicar a la nueva URL acortada"
msgid ""
"The date from which this short URL will be valid. If someone tries to access "
"it before this date, it will not be found."
msgstr ""
"La fecha desde la cual será válida esta URL acortada. Si alguien intenta "
"acceder a ella antes de esta fecha, no será encontrada."
msgid ""
"The date until which this short URL will be valid. If someone tries to "
"access it after this date, it will not be found."
msgstr ""
"La fecha hasta la cual será válida está URL acortada. Si alguien intenta "
"acceder a ella después de esta fecha, no será encontrada."
msgid "If provided, this slug will be used instead of generating a short code"
msgstr ""
"Si se proporciona, este slug será usado en vez de generar un código corto"
msgid "This will limit the number of visits for this short URL."
msgstr "Esto limitará el número de visitas a esta URL acortada."
#, fuzzy
#| msgid "A long URL was not provided. Which URL do you want to shorten?:"
msgid "A long URL was not provided. Which URL do you want to be shortened?"
msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?"
msgid "A URL was not provided!"
msgstr "¡No se ha proporcionado una URL!"
msgid "Processed long URL:"
msgstr "URL larga procesada:"
msgid "Generated short URL:"
msgstr "URL corta generada:"
#, php-format
msgid "Provided URL \"%s\" is invalid. Try with a different one."
msgstr "La URL proporcionada \"%s\" e inválida. Prueba con una diferente."
#, php-format
msgid ""
"Provided slug \"%s\" is already in use by another URL. Try with a different "
"one."
msgstr ""
"El slug proporcionado \"%s\" ya está siendo usado para otra URL. Prueba con "
"uno diferente."
msgid "Returns the detailed visits information for provided short code"
msgstr ""
"Devuelve la información detallada de visitas para el código corto "
"proporcionado"
msgid "The short code which visits we want to get"
msgstr "El código corto del cual queremos obtener las visitas"
msgid "Allows to filter visits, returning only those older than start date"
msgstr ""
"Permite filtrar las visitas, devolviendo sólo aquellas más antiguas que "
"startDate"
msgid "Allows to filter visits, returning only those newer than end date"
msgstr ""
"Permite filtrar las visitas, devolviendo sólo aquellas más nuevas que endDate"
msgid "A short code was not provided. Which short code do you want to use?"
msgstr "No se proporcionó un código corto. ¿Qué código corto deseas usar?"
msgid "Referer"
msgstr "Origen"
msgid "Date"
msgstr "Fecha"
msgid "Remote Address"
msgstr "Dirección remota"
msgid "User agent"
msgstr "Agente de usuario"
msgid "List all short URLs"
msgstr "Listar todas las URLs cortas"
#, php-format
msgid "The first page to list (%s items per page)"
msgstr "La primera página a listar (%s elementos por página)"
msgid ""
"A query used to filter results by searching for it on the longUrl and "
"shortCode fields"
msgstr ""
"Una consulta usada para filtrar el resultado buscándola en los campos "
"longUrl y shortCode"
msgid "A comma-separated list of tags to filter results"
msgstr "Una lista de etiquetas separadas por coma para filtrar el resultado"
msgid ""
"The field from which we want to order by. Pass ASC or DESC separated by a "
"comma"
msgstr ""
"El campo por el cual queremos ordernar. Pasa ASC o DESC separado por una coma"
msgid "Whether to display the tags or not"
msgstr "Si se desea mostrar las etiquetas o no"
msgid "Short code"
msgstr "Código corto"
msgid "Original URL"
msgstr "URL original"
msgid "Date created"
msgstr "Fecha de creación"
msgid "Visits count"
msgstr "Número de visitas"
msgid "Tags"
msgstr "Etiquetas"
msgid "Short codes properly listed"
msgstr "Códigos cortos correctamente listados"
msgid "Continue with page"
msgstr "Continuar con la página"
msgid "Returns the long URL behind a short code"
msgstr "Devuelve la URL larga detrás de un código corto"
msgid "The short code to parse"
msgstr "El código corto a convertir"
msgid "A short code was not provided. Which short code do you want to parse?"
msgstr ""
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?"
msgid "Long URL:"
msgstr "URL larga:"
#, php-format
msgid "Provided short code \"%s\" has an invalid format."
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
#, php-format
msgid "Provided short code \"%s\" could not be found."
msgstr "El código corto proporcionado \"%s\" no ha podido ser encontrado."
msgid "Creates one or more tags."
msgstr "Crea una o más etiquetas."
msgid "The name of the tags to create"
msgstr "El nombre de las etiquetas a crear"
msgid "You have to provide at least one tag name"
msgstr "Debes proporcionar al menos un nombre de etiqueta"
msgid "Tags properly created"
msgstr "Etiquetas correctamente creadas"
msgid "Deletes one or more tags."
msgstr "Elimina una o más etiquetas."
msgid "The name of the tags to delete"
msgstr "El nombre de las etiquetas a eliminar"
msgid "Tags properly deleted"
msgstr "Etiquetas correctamente eliminadas"
msgid "Lists existing tags."
msgstr "Lista las etiquetas existentes."
#, fuzzy
msgid "Name"
msgstr "Nombre"
msgid "No tags yet"
msgstr "Aún no hay etiquetas"
msgid "Renames one existing tag."
msgstr "Renombra una etiqueta existente."
msgid "Current name of the tag."
msgstr "Nombre actual de la etiqueta."
msgid "New name of the tag."
msgstr "Nuevo nombre de la etiqueta."
msgid "Tag properly renamed."
msgstr "Etiqueta correctamente renombrada."
#, php-format
msgid "A tag with name \"%s\" was not found"
msgstr "Una etiqueta con nombre \"%s\" no ha sido encontrada"
msgid "Processes visits where location is not set yet"
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
msgid "Processing IP"
msgstr "Procesando IP"
msgid "Ignored localhost address"
msgstr "Ignorada IP de localhost"
#, php-format
msgid "Address located at \"%s\""
msgstr "Dirección localizada en \"%s\""
msgid "Finished processing all IPs"
msgstr "Finalizado el procesado de todas las IPs"
#~ msgid "You have reached last page"
#~ msgstr "Has alcanzado la última página"
#~ msgid "No URL found for short code \"%s\""
#~ msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
#~ msgid "Created tags"
#~ msgstr "Etiquetas creadas"
#~ msgid "Deleted tags"
#~ msgstr "Etiquetas eliminadas"

View File

@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class DisableKeyCommand extends Command
{
const NAME = 'api-key:disable';
/**
* @var ApiKeyServiceInterface
*/
private $apiKeyService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
{
$this->apiKeyService = $apiKeyService;
$this->translator = $translator;
parent::__construct();
}
public function configure()
{
$this->setName(self::NAME)
->setDescription($this->translator->translate('Disables an API key.'))
->addArgument('apiKey', InputArgument::REQUIRED, $this->translator->translate('The API key to disable'));
}
public function execute(InputInterface $input, OutputInterface $output)
{
$apiKey = $input->getArgument('apiKey');
$io = new SymfonyStyle($input, $output);
try {
$this->apiKeyService->disable($apiKey);
$io->success(sprintf($this->translator->translate('API key "%s" properly disabled'), $apiKey));
} catch (\InvalidArgumentException $e) {
$io->error(sprintf($this->translator->translate('API key "%s" does not exist.'), $apiKey));
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
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;
use Zend\I18n\Translator\TranslatorInterface;
class GenerateKeyCommand extends Command
{
const NAME = 'api-key:generate';
/**
* @var ApiKeyServiceInterface
*/
private $apiKeyService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
{
$this->apiKeyService = $apiKeyService;
$this->translator = $translator;
parent::__construct();
}
public function configure()
{
$this->setName(self::NAME)
->setDescription($this->translator->translate('Generates a new valid API key.'))
->addOption(
'expirationDate',
'e',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('The date in which the API key should expire. Use any valid PHP format.')
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$expirationDate = $input->getOption('expirationDate');
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? new \DateTime($expirationDate) : null);
(new SymfonyStyle($input, $output))->success(
sprintf($this->translator->translate('Generated API key: "%s"'), $apiKey)
);
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
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;
use Zend\I18n\Translator\TranslatorInterface;
class ListKeysCommand extends Command
{
const NAME = 'api-key:list';
/**
* @var ApiKeyServiceInterface
*/
private $apiKeyService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
{
$this->apiKeyService = $apiKeyService;
$this->translator = $translator;
parent::__construct();
}
public function configure()
{
$this->setName(self::NAME)
->setDescription($this->translator->translate('Lists all the available API keys.'))
->addOption(
'enabledOnly',
null,
InputOption::VALUE_NONE,
$this->translator->translate('Tells if only enabled API keys should be returned.')
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$enabledOnly = $input->getOption('enabledOnly');
$list = $this->apiKeyService->listKeys($enabledOnly);
$rows = [];
/** @var ApiKey $row */
foreach ($list as $row) {
$key = $row->getKey();
$expiration = $row->getExpirationDate();
$formatMethod = $this->determineFormatMethod($row);
// Set columns for this row
$rowData = [$formatMethod($key)];
if (! $enabledOnly) {
$rowData[] = $formatMethod($this->getEnabledSymbol($row));
}
$rowData[] = $expiration !== null ? $expiration->format(\DateTime::ATOM) : '-';
$rows[] = $rowData;
}
$io->table(array_filter([
$this->translator->translate('Key'),
! $enabledOnly ? $this->translator->translate('Is enabled') : null,
$this->translator->translate('Expiration date'),
]), $rows);
}
private function determineFormatMethod(ApiKey $apiKey): callable
{
if (! $apiKey->isEnabled()) {
return [$this, 'getErrorString'];
}
return $apiKey->isExpired() ? [$this, 'getWarningString'] : [$this, 'getSuccessString'];
}
/**
* @param string $value
* @return string
*/
private function getErrorString(string $value): string
{
return sprintf('<fg=red>%s</>', $value);
}
/**
* @param string $value
* @return string
*/
private function getSuccessString(string $value): string
{
return sprintf('<info>%s</info>', $value);
}
/**
* @param string $value
* @return string
*/
private function getWarningString(string $value): string
{
return sprintf('<comment>%s</comment>', $value);
}
/**
* @param ApiKey $apiKey
* @return string
*/
private function getEnabledSymbol(ApiKey $apiKey): string
{
return ! $apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++';
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class GenerateCharsetCommand extends Command
{
const NAME = 'config:generate-charset';
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
parent::__construct();
}
public function configure()
{
$this->setName(self::NAME)
->setDescription(sprintf($this->translator->translate(
'Generates a character set sample just by shuffling the default one, "%s". '
. 'Then it can be set in the SHORTCODE_CHARS environment variable'
), UrlShortener::DEFAULT_CHARS));
}
public function execute(InputInterface $input, OutputInterface $output)
{
$charSet = str_shuffle(UrlShortener::DEFAULT_CHARS);
(new SymfonyStyle($input, $output))->success(
\sprintf($this->translator->translate('Character set: "%s"'), $charSet)
);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class GenerateSecretCommand extends Command
{
use StringUtilsTrait;
const NAME = 'config:generate-secret';
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
parent::__construct();
}
public function configure()
{
$this->setName(self::NAME)
->setDescription($this->translator->translate(
'Generates a random secret string that can be used for JWT token encryption'
));
}
public function execute(InputInterface $input, OutputInterface $output)
{
$secret = $this->generateRandomString(32);
(new SymfonyStyle($input, $output))->success(
sprintf($this->translator->translate('Secret key: "%s"'), $secret)
);
}
}

View File

@@ -0,0 +1,235 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Install;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerManagerInterface;
use Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Zend\Config\Writer\WriterInterface;
class InstallCommand extends Command
{
const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
/**
* @var SymfonyStyle
*/
private $io;
/**
* @var ProcessHelper
*/
private $processHelper;
/**
* @var WriterInterface
*/
private $configWriter;
/**
* @var Filesystem
*/
private $filesystem;
/**
* @var ConfigCustomizerManagerInterface
*/
private $configCustomizers;
/**
* @var bool
*/
private $isUpdate;
/**
* InstallCommand constructor.
* @param WriterInterface $configWriter
* @param Filesystem $filesystem
* @param ConfigCustomizerManagerInterface $configCustomizers
* @param bool $isUpdate
* @throws LogicException
*/
public function __construct(
WriterInterface $configWriter,
Filesystem $filesystem,
ConfigCustomizerManagerInterface $configCustomizers,
$isUpdate = false
) {
parent::__construct();
$this->configWriter = $configWriter;
$this->isUpdate = $isUpdate;
$this->filesystem = $filesystem;
$this->configCustomizers = $configCustomizers;
}
public function configure()
{
$this
->setName('shlink:install')
->setDescription('Installs or updates Shlink');
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return void
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function execute(InputInterface $input, OutputInterface $output)
{
$this->io = new SymfonyStyle($input, $output);
$this->io->writeln([
'<info>Welcome to Shlink!!</info>',
'This will guide you through the installation process.',
]);
// Check if a cached config file exists and drop it if so
if ($this->filesystem->exists('data/cache/app_config.php')) {
$this->io->write('Deleting old cached config...');
try {
$this->filesystem->remove('data/cache/app_config.php');
$this->io->writeln(' <info>Success</info>');
} catch (IOException $e) {
$this->io->error(
'Failed! You will have to manually delete the data/cache/app_config.php file to'
. ' get new config applied.'
);
if ($this->io->isVerbose()) {
$this->getApplication()->renderException($e, $output);
}
return;
}
}
// If running update command, ask the user to import previous config
$config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig();
// Ask for custom config params
foreach ([
Plugin\DatabaseConfigCustomizer::class,
Plugin\UrlShortenerConfigCustomizer::class,
Plugin\LanguageConfigCustomizer::class,
Plugin\ApplicationConfigCustomizer::class,
] as $pluginName) {
/** @var Plugin\ConfigCustomizerInterface $configCustomizer */
$configCustomizer = $this->configCustomizers->get($pluginName);
$configCustomizer->process($this->io, $config);
}
// Generate config params files
$this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
$this->io->writeln(['<info>Custom configuration properly generated!</info>', '']);
// If current command is not update, generate database
if (! $this->isUpdate) {
$this->io->write('Initializing database...');
if (! $this->runCommand(
'php vendor/bin/doctrine.php orm:schema-tool:create',
'Error generating database.',
$output
)) {
return;
}
}
// Run database migrations
$this->io->write('Updating database...');
if (! $this->runCommand(
'php vendor/bin/doctrine-migrations migrations:migrate',
'Error updating database.',
$output
)) {
return;
}
// Generate proxies
$this->io->write('Generating proxies...');
if (! $this->runCommand(
'php vendor/bin/doctrine.php orm:generate-proxies',
'Error generating proxies.',
$output
)) {
return;
}
$this->io->success('Installation complete!');
}
/**
* @return CustomizableAppConfig
* @throws RuntimeException
*/
private function importConfig(): CustomizableAppConfig
{
$config = new CustomizableAppConfig();
// Ask the user if he/she wants to import an older configuration
$importConfig = $this->io->confirm('Do you want to import configuration from previous installation?');
if (! $importConfig) {
return $config;
}
// Ask the user for the older shlink path
$keepAsking = true;
do {
$config->setImportedInstallationPath($this->io->ask(
'Previous shlink installation path from which to import config'
));
$configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
$configExists = $this->filesystem->exists($configFile);
if (! $configExists) {
$keepAsking = $this->io->confirm(
'Provided path does not seem to be a valid shlink root path. Do you want to try another path?'
);
}
} while (! $configExists && $keepAsking);
// If after some retries the user has chosen not to test another path, return
if (! $configExists) {
return $config;
}
// Read the config file
$config->exchangeArray(include $configFile);
return $config;
}
/**
* @param string $command
* @param string $errorMessage
* @param OutputInterface $output
* @return bool
* @throws LogicException
* @throws InvalidArgumentException
*/
private function runCommand($command, $errorMessage, OutputInterface $output): bool
{
if ($this->processHelper === null) {
$this->processHelper = $this->getHelper('process');
}
$process = $this->processHelper->run($output, $command);
if ($process->isSuccessful()) {
$this->io->writeln(' <info>Success!</info>');
return true;
}
if ($this->io->isVerbose()) {
return false;
}
$this->io->error($errorMessage . ' Run this command with -vvv to see specific error info.');
return false;
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class GeneratePreviewCommand extends Command
{
const NAME = 'shortcode:process-previews';
/**
* @var PreviewGeneratorInterface
*/
private $previewGenerator;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var ShortUrlServiceInterface
*/
private $shortUrlService;
public function __construct(
ShortUrlServiceInterface $shortUrlService,
PreviewGeneratorInterface $previewGenerator,
TranslatorInterface $translator
) {
$this->shortUrlService = $shortUrlService;
$this->previewGenerator = $previewGenerator;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName(self::NAME)
->setDescription(
$this->translator->translate(
'Processes and generates the previews for every URL, improving performance for later web requests.'
)
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$page = 1;
do {
$shortUrls = $this->shortUrlService->listShortUrls($page);
$page += 1;
foreach ($shortUrls as $shortUrl) {
$this->processUrl($shortUrl->getOriginalUrl(), $output);
}
} while ($page <= $shortUrls->count());
(new SymfonyStyle($input, $output))->success($this->translator->translate('Finished processing all URLs'));
}
protected function processUrl($url, OutputInterface $output)
{
try {
$output->write(\sprintf($this->translator->translate('Processing URL %s...'), $url));
$this->previewGenerator->generatePreview($url);
$output->writeln($this->translator->translate(' <info>Success!</info>'));
} catch (PreviewGenerationException $e) {
$output->writeln(' <error>' . $this->translator->translate('Error') . '</error>');
if ($output->isVerbose()) {
$this->getApplication()->renderException($e, $output);
}
}
}
}

View File

@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\Diactoros\Uri;
use Zend\I18n\Translator\TranslatorInterface;
class GenerateShortcodeCommand extends Command
{
const NAME = 'shortcode:generate';
/**
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var array
*/
private $domainConfig;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
array $domainConfig
) {
$this->urlShortener = $urlShortener;
$this->translator = $translator;
$this->domainConfig = $domainConfig;
parent::__construct(null);
}
public function configure()
{
$this->setName(self::NAME)
->setDescription(
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
)
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
->addOption(
'tags',
't',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
$this->translator->translate('Tags to apply to the new short URL')
)
->addOption('validSince', 's', InputOption::VALUE_REQUIRED, $this->translator->translate(
'The date from which this short URL will be valid. '
. 'If someone tries to access it before this date, it will not be found.'
))
->addOption('validUntil', 'u', InputOption::VALUE_REQUIRED, $this->translator->translate(
'The date until which this short URL will be valid. '
. 'If someone tries to access it after this date, it will not be found.'
))
->addOption('customSlug', 'c', InputOption::VALUE_REQUIRED, $this->translator->translate(
'If provided, this slug will be used instead of generating a short code'
))
->addOption('maxVisits', 'm', InputOption::VALUE_REQUIRED, $this->translator->translate(
'This will limit the number of visits for this short URL.'
));
}
public function interact(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$longUrl = $input->getArgument('longUrl');
if (! empty($longUrl)) {
return;
}
$longUrl = $io->ask(
$this->translator->translate('A long URL was not provided. Which URL do you want to be shortened?')
);
if (! empty($longUrl)) {
$input->setArgument('longUrl', $longUrl);
}
}
public function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$longUrl = $input->getArgument('longUrl');
if (empty($longUrl)) {
$io->error($this->translator->translate('A URL was not provided!'));
return;
}
$tags = $input->getOption('tags');
$processedTags = [];
foreach ($tags as $key => $tag) {
$explodedTags = \explode(',', $tag);
$processedTags = \array_merge($processedTags, $explodedTags);
}
$tags = $processedTags;
$customSlug = $input->getOption('customSlug');
$maxVisits = $input->getOption('maxVisits');
try {
$shortCode = $this->urlShortener->urlToShortCode(
new Uri($longUrl),
$tags,
$this->getOptionalDate($input, 'validSince'),
$this->getOptionalDate($input, 'validUntil'),
$customSlug,
$maxVisits !== null ? (int) $maxVisits : null
);
$shortUrl = (new Uri())->withPath($shortCode)
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
$io->writeln([
\sprintf('%s <info>%s</info>', $this->translator->translate('Processed long URL:'), $longUrl),
\sprintf('%s <info>%s</info>', $this->translator->translate('Generated short URL:'), $shortUrl),
]);
} catch (InvalidUrlException $e) {
$io->error(\sprintf(
$this->translator->translate('Provided URL "%s" is invalid. Try with a different one.'),
$longUrl
));
} catch (NonUniqueSlugException $e) {
$io->error(\sprintf(
$this->translator->translate(
'Provided slug "%s" is already in use by another URL. Try with a different one.'
),
$customSlug
));
}
}
private function getOptionalDate(InputInterface $input, string $fieldName)
{
$since = $input->getOption($fieldName);
return $since !== null ? new \DateTime($since) : null;
}
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class GetVisitsCommand extends Command
{
const NAME = 'shortcode:visits';
/**
* @var VisitsTrackerInterface
*/
private $visitsTracker;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
{
$this->visitsTracker = $visitsTracker;
$this->translator = $translator;
parent::__construct();
}
public function configure()
{
$this->setName(self::NAME)
->setDescription(
$this->translator->translate('Returns the detailed visits information for provided short code')
)
->addArgument(
'shortCode',
InputArgument::REQUIRED,
$this->translator->translate('The short code which visits we want to get')
)
->addOption(
'startDate',
's',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('Allows to filter visits, returning only those older than start date')
)
->addOption(
'endDate',
'e',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('Allows to filter visits, returning only those newer than end date')
);
}
public function interact(InputInterface $input, OutputInterface $output)
{
$shortCode = $input->getArgument('shortCode');
if (! empty($shortCode)) {
return;
}
$io = new SymfonyStyle($input, $output);
$shortCode = $io->ask(
$this->translator->translate('A short code was not provided. Which short code do you want to use?')
);
if (! empty($shortCode)) {
$input->setArgument('shortCode', $shortCode);
}
}
public function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$shortCode = $input->getArgument('shortCode');
$startDate = $this->getDateOption($input, 'startDate');
$endDate = $this->getDateOption($input, 'endDate');
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
$rows = [];
foreach ($visits as $row) {
$rowData = $row->jsonSerialize();
// Unset location info
unset($rowData['visitLocation']);
$rows[] = \array_values($rowData);
}
$io->table([
$this->translator->translate('Referer'),
$this->translator->translate('Date'),
$this->translator->translate('Remote Address'),
$this->translator->translate('User agent'),
], $rows);
}
protected function getDateOption(InputInterface $input, $key)
{
$value = $input->getOption($key);
if (! empty($value)) {
$value = new \DateTime($value);
}
return $value;
}
}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
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;
use Zend\I18n\Translator\TranslatorInterface;
class ListShortcodesCommand extends Command
{
use PaginatorUtilsTrait;
const NAME = 'shortcode:list';
/**
* @var ShortUrlServiceInterface
*/
private $shortUrlService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
{
$this->shortUrlService = $shortUrlService;
$this->translator = $translator;
parent::__construct();
}
public function configure()
{
$this->setName(self::NAME)
->setDescription($this->translator->translate('List all short URLs'))
->addOption(
'page',
'p',
InputOption::VALUE_OPTIONAL,
sprintf(
$this->translator->translate('The first page to list (%s items per page)'),
PaginableRepositoryAdapter::ITEMS_PER_PAGE
),
1
)
->addOption(
'searchTerm',
's',
InputOption::VALUE_OPTIONAL,
$this->translator->translate(
'A query used to filter results by searching for it on the longUrl and shortCode fields'
)
)
->addOption(
'tags',
't',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('A comma-separated list of tags to filter results')
)
->addOption(
'orderBy',
'o',
InputOption::VALUE_OPTIONAL,
$this->translator->translate(
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
)
)
->addOption(
'showTags',
null,
InputOption::VALUE_NONE,
$this->translator->translate('Whether to display the tags or not')
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$page = (int) $input->getOption('page');
$searchTerm = $input->getOption('searchTerm');
$tags = $input->getOption('tags');
$tags = ! empty($tags) ? \explode(',', $tags) : [];
$showTags = $input->getOption('showTags');
do {
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
$page++;
$headers = [
$this->translator->translate('Short code'),
$this->translator->translate('Original URL'),
$this->translator->translate('Date created'),
$this->translator->translate('Visits count'),
];
if ($showTags) {
$headers[] = $this->translator->translate('Tags');
}
$rows = [];
foreach ($result as $row) {
$shortUrl = $row->jsonSerialize();
if ($showTags) {
$shortUrl['tags'] = [];
foreach ($row->getTags() as $tag) {
$shortUrl['tags'][] = $tag->getName();
}
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
} else {
unset($shortUrl['tags']);
}
$rows[] = \array_values($shortUrl);
}
$io->table($headers, $rows);
if ($this->isLastPage($result)) {
$continue = false;
$io->success($this->translator->translate('Short codes properly listed'));
} else {
$continue = $io->confirm(
\sprintf($this->translator->translate('Continue with page') . ' <options=bold>%s</>?', $page),
false
);
}
} while ($continue);
}
private function processOrderBy(InputInterface $input)
{
$orderBy = $input->getOption('orderBy');
if (empty($orderBy)) {
return null;
}
$orderBy = explode(',', $orderBy);
return count($orderBy) === 1 ? $orderBy[0] : [$orderBy[0] => $orderBy[1]];
}
}

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class ResolveUrlCommand extends Command
{
const NAME = 'shortcode:parse';
/**
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
{
$this->urlShortener = $urlShortener;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName(self::NAME)
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
->addArgument(
'shortCode',
InputArgument::REQUIRED,
$this->translator->translate('The short code to parse')
);
}
public function interact(InputInterface $input, OutputInterface $output)
{
$shortCode = $input->getArgument('shortCode');
if (! empty($shortCode)) {
return;
}
$io = new SymfonyStyle($input, $output);
$shortCode = $io->ask(
$this->translator->translate('A short code was not provided. Which short code do you want to parse?')
);
if (! empty($shortCode)) {
$input->setArgument('shortCode', $shortCode);
}
}
public function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$shortCode = $input->getArgument('shortCode');
try {
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
$output->writeln(\sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $longUrl));
} catch (InvalidShortCodeException $e) {
$io->error(
\sprintf($this->translator->translate('Provided short code "%s" has an invalid format.'), $shortCode)
);
} catch (EntityDoesNotExistException $e) {
$io->error(
\sprintf($this->translator->translate('Provided short code "%s" could not be found.'), $shortCode)
);
}
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\Core\Service\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;
use Zend\I18n\Translator\TranslatorInterface;
class CreateTagCommand extends Command
{
const NAME = 'tag:create';
/**
* @var TagServiceInterface
*/
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
}
protected function configure()
{
$this
->setName(self::NAME)
->setDescription($this->translator->translate('Creates one or more tags.'))
->addOption(
'name',
't',
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
$this->translator->translate('The name of the tags to create')
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$tagNames = $input->getOption('name');
if (empty($tagNames)) {
$io->warning($this->translator->translate('You have to provide at least one tag name'));
return;
}
$this->tagService->createTags($tagNames);
$io->success($this->translator->translate('Tags properly created'));
}
}

View File

@@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\Core\Service\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;
use Zend\I18n\Translator\TranslatorInterface;
class DeleteTagsCommand extends Command
{
const NAME = 'tag:delete';
/**
* @var TagServiceInterface
*/
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
}
protected function configure()
{
$this
->setName(self::NAME)
->setDescription($this->translator->translate('Deletes one or more tags.'))
->addOption(
'name',
't',
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
$this->translator->translate('The name of the tags to delete')
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$tagNames = $input->getOption('name');
if (empty($tagNames)) {
$io->warning($this->translator->translate('You have to provide at least one tag name'));
return;
}
$this->tagService->deleteTags($tagNames);
$io->success($this->translator->translate('Tags properly deleted'));
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class ListTagsCommand extends Command
{
const NAME = 'tag:list';
/**
* @var TagServiceInterface
*/
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
}
protected function configure()
{
$this
->setName(self::NAME)
->setDescription($this->translator->translate('Lists existing tags.'));
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$io->table([$this->translator->translate('Name')], $this->getTagsRows());
}
private function getTagsRows()
{
$tags = $this->tagService->listTags();
if (empty($tags)) {
return [[$this->translator->translate('No tags yet')]];
}
return \array_map(function (Tag $tag) {
return [$tag->getName()];
}, $tags);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class RenameTagCommand extends Command
{
const NAME = 'tag:rename';
/**
* @var TagServiceInterface
*/
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
}
protected function configure()
{
$this
->setName(self::NAME)
->setDescription($this->translator->translate('Renames one existing tag.'))
->addArgument('oldName', InputArgument::REQUIRED, $this->translator->translate('Current name of the tag.'))
->addArgument('newName', InputArgument::REQUIRED, $this->translator->translate('New name of the tag.'));
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$oldName = $input->getArgument('oldName');
$newName = $input->getArgument('newName');
try {
$this->tagService->renameTag($oldName, $newName);
$io->success($this->translator->translate('Tag properly renamed.'));
} catch (EntityDoesNotExistException $e) {
$io->error(\sprintf($this->translator->translate('A tag with name "%s" was not found'), $oldName));
}
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Service\IpLocationResolverInterface;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Zend\I18n\Translator\TranslatorInterface;
class ProcessVisitsCommand extends Command
{
const LOCALHOST = '127.0.0.1';
const NAME = 'visit:process';
/**
* @var VisitServiceInterface
*/
private $visitService;
/**
* @var IpLocationResolverInterface
*/
private $ipLocationResolver;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(
VisitServiceInterface $visitService,
IpLocationResolverInterface $ipLocationResolver,
TranslatorInterface $translator
) {
$this->visitService = $visitService;
$this->ipLocationResolver = $ipLocationResolver;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName(self::NAME)
->setDescription(
$this->translator->translate('Processes visits where location is not set yet')
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$io = new SymfonyStyle($input, $output);
$visits = $this->visitService->getUnlocatedVisits();
foreach ($visits as $visit) {
$ipAddr = $visit->getRemoteAddr();
$io->write(sprintf('%s <info>%s</info>', $this->translator->translate('Processing IP'), $ipAddr));
if ($ipAddr === self::LOCALHOST) {
$io->writeln(
sprintf(' (<comment>%s</comment>)', $this->translator->translate('Ignored localhost address'))
);
continue;
}
try {
$result = $this->ipLocationResolver->resolveIpLocation($ipAddr);
$location = new VisitLocation();
$location->exchangeArray($result);
$visit->setVisitLocation($location);
$this->visitService->saveVisit($visit);
$io->writeln(sprintf(
' (' . $this->translator->translate('Address located at "%s"') . ')',
$location->getCityName()
));
} catch (WrongIpException $e) {
continue;
}
}
$io->success($this->translator->translate('Finished processing all IPs'));
}
}

View File

@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;
use Zend\Config\Factory;
use Zend\Stdlib\Glob;
class ConfigProvider
{
public function __invoke()
{
return Factory::fromFiles(Glob::glob(__DIR__ . '/../config/{,*.}config.php', Glob::GLOB_BRACE));
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Symfony\Component\Console\Application as CliApp;
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class ApplicationFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return CliApp
* @throws NotFoundExceptionInterface
* @throws ContainerExceptionInterface
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when creating a service.
* @throws ContainerException if any other error occurs
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null): CliApp
{
$config = $container->get('config')['cli'];
$appOptions = $container->get(AppOptions::class);
$translator = $container->get(Translator::class);
$translator->setLocale($config['locale']);
$commands = $config['commands'] ?? [];
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());
$app->setCommandLoader(new ContainerCommandLoader($container, $commands));
return $app;
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerManager;
use Shlinkio\Shlink\CLI\Install\Plugin;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Filesystem\Filesystem;
use Zend\Config\Writer\PhpArray;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
use Zend\ServiceManager\Factory\InvokableFactory;
class InstallApplicationFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @throws LogicException
* @throws ServiceNotFoundException if unable to resolve the service.
* @throws ServiceNotCreatedException if an exception is raised when
* creating a service.
* @throws ContainerException if any other error occurs
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$isUpdate = $options !== null && isset($options['isUpdate']) ? (bool) $options['isUpdate'] : false;
$app = new Application();
$command = new InstallCommand(
new PhpArray(),
$container->get(Filesystem::class),
new ConfigCustomizerManager($container, ['factories' => [
Plugin\DatabaseConfigCustomizer::class => ConfigAbstractFactory::class,
Plugin\UrlShortenerConfigCustomizer::class => InvokableFactory::class,
Plugin\LanguageConfigCustomizer::class => InvokableFactory::class,
Plugin\ApplicationConfigCustomizer::class => InvokableFactory::class,
]]),
$isUpdate
);
$app->add($command);
$app->setDefaultCommand($command->getName());
return $app;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Install;
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerInterface;
use Zend\ServiceManager\AbstractPluginManager;
class ConfigCustomizerManager extends AbstractPluginManager implements ConfigCustomizerManagerInterface
{
protected $instanceOf = ConfigCustomizerInterface::class;
}

View File

@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Install;
use Psr\Container\ContainerInterface;
interface ConfigCustomizerManagerInterface extends ContainerInterface
{
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
use Symfony\Component\Console\Style\SymfonyStyle;
class ApplicationConfigCustomizer implements ConfigCustomizerInterface
{
use StringUtilsTrait;
/**
* @param SymfonyStyle $io
* @param CustomizableAppConfig $appConfig
* @return void
*/
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
{
$io->title('APPLICATION');
if ($appConfig->hasApp() && $io->confirm('Do you want to keep imported application config?')) {
return;
}
$validator = function ($value) {
return $value;
};
$appConfig->setApp([
'SECRET' => $io->ask(
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
null,
$validator
) ?: $this->generateRandomString(32),
'DISABLE_TRACK_PARAM' => $io->ask(
'Provide a parameter name that you will be able to use to disable tracking on specific request to '
. 'short URLs (leave empty and this feature won\'t be enabled)',
null,
$validator
),
]);
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
interface ConfigCustomizerInterface
{
/**
* @param SymfonyStyle $io
* @param CustomizableAppConfig $appConfig
* @return void
*/
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig);
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
class DatabaseConfigCustomizer implements ConfigCustomizerInterface
{
const DATABASE_DRIVERS = [
'MySQL' => 'pdo_mysql',
'PostgreSQL' => 'pdo_pgsql',
'SQLite' => 'pdo_sqlite',
];
/**
* @var Filesystem
*/
private $filesystem;
public function __construct(Filesystem $filesystem)
{
$this->filesystem = $filesystem;
}
/**
* @param SymfonyStyle $io
* @param CustomizableAppConfig $appConfig
* @return void
* @throws IOException
*/
public function process(SymfonyStyle $io, CustomizableAppConfig $appConfig)
{
$io->title('DATABASE');
if ($appConfig->hasDatabase() && $io->confirm('Do you want to keep imported database config?')) {
// If the user selected to keep DB config and is configured to use sqlite, copy DB file
if ($appConfig->getDatabase()['DRIVER'] === self::DATABASE_DRIVERS['SQLite']) {
try {
$this->filesystem->copy(
$appConfig->getImportedInstallationPath() . '/' . CustomizableAppConfig::SQLITE_DB_PATH,
CustomizableAppConfig::SQLITE_DB_PATH
);
} catch (IOException $e) {
$io->error('It wasn\'t possible to import the SQLite database');
throw $e;
}
}
return;
}
// Select database type
$params = [];
$databases = \array_keys(self::DATABASE_DRIVERS);
$dbType = $io->choice('Select database type', $databases, $databases[0]);
$params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
// Ask for connection params if database is not SQLite
if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
$params['NAME'] = $io->ask('Database name', 'shlink');
$params['USER'] = $io->ask('Database username');
$params['PASSWORD'] = $io->ask('Database password');
$params['HOST'] = $io->ask('Database host', 'localhost');
$params['PORT'] = $io->ask('Database port', $this->getDefaultDbPort($params['DRIVER']));
}
$appConfig->setDatabase($params);
}
private function getDefaultDbPort(string $driver): string
{
return $driver === 'pdo_mysql' ? '3306' : '5432';
}
}

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