Compare commits

..

192 Commits

Author SHA1 Message Date
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
222 changed files with 6965 additions and 1210 deletions

2
.gitignore vendored
View File

@@ -3,3 +3,5 @@ build
composer.lock
vendor/
.env
data/database.sqlite
docs/swagger-ui

View File

@@ -1,7 +1,8 @@
<?php
namespace PHPSTORM_META;
use Interop\Container\ContainerInterface;
use Psr\Container\ContainerInterface;
use Zend\ServiceManager\ServiceManager;
/**
* PhpStorm Container Interop code completion
@@ -16,4 +17,7 @@ $STATIC_METHOD_TYPES = [
ContainerInterface::get('') => [
'' == '@',
],
ServiceManager::build('') => [
'' == '@',
],
];

View File

@@ -1,63 +1,155 @@
## CHANGELOG
### 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/acelaya/url-shortener/issues/46)
* [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/acelaya/url-shortener/issues/32)
* [14: https://github.com/shlinkio/shlink/issues/14](https://github.com/acelaya/url-shortener/issues/14)
* [41: Cache the "short code" => "URL" map to prevent extra DB hits](https://github.com/acelaya/url-shortener/issues/41)
* [13: Improve REST authentication](https://github.com/acelaya/url-shortener/issues/13)
* [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/acelaya/url-shortener/issues/39)
* [42: Make REST endpoints that need to find something return a 404 when "something" is not found](https://github.com/acelaya/url-shortener/issues/42)
* [35: Make CLI commands to use the same PHP namespace as the one used for the command name](https://github.com/acelaya/url-shortener/issues/35)
* [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/acelaya/url-shortener/issues/40)
* [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/acelaya/url-shortener/issues/33)
* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/acelaya/url-shortener/issues/15)
* [23: Translate application literals](https://github.com/acelaya/url-shortener/issues/23)
* [21: Allow to filter visits by date range](https://github.com/acelaya/url-shortener/issues/21)
* [22: Save visits locations data on a visit_locations table](https://github.com/acelaya/url-shortener/issues/22)
* [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/acelaya/url-shortener/issues/20)
* [11: Separate code into multiple modules](https://github.com/acelaya/url-shortener/issues/11)
* [18: Group routable middleware in an Action namespace](https://github.com/acelaya/url-shortener/issues/18)
* [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/acelaya/url-shortener/issues/36)
* [4: Installation steps](https://github.com/acelaya/url-shortener/issues/4)
* [6: Remove dependency on expressive helpers package](https://github.com/acelaya/url-shortener/issues/6)
* [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/acelaya/url-shortener/issues/30)
* [12: Improve code coverage](https://github.com/acelaya/url-shortener/issues/12)
* [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/acelaya/url-shortener/issues/25)
* [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/acelaya/url-shortener/issues/19)
* [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/acelaya/url-shortener/issues/24)
* [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/acelaya/url-shortener/issues/9)
* [8: Create a REST API](https://github.com/acelaya/url-shortener/issues/8)
* [10: Add more CLI functionality](https://github.com/acelaya/url-shortener/issues/10)
* [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/acelaya/url-shortener/issues/5)
* [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

11
bin/cli
View File

@@ -2,16 +2,7 @@
<?php
use Interop\Container\ContainerInterface;
use Symfony\Component\Console\Application as CliApp;
use Symfony\Component\Console\Application;
use Zend\I18n\Translator\Translator;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
/** @var Translator $translator */
$translator = $container->get('translator');
$translator->setLocale(env('CLI_LOCALE', 'en'));
/** @var Application $app */
$app = $container->get(CliApp::class);
$app->run();
$container->get(CliApp::class)->run();

19
bin/install Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env php
<?php
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Filesystem\Filesystem;
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,
QuestionHelper::class => InvokableFactory::class,
]]);
$container->build(Application::class)->run();

19
bin/update Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env php
<?php
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Filesystem\Filesystem;
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,
QuestionHelper::class => InvokableFactory::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 -r vendor
rm 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 -r build
rm -f 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,43 +1,49 @@
{
"name": "shlinkio/shlink",
"type": "project",
"homepage": "http://shlink.io",
"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",
"homepage": "https://www.alejandrocelaya.com",
"email": "alejandro@alejandrocelaya.com"
}
],
"require": {
"php": "^5.6 || ^7.0",
"zendframework/zend-expressive": "^1.0",
"zendframework/zend-expressive-fastroute": "^1.1",
"zendframework/zend-expressive-twigrenderer": "^1.0",
"zendframework/zend-stdlib": "^2.7",
"zendframework/zend-expressive": "^2.0",
"zendframework/zend-expressive-fastroute": "^2.0",
"zendframework/zend-expressive-twigrenderer": "^1.4",
"zendframework/zend-stdlib": "^3.0",
"zendframework/zend-servicemanager": "^3.0",
"zendframework/zend-paginator": "^2.6",
"zendframework/zend-config": "^2.6",
"zendframework/zend-config": "^3.0",
"zendframework/zend-i18n": "^2.7",
"mtymek/expressive-config-manager": "^0.4",
"acelaya/zsm-annotated-services": "^0.2.0",
"zendframework/zend-config-aggregator": "^0.1",
"acelaya/zsm-annotated-services": "^1.0",
"acelaya/ze-content-based-error-handler": "^2.0",
"doctrine/orm": "^2.5",
"guzzlehttp/guzzle": "^6.2",
"symfony/console": "^3.0",
"symfony/process": "^3.0",
"symfony/filesystem": "^3.0",
"firebase/php-jwt": "^4.0",
"monolog/monolog": "^1.21",
"theorchard/monolog-cascade": "^0.4",
"endroid/qrcode": "^1.7"
"endroid/qrcode": "^1.7",
"mikehaertl/phpwkhtmltopdf": "^2.2",
"doctrine/migrations": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "^5.0",
"phpunit/phpunit": "^5.7 || ^6.0",
"squizlabs/php_codesniffer": "^2.3",
"roave/security-advisories": "dev-master",
"filp/whoops": "^2.0",
"symfony/var-dumper": "^3.0",
"vlucas/phpdotenv": "^2.2"
"vlucas/phpdotenv": "^2.2",
"zendframework/zend-expressive-tooling": "^0.4"
},
"autoload": {
"psr-4": {
@@ -68,5 +74,8 @@
"serve": "php -S 0.0.0.0:8000 -t public/",
"test": "phpunit --coverage-clover build/clover.xml",
"pretty-test": "phpunit --coverage-html build/coverage"
},
"config": {
"process-timeout": 0
}
}

View File

@@ -1,10 +1,12 @@
<?php
use Shlinkio\Shlink\Common;
return [
'app_options' => [
'name' => 'Shlink',
'version' => '1.1.0',
'secret_key' => env('SECRET_KEY'),
'version' => '1.2.0',
'secret_key' => Common\env('SECRET_KEY'),
],
];

View File

@@ -1,23 +1,23 @@
<?php
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
use Shlinkio\Shlink\Common\Factory\EmptyResponseImplicitOptionsMiddlewareFactory;
use Zend\Expressive;
use Zend\Expressive\Container;
use Zend\Expressive\Middleware;
use Zend\Expressive\Router;
use Zend\Expressive\Template;
use Zend\Expressive\Twig;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\Stratigility\Middleware\ErrorHandler;
return [
'dependencies' => [
'factories' => [
Expressive\Application::class => Container\ApplicationFactory::class,
Router\FastRouteRouter::class => InvokableFactory::class,
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
],
'aliases' => [
Router\RouterInterface::class => Router\FastRouteRouter::class,
'Zend\Expressive\FinalHandler' => ContentBasedErrorHandler::class,
\Twig_Environment::class => Twig\TwigEnvironmentFactory::class,
Router\RouterInterface::class => Router\FastRouteRouterFactory::class,
ErrorHandler::class => Container\ErrorHandlerFactory::class,
Middleware\ImplicitOptionsMiddleware::class => EmptyResponseImplicitOptionsMiddlewareFactory::class,
],
],

View File

@@ -1,4 +1,6 @@
<?php
use Shlinkio\Shlink\Common;
return [
'entity_manager' => [
@@ -6,14 +8,10 @@ return [
'proxies_dir' => 'data/proxies',
],
'connection' => [
'driver' => 'pdo_mysql',
'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'dbname' => env('DB_NAME', 'shlink'),
'user' => Common\env('DB_USER'),
'password' => Common\env('DB_PASSWORD'),
'dbname' => Common\env('DB_NAME', 'shlink'),
'charset' => 'utf8',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES 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

@@ -1,6 +1,5 @@
<?php
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
use Zend\Expressive\Container\WhoopsErrorHandlerFactory;
use Zend\Expressive\Container\WhoopsErrorResponseGeneratorFactory;
return [
'dependencies' => [
@@ -21,7 +20,7 @@ return [
'error_handler' => [
'plugins' => [
'factories' => [
ContentBasedErrorHandler::DEFAULT_CONTENT => WhoopsErrorHandlerFactory::class,
'text/html' => WhoopsErrorResponseGeneratorFactory::class,
],
],
],

View File

@@ -1,19 +1,51 @@
<?php
use Zend\Expressive\Container\ApplicationFactory;
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' => [
'pre-routing' => [
'middleware' => [
ErrorHandler::class,
LocaleMiddleware::class,
],
'priority' => 11,
],
'pre-routing-rest' => [
'path' => '/rest',
'middleware' => [
PathVersionMiddleware::class,
],
'priority' => 11,
],
'routing' => [
'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE,
Expressive\Application::ROUTING_MIDDLEWARE,
],
'priority' => 10,
],
'rest' => [
'path' => '/rest',
'middleware' => [
CrossDomainMiddleware::class,
Expressive\Middleware\ImplicitOptionsMiddleware::class,
BodyParserMiddleware::class,
CheckAuthenticationMiddleware::class,
],
'priority' => 5,
],
'post-routing' => [
'middleware' => [
ApplicationFactory::DISPATCH_MIDDLEWARE,
Expressive\Application::DISPATCH_MIDDLEWARE,
],
'priority' => 1,
],

View File

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

View File

@@ -0,0 +1,8 @@
<?php
return [
'preview_generation' => [
'files_location' => 'data/cache',
],
];

View File

@@ -0,0 +1,13 @@
<?php
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,8 +1,10 @@
<?php
use Shlinkio\Shlink\Common;
return [
'translator' => [
'locale' => env('DEFAULT_LOCALE', 'en'),
'locale' => Common\env('DEFAULT_LOCALE', 'en'),
],
];

View File

@@ -1,14 +1,15 @@
<?php
use Shlinkio\Shlink\Common;
use Shlinkio\Shlink\Core\Service\UrlShortener;
return [
'url_shortener' => [
'domain' => [
'schema' => env('SHORTENED_URL_SCHEMA', 'http'),
'hostname' => env('SHORTENED_URL_HOSTNAME'),
'schema' => Common\env('SHORTENED_URL_SCHEMA', 'http'),
'hostname' => Common\env('SHORTENED_URL_HOSTNAME'),
],
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
'shortcode_chars' => Common\env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
],
];

View File

@@ -1,10 +1,10 @@
<?php
use Acelaya\ExpressiveErrorHandler;
use Shlinkio\Shlink\CLI;
use Shlinkio\Shlink\Common;
use Shlinkio\Shlink\Core;
use Shlinkio\Shlink\Rest;
use Zend\Expressive\ConfigManager\ConfigManager;
use Zend\Expressive\ConfigManager\ZendConfigProvider;
use Zend\ConfigAggregator;
/**
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
@@ -15,10 +15,11 @@ use Zend\Expressive\ConfigManager\ZendConfigProvider;
* Obviously, if you use closures in your config you can't cache it.
*/
return (new ConfigManager([
return (new ConfigAggregator\ConfigAggregator([
ExpressiveErrorHandler\ConfigProvider::class,
Common\ConfigProvider::class,
Core\ConfigProvider::class,
CLI\ConfigProvider::class,
Rest\ConfigProvider::class,
new ZendConfigProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
new ConfigAggregator\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
], 'data/cache/app_config.php'))->getMergedConfig();

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

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

@@ -0,0 +1,87 @@
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/php7.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

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

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

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;
}
}

View File

@@ -0,0 +1,39 @@
<?php
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,80 @@
<?php
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');
}
}

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

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,7 @@
{
"name": "Authorization",
"in": "header",
"description": "The authorization token with Bearer type",
"required": true,
"type": "string"
}

View File

@@ -0,0 +1,55 @@
{
"post": {
"tags": [
"Authentication"
],
"summary": "Perform authentication",
"description": "Performs an authentication",
"parameters": [
{
"name": "apiKey",
"in": "formData",
"description": "The API key to authenticate with",
"required": true,
"type": "string"
}
],
"responses": {
"200": {
"description": "The authentication worked.",
"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.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"401": {
"description": "The API key is incorrect, is disabled or has expired.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}

View File

@@ -0,0 +1,182 @@
{
"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,
"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,
"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,
"type": "array",
"items": {
"type": "string"
}
},
{
"name": "orderBy",
"in": "query",
"description": "The field from which you want to order the result. (Since v1.3.0)",
"enum": [
"originalUrl",
"shortCode",
"dateCreated",
"visits"
],
"required": false,
"type": "string"
},
{
"$ref": "../parameters/Authorization.json"
}
],
"responses": {
"200": {
"description": "The list of short URLs",
"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.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"post": {
"tags": [
"ShortCodes"
],
"summary": "Create short URL",
"description": "Creates a new short code",
"parameters": [
{
"name": "longUrl",
"in": "formData",
"description": "The URL to parse",
"required": true,
"type": "string"
},
{
"name": "tags",
"in": "formData",
"description": "The URL to parse",
"required": false,
"type": "array",
"items": {
"type": "string"
}
},
{
"$ref": "../parameters/Authorization.json"
}
],
"responses": {
"200": {
"description": "The result of parsing the long URL",
"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.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}

View File

@@ -0,0 +1,58 @@
{
"get": {
"tags": [
"ShortCodes"
],
"summary": "Parse short code",
"description": "Get the long URL behind a short code.",
"parameters": [
{
"name": "shortCode",
"in": "path",
"type": "string",
"description": "The short code to resolve.",
"required": true
},
{
"$ref": "../parameters/Authorization.json"
}
],
"responses": {
"200": {
"description": "The long URL behind a short code.",
"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.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"404": {
"description": "No URL was found for provided short code.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}

View File

@@ -0,0 +1,74 @@
{
"put": {
"tags": [
"ShortCodes",
"Tags"
],
"summary": "Edit tags on short URL",
"description": "Edit the tags on provided short code.",
"parameters": [
{
"name": "shortCode",
"in": "path",
"type": "string",
"description": "The shortCode in which we want to edit tags.",
"required": true
},
{
"name": "tags",
"in": "formData",
"type": "array",
"items": {
"type": "string"
},
"description": "The list of tags to set to the short URL.",
"required": true
},
{
"$ref": "../parameters/Authorization.json"
}
],
"responses": {
"200": {
"description": "List of tags.",
"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.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"404": {
"description": "No short URL was found for provided short code.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}

View File

@@ -0,0 +1,81 @@
{
"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",
"type": "string",
"description": "The shortCode from which we want to get the visits.",
"required": true
},
{
"$ref": "../parameters/Authorization.json"
}
],
"responses": {
"200": {
"description": "List of visits.",
"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.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}

View File

@@ -0,0 +1,193 @@
{
"get": {
"tags": [
"Tags"
],
"summary": "List existing tags",
"description": "Returns the list of all tags used in any short URL, ordered by name",
"parameters": [
{
"$ref": "../parameters/Authorization.json"
}
],
"responses": {
"200": {
"description": "The list of tags",
"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.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"post": {
"tags": [
"Tags"
],
"summary": "Create tags",
"description": "Provided a list of tags, creates all that do not yet exist",
"parameters": [
{
"$ref": "../parameters/Authorization.json"
},
{
"name": "tags[]",
"in": "formData",
"description": "The list of tag names to create",
"required": true,
"type": "array"
}
],
"responses": {
"200": {
"description": "The list of tags",
"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.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"put": {
"tags": [
"Tags"
],
"summary": "Rename tag",
"description": "Renames one existing tag",
"parameters": [
{
"$ref": "../parameters/Authorization.json"
},
{
"name": "oldName",
"in": "formData",
"description": "Current name of the tag",
"required": true,
"type": "string"
},
{
"name": "newName",
"in": "formData",
"description": "New name of the tag",
"required": true,
"type": "string"
}
],
"responses": {
"204": {
"description": "The tag has been properly renamed"
},
"400": {
"description": "You have not provided either the oldName or the newName params.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"404": {
"description": "There's no tag found with the name provided in oldName param.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"delete": {
"tags": [
"Tags"
],
"summary": "Delete tags",
"description": "Deletes provided list of tags",
"parameters": [
{
"$ref": "../parameters/Authorization.json"
},
{
"name": "tags[]",
"in": "query",
"description": "The names of the tags to delete",
"required": true,
"type": "array"
}
],
"responses": {
"204": {
"description": "Tags properly deleted"
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}

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

@@ -0,0 +1,44 @@
{
"swagger": "2.0",
"info": {
"title": "Shlink",
"description": "Shlink, the self-hosted URL shortener",
"version": "1.0"
},
"schemes": [
"http",
"https"
],
"basePath": "/rest",
"produces": [
"application/json"
],
"consumes": [
"application/x-www-form-urlencoded",
"application/json"
],
"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"
}
}
}

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

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

View File

@@ -14,12 +14,17 @@ return [
Command\Shortcode\ResolveUrlCommand::class => AnnotatedFactory::class,
Command\Shortcode\ListShortcodesCommand::class => AnnotatedFactory::class,
Command\Shortcode\GetVisitsCommand::class => AnnotatedFactory::class,
Command\Shortcode\GeneratePreviewCommand::class => AnnotatedFactory::class,
Command\Visit\ProcessVisitsCommand::class => AnnotatedFactory::class,
Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class,
Command\Config\GenerateSecretCommand::class => AnnotatedFactory::class,
Command\Api\GenerateKeyCommand::class => AnnotatedFactory::class,
Command\Api\DisableKeyCommand::class => AnnotatedFactory::class,
Command\Api\ListKeysCommand::class => AnnotatedFactory::class,
Command\Tag\ListTagsCommand::class => AnnotatedFactory::class,
Command\Tag\CreateTagCommand::class => AnnotatedFactory::class,
Command\Tag\RenameTagCommand::class => AnnotatedFactory::class,
Command\Tag\DeleteTagsCommand::class => AnnotatedFactory::class,
],
],

Binary file not shown.

View File

@@ -1,15 +1,15 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2016-08-07 20:16+0200\n"
"PO-Revision-Date: 2016-08-07 20:18+0200\n"
"POT-Creation-Date: 2017-07-16 09:35+0200\n"
"PO-Revision-Date: 2017-07-16 09:39+0200\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 1.8.7.1\n"
"X-Generator: Poedit 2.0.1\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
@@ -68,7 +68,35 @@ msgstr ""
msgid "Character set:"
msgstr "Grupo de caracteres:"
#, fuzzy
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"
msgid "Secret key:"
msgstr "Clave secreta:"
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"
@@ -76,6 +104,9 @@ msgstr ""
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 "A long URL was not provided. Which URL do you want to shorten?:"
msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?"
@@ -131,6 +162,25 @@ msgstr "Listar todas las URLs cortas"
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"
@@ -143,28 +193,15 @@ msgstr "Fecha de creación"
msgid "Visits count"
msgstr "Número de visitas"
msgid "Tags"
msgstr "Etiquetas"
msgid "You have reached last page"
msgstr "Has alcanzado la última página"
msgid "Continue with page"
msgstr "Continuar con la página"
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 "Returns the long URL behind a short code"
msgstr "Devuelve la URL larga detrás de un código corto"
@@ -185,3 +222,66 @@ 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."
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 "Created tags"
msgstr "Etiquetas 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 "Deleted tags"
msgstr "Etiquetas 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"

View File

@@ -32,7 +32,7 @@ class DisableKeyCommand extends Command
{
$this->apiKeyService = $apiKeyService;
$this->translator = $translator;
parent::__construct(null);
parent::__construct();
}
public function configure()

View File

@@ -73,15 +73,18 @@ class ListKeysCommand extends Command
$key = $row->getKey();
$expiration = $row->getExpirationDate();
$rowData = [];
$formatMethod = ! $row->isEnabled()
? 'getErrorString'
: ($row->isExpired() ? 'getWarningString' : 'getSuccessString');
if ($enabledOnly) {
$rowData[] = $key;
$rowData[] = $this->{$formatMethod}($key);
} else {
$rowData[] = $row->isEnabled() ? $this->getSuccessString($key) : $this->getErrorString($key);
$rowData[] = $row->isEnabled() ? $this->getSuccessString('+++') : $this->getErrorString('---');
$rowData[] = $this->{$formatMethod}($key);
$rowData[] = $this->{$formatMethod}($this->getEnabledSymbol($row));
}
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ISO8601) : '-';
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ATOM) : '-';
$table->addRow($rowData);
}
@@ -105,4 +108,22 @@ class ListKeysCommand extends Command
{
return sprintf('<info>%s</info>', $string);
}
/**
* @param $string
* @return string
*/
protected function getWarningString($string)
{
return sprintf('<comment>%s</comment>', $string);
}
/**
* @param ApiKey $apiKey
* @return string
*/
protected function getEnabledSymbol(ApiKey $apiKey)
{
return ! $apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++';
}
}

View File

@@ -0,0 +1,247 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Install;
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManagerInterface;
use Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
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 InputInterface
*/
private $input;
/**
* @var OutputInterface
*/
private $output;
/**
* @var QuestionHelper
*/
private $questionHelper;
/**
* @var ProcessHelper
*/
private $processHelper;
/**
* @var WriterInterface
*/
private $configWriter;
/**
* @var Filesystem
*/
private $filesystem;
/**
* @var ConfigCustomizerPluginManagerInterface
*/
private $configCustomizers;
/**
* @var bool
*/
private $isUpdate;
/**
* InstallCommand constructor.
* @param WriterInterface $configWriter
* @param Filesystem $filesystem
* @param bool $isUpdate
* @throws LogicException
*/
public function __construct(
WriterInterface $configWriter,
Filesystem $filesystem,
ConfigCustomizerPluginManagerInterface $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');
}
public function execute(InputInterface $input, OutputInterface $output)
{
$this->input = $input;
$this->output = $output;
$this->questionHelper = $this->getHelper('question');
$this->processHelper = $this->getHelper('process');
$output->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')) {
$output->write('Deleting old cached config...');
try {
$this->filesystem->remove('data/cache/app_config.php');
$output->writeln(' <info>Success</info>');
} catch (IOException $e) {
$output->writeln(
' <error>Failed!</error> You will have to manually delete the data/cache/app_config.php file to get'
. ' new config applied.'
);
if ($output->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\DatabaseConfigCustomizerPlugin::class,
Plugin\UrlShortenerConfigCustomizerPlugin::class,
Plugin\LanguageConfigCustomizerPlugin::class,
Plugin\ApplicationConfigCustomizerPlugin::class,
] as $pluginName) {
/** @var Plugin\ConfigCustomizerPluginInterface $configCustomizer */
$configCustomizer = $this->configCustomizers->get($pluginName);
$configCustomizer->process($input, $output, $config);
}
// Generate config params files
$this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
$output->writeln(['<info>Custom configuration properly generated!</info>', '']);
// If current command is not update, generate database
if (! $this->isUpdate) {
$this->output->writeln('Initializing database...');
if (! $this->runCommand(
'php vendor/bin/doctrine.php orm:schema-tool:create',
'Error generating database.'
)) {
return;
}
}
// Run database migrations
$output->writeln('Updating database...');
if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) {
return;
}
// Generate proxies
$output->writeln('Generating proxies...');
if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) {
return;
}
}
/**
* @return CustomizableAppConfig
* @throws RuntimeException
*/
private function importConfig()
{
$config = new CustomizableAppConfig();
// Ask the user if he/she wants to import an older configuration
$importConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
'<question>Do you want to import previous configuration? (Y/n):</question> '
));
if (! $importConfig) {
return $config;
}
// Ask the user for the older shlink path
$keepAsking = true;
do {
$config->setImportedInstallationPath($this->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->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
'Provided path does not seem to be a valid shlink root path. '
. '<question>Do you want to try another path? (Y/n):</question> '
));
}
} 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 $text
* @param string|null $default
* @param bool $allowEmpty
* @return string
* @throws RuntimeException
*/
private function ask($text, $default = null, $allowEmpty = false)
{
if ($default !== null) {
$text .= ' (defaults to ' . $default . ')';
}
do {
$value = $this->questionHelper->ask($this->input, $this->output, new Question(
'<question>' . $text . ':</question> ',
$default
));
if (empty($value) && ! $allowEmpty) {
$this->output->writeln('<error>Value can\'t be empty</error>');
}
} while (empty($value) && $default === null && ! $allowEmpty);
return $value;
}
/**
* @param string $command
* @param string $errorMessage
* @return bool
*/
private function runCommand($command, $errorMessage)
{
$process = $this->processHelper->run($this->output, $command);
if ($process->isSuccessful()) {
$this->output->writeln(' <info>Success!</info>');
return true;
}
if ($this->output->isVerbose()) {
return false;
}
$this->output->writeln(
' <error>' . $errorMessage . '</error> Run this command with -vvv to see specific error info.'
);
return false;
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
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 Zend\I18n\Translator\TranslatorInterface;
class GeneratePreviewCommand extends Command
{
/**
* @var PreviewGeneratorInterface
*/
private $previewGenerator;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* @var ShortUrlServiceInterface
*/
private $shortUrlService;
/**
* GeneratePreviewCommand constructor.
* @param ShortUrlServiceInterface $shortUrlService
* @param PreviewGeneratorInterface $previewGenerator
* @param TranslatorInterface $translator
*
* @Inject({ShortUrlService::class, PreviewGenerator::class, "translator"})
*/
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('shortcode:process-previews')
->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());
$output->writeln('<info>' . $this->translator->translate('Finished processing all URLs') . '</info>');
}
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) {
$messages = [' <error>' . $this->translator->translate('Error') . '</error>'];
if ($output->isVerbose()) {
$messages[] = '<error>' . $e->__toString() . '</error>';
}
$output->writeln($messages);
}
}
}

View File

@@ -9,6 +9,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
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\Question\Question;
use Zend\Diactoros\Uri;
@@ -54,7 +55,13 @@ class GenerateShortcodeCommand extends Command
->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'));
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
->addOption(
'tags',
't',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
$this->translator->translate('Tags to apply to the new short URL')
);
}
public function interact(InputInterface $input, OutputInterface $output)
@@ -80,6 +87,13 @@ class GenerateShortcodeCommand extends Command
public function execute(InputInterface $input, OutputInterface $output)
{
$longUrl = $input->getArgument('longUrl');
$tags = $input->getOption('tags');
$processedTags = [];
foreach ($tags as $key => $tag) {
$explodedTags = explode(',', $tag);
$processedTags = array_merge($processedTags, $explodedTags);
}
$tags = $processedTags;
try {
if (! isset($longUrl)) {
@@ -87,10 +101,10 @@ class GenerateShortcodeCommand extends Command
return;
}
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl));
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl), $tags);
$shortUrl = (new Uri())->withPath($shortCode)
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
$output->writeln([
sprintf('%s <info>%s</info>', $this->translator->translate('Processed URL:'), $longUrl),

View File

@@ -55,28 +55,78 @@ class ListShortcodesCommand extends Command
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)
{
$page = intval($input->getOption('page'));
$searchTerm = $input->getOption('searchTerm');
$tags = $input->getOption('tags');
$tags = ! empty($tags) ? explode(',', $tags) : [];
$showTags = $input->getOption('showTags');
$orderBy = $input->getOption('orderBy');
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
do {
$result = $this->shortUrlService->listShortUrls($page);
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
$page++;
$table = new Table($output);
$table->setHeaders([
$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');
}
$table->setHeaders($headers);
foreach ($result as $row) {
$table->addRow(array_values($row->jsonSerialize()));
$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']);
}
$table->addRow(array_values($shortUrl));
}
$table->render();
@@ -95,4 +145,15 @@ class ListShortcodesCommand extends Command
}
} while ($continue);
}
protected 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,69 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
use Shlinkio\Shlink\Core\Service\Tag\TagService;
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 Zend\I18n\Translator\Translator;
use Zend\I18n\Translator\TranslatorInterface;
class CreateTagCommand extends Command
{
/**
* @var TagServiceInterface
*/
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* CreateTagCommand constructor.
* @param TagServiceInterface $tagService
* @param TranslatorInterface $translator
*
* @DI\Inject({TagService::class, Translator::class})
*/
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
}
protected function configure()
{
$this
->setName('tag:create')
->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)
{
$tagNames = $input->getOption('name');
if (empty($tagNames)) {
$output->writeln(sprintf(
'<comment>%s</comment>',
$this->translator->translate('You have to provide at least one tag name')
));
return;
}
$this->tagService->createTags($tagNames);
$output->writeln($this->translator->translate('Created tags') . sprintf(': ["<info>%s</info>"]', implode(
'</info>", "<info>',
$tagNames
)));
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
use Shlinkio\Shlink\Core\Service\Tag\TagService;
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 Zend\I18n\Translator\Translator;
use Zend\I18n\Translator\TranslatorInterface;
class DeleteTagsCommand extends Command
{
/**
* @var TagServiceInterface
*/
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ListTagsCommand constructor.
* @param TagServiceInterface $tagService
* @param TranslatorInterface $translator
*
* @DI\Inject({TagService::class, Translator::class})
*/
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
}
protected function configure()
{
$this
->setName('tag:delete')
->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)
{
$tagNames = $input->getOption('name');
if (empty($tagNames)) {
$output->writeln(sprintf(
'<comment>%s</comment>',
$this->translator->translate('You have to provide at least one tag name')
));
return;
}
$this->tagService->deleteTags($tagNames);
$output->writeln($this->translator->translate('Deleted tags') . sprintf(': ["<info>%s</info>"]', implode(
'</info>", "<info>',
$tagNames
)));
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagService;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Zend\I18n\Translator\Translator;
use Zend\I18n\Translator\TranslatorInterface;
class ListTagsCommand extends Command
{
/**
* @var TagServiceInterface
*/
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ListTagsCommand constructor.
* @param TagServiceInterface $tagService
* @param TranslatorInterface $translator
*
* @DI\Inject({TagService::class, Translator::class})
*/
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
}
protected function configure()
{
$this
->setName('tag:list')
->setDescription($this->translator->translate('Lists existing tags.'));
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$table = new Table($output);
$table->setHeaders([$this->translator->translate('Name')])
->setRows($this->getTagsRows());
$table->render();
}
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,63 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Service\Tag\TagService;
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 Zend\I18n\Translator\Translator;
use Zend\I18n\Translator\TranslatorInterface;
class RenameTagCommand extends Command
{
/**
* @var TagServiceInterface
*/
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* RenameTagCommand constructor.
* @param TagServiceInterface $tagService
* @param TranslatorInterface $translator
*
* @DI\Inject({TagService::class, Translator::class})
*/
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
}
protected function configure()
{
$this
->setName('tag:rename')
->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)
{
$oldName = $input->getArgument('oldName');
$newName = $input->getArgument('newName');
try {
$this->tagService->renameTag($oldName, $newName);
$output->writeln(sprintf('<info>%s</info>', $this->translator->translate('Tag properly renamed.')));
} catch (EntityDoesNotExistException $e) {
$output->writeln('<error>' . sprintf($this->translator->translate(
'A tag with name "%s" was not found'
), $oldName) . '</error>');
}
}
}

View File

@@ -3,7 +3,9 @@ namespace Shlinkio\Shlink\CLI\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Symfony\Component\Console\Application as CliApp;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
@@ -25,9 +27,12 @@ class ApplicationFactory implements FactoryInterface
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('config')['cli'];
$app = new CliApp('Shlink', '1.0.0');
$appOptions = $container->get(AppOptions::class);
$translator = $container->get(Translator::class);
$translator->setLocale($config['locale']);
$commands = isset($config['commands']) ? $config['commands'] : [];
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());
foreach ($commands as $command) {
if (! $container->has($command)) {
continue;

View File

@@ -0,0 +1,55 @@
<?php
namespace Shlinkio\Shlink\CLI\Factory;
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManager;
use Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Install\Plugin\Factory\DefaultConfigCustomizerPluginFactory;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Filesystem\Filesystem;
use Zend\Config\Writer\PhpArray;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
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 ConfigCustomizerPluginManager($container, ['factories' => [
Plugin\DatabaseConfigCustomizerPlugin::class => AnnotatedFactory::class,
Plugin\UrlShortenerConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
Plugin\LanguageConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
Plugin\ApplicationConfigCustomizerPlugin::class => DefaultConfigCustomizerPluginFactory::class,
]]),
$isUpdate
);
$app->add($command);
$app->setDefaultCommand($command->getName());
return $app;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Shlinkio\Shlink\CLI\Install;
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerPluginInterface;
use Zend\ServiceManager\AbstractPluginManager;
class ConfigCustomizerPluginManager extends AbstractPluginManager implements ConfigCustomizerPluginManagerInterface
{
protected $instanceOf = ConfigCustomizerPluginInterface::class;
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Shlinkio\Shlink\CLI\Install;
use Psr\Container\ContainerInterface;
interface ConfigCustomizerPluginManagerInterface extends ContainerInterface
{
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Shlinkio\Shlink\CLI\Install\Plugin;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
abstract class AbstractConfigCustomizerPlugin implements ConfigCustomizerPluginInterface
{
/**
* @var QuestionHelper
*/
protected $questionHelper;
public function __construct(QuestionHelper $questionHelper)
{
$this->questionHelper = $questionHelper;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param string $text
* @param string|null $default
* @param bool $allowEmpty
* @return string
* @throws RuntimeException
*/
protected function ask(InputInterface $input, OutputInterface $output, $text, $default = null, $allowEmpty = false)
{
if ($default !== null) {
$text .= ' (defaults to ' . $default . ')';
}
do {
$value = $this->questionHelper->ask($input, $output, new Question(
'<question>' . $text . ':</question> ',
$default
));
if (empty($value) && ! $allowEmpty) {
$output->writeln('<error>Value can\'t be empty</error>');
}
} while (empty($value) && $default === null && ! $allowEmpty);
return $value;
}
/**
* @param OutputInterface $output
* @param string $text
*/
protected function printTitle(OutputInterface $output, $text)
{
$text = trim($text);
$length = strlen($text) + 4;
$header = str_repeat('*', $length);
$output->writeln([
'',
'<info>' . $header . '</info>',
'<info>* ' . strtoupper($text) . ' *</info>',
'<info>' . $header . '</info>',
]);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class ApplicationConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
{
use StringUtilsTrait;
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param CustomizableAppConfig $appConfig
* @return void
* @throws \Symfony\Component\Console\Exception\RuntimeException
*/
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
{
$this->printTitle($output, 'APPLICATION');
if ($appConfig->hasApp() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
'<question>Do you want to keep imported application config? (Y/n):</question> '
))) {
return;
}
$appConfig->setApp([
'SECRET' => $this->ask(
$input,
$output,
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
null,
true
) ?: $this->generateRandomString(32),
]);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
interface ConfigCustomizerPluginInterface
{
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param CustomizableAppConfig $appConfig
* @return void
*/
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig);
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Shlinkio\Shlink\CLI\Install\Plugin;
use Acelaya\ZsmAnnotatedServices\Annotation as DI;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
class DatabaseConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
{
const DATABASE_DRIVERS = [
'MySQL' => 'pdo_mysql',
'PostgreSQL' => 'pdo_pgsql',
'SQLite' => 'pdo_sqlite',
];
/**
* @var Filesystem
*/
private $filesystem;
/**
* DatabaseConfigCustomizerPlugin constructor.
* @param QuestionHelper $questionHelper
* @param Filesystem $filesystem
*
* @DI\Inject({QuestionHelper::class, Filesystem::class})
*/
public function __construct(QuestionHelper $questionHelper, Filesystem $filesystem)
{
parent::__construct($questionHelper);
$this->filesystem = $filesystem;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param CustomizableAppConfig $appConfig
* @return void
* @throws IOException
* @throws RuntimeException
*/
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
{
$this->printTitle($output, 'DATABASE');
if ($appConfig->hasDatabase() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
'<question>Do you want to keep imported database config? (Y/n):</question> '
))) {
// 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) {
$output->writeln('<error>It wasn\'t possible to import the SQLite database</error>');
throw $e;
}
}
return;
}
// Select database type
$params = [];
$databases = array_keys(self::DATABASE_DRIVERS);
$dbType = $this->questionHelper->ask($input, $output, new ChoiceQuestion(
'<question>Select database type (defaults to ' . $databases[0] . '):</question>',
$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'] = $this->ask($input, $output, 'Database name', 'shlink');
$params['USER'] = $this->ask($input, $output, 'Database username');
$params['PASSWORD'] = $this->ask($input, $output, 'Database password');
$params['HOST'] = $this->ask($input, $output, 'Database host', 'localhost');
$params['PORT'] = $this->ask($input, $output, 'Database port', $this->getDefaultDbPort($params['DRIVER']));
}
$appConfig->setDatabase($params);
}
private function getDefaultDbPort($driver)
{
return $driver === 'pdo_mysql' ? '3306' : '5432';
}
}

View File

@@ -1,13 +1,14 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
namespace Shlinkio\Shlink\CLI\Install\Plugin\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Symfony\Component\Console\Helper\QuestionHelper;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class ErrorHandlerManagerFactory implements FactoryInterface
class DefaultConfigCustomizerPluginFactory implements FactoryInterface
{
/**
* Create an object
@@ -23,8 +24,6 @@ class ErrorHandlerManagerFactory implements FactoryInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('config')['error_handler'];
$plugins = isset($config['plugins']) ? $config['plugins'] : [];
return new ErrorHandlerManager($container, $plugins);
return new $requestedName($container->get(QuestionHelper::class));
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class LanguageConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
{
const SUPPORTED_LANGUAGES = ['en', 'es'];
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param CustomizableAppConfig $appConfig
* @return void
* @throws RuntimeException
*/
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
{
$this->printTitle($output, 'LANGUAGE');
if ($appConfig->hasLanguage() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
'<question>Do you want to keep imported language? (Y/n):</question> '
))) {
return;
}
$appConfig->setLanguage([
'DEFAULT' => $this->questionHelper->ask($input, $output, new ChoiceQuestion(
'<question>Select default language for the application in general (defaults to '
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
self::SUPPORTED_LANGUAGES,
0
)),
'CLI' => $this->questionHelper->ask($input, $output, new ChoiceQuestion(
'<question>Select default language for CLI executions (defaults to '
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
self::SUPPORTED_LANGUAGES,
0
)),
]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class UrlShortenerConfigCustomizerPlugin extends AbstractConfigCustomizerPlugin
{
/**
* @param InputInterface $input
* @param OutputInterface $output
* @param CustomizableAppConfig $appConfig
* @return void
* @throws RuntimeException
*/
public function process(InputInterface $input, OutputInterface $output, CustomizableAppConfig $appConfig)
{
$this->printTitle($output, 'URL SHORTENER');
if ($appConfig->hasUrlShortener() && $this->questionHelper->ask($input, $output, new ConfirmationQuestion(
'<question>Do you want to keep imported URL shortener config? (Y/n):</question> '
))) {
return;
}
// Ask for URL shortener params
$appConfig->setUrlShortener([
'SCHEMA' => $this->questionHelper->ask($input, $output, new ChoiceQuestion(
'<question>Select schema for generated short URLs (defaults to http):</question>',
['http', 'https'],
0
)),
'HOSTNAME' => $this->ask($input, $output, 'Hostname for generated URLs'),
'CHARS' => $this->ask(
$input,
$output,
'Character set for generated short codes (leave empty to autogenerate one)',
null,
true
) ?: str_shuffle(UrlShortener::DEFAULT_CHARS)
]);
}
}

View File

@@ -0,0 +1,265 @@
<?php
namespace Shlinkio\Shlink\CLI\Model;
use Zend\Stdlib\ArraySerializableInterface;
final class CustomizableAppConfig implements ArraySerializableInterface
{
const SQLITE_DB_PATH = 'data/database.sqlite';
/**
* @var array
*/
private $database;
/**
* @var array
*/
private $urlShortener;
/**
* @var array
*/
private $language;
/**
* @var array
*/
private $app;
/**
* @var string
*/
private $importedInstallationPath;
/**
* @return array
*/
public function getDatabase()
{
return $this->database;
}
/**
* @param array $database
* @return $this
*/
public function setDatabase(array $database)
{
$this->database = $database;
return $this;
}
/**
* @return bool
*/
public function hasDatabase()
{
return ! empty($this->database);
}
/**
* @return array
*/
public function getUrlShortener()
{
return $this->urlShortener;
}
/**
* @param array $urlShortener
* @return $this
*/
public function setUrlShortener(array $urlShortener)
{
$this->urlShortener = $urlShortener;
return $this;
}
/**
* @return bool
*/
public function hasUrlShortener()
{
return ! empty($this->urlShortener);
}
/**
* @return array
*/
public function getLanguage()
{
return $this->language;
}
/**
* @param array $language
* @return $this
*/
public function setLanguage(array $language)
{
$this->language = $language;
return $this;
}
/**
* @return bool
*/
public function hasLanguage()
{
return ! empty($this->language);
}
/**
* @return array
*/
public function getApp()
{
return $this->app;
}
/**
* @param array $app
* @return $this
*/
public function setApp(array $app)
{
$this->app = $app;
return $this;
}
/**
* @return bool
*/
public function hasApp()
{
return ! empty($this->app);
}
/**
* @return string
*/
public function getImportedInstallationPath()
{
return $this->importedInstallationPath;
}
/**
* @param string $importedInstallationPath
* @return $this|self
*/
public function setImportedInstallationPath($importedInstallationPath)
{
$this->importedInstallationPath = $importedInstallationPath;
return $this;
}
/**
* @return bool
*/
public function hasImportedInstallationPath()
{
return $this->importedInstallationPath !== null;
}
/**
* Exchange internal values from provided array
*
* @param array $array
* @return void
*/
public function exchangeArray(array $array)
{
if (isset($array['app_options'], $array['app_options']['secret_key'])) {
$this->setApp([
'SECRET' => $array['app_options']['secret_key'],
]);
}
if (isset($array['entity_manager'], $array['entity_manager']['connection'])) {
$this->deserializeDatabase($array['entity_manager']['connection']);
}
if (isset($array['translator'], $array['translator']['locale'], $array['cli'], $array['cli']['locale'])) {
$this->setLanguage([
'DEFAULT' => $array['translator']['locale'],
'CLI' => $array['cli']['locale'],
]);
}
if (isset($array['url_shortener'])) {
$urlShortener = $array['url_shortener'];
$this->setUrlShortener([
'SCHEMA' => $urlShortener['domain']['schema'],
'HOSTNAME' => $urlShortener['domain']['hostname'],
'CHARS' => $urlShortener['shortcode_chars'],
]);
}
}
private function deserializeDatabase(array $conn)
{
if (! isset($conn['driver'])) {
return;
}
$driver = $conn['driver'];
$params = ['DRIVER' => $driver];
if ($driver !== 'pdo_sqlite') {
$params['USER'] = $conn['user'];
$params['PASSWORD'] = $conn['password'];
$params['NAME'] = $conn['dbname'];
$params['HOST'] = $conn['host'];
$params['PORT'] = $conn['port'];
}
$this->setDatabase($params);
}
/**
* Return an array representation of the object
*
* @return array
*/
public function getArrayCopy()
{
$config = [
'app_options' => [
'secret_key' => $this->app['SECRET'],
],
'entity_manager' => [
'connection' => [
'driver' => $this->database['DRIVER'],
],
],
'translator' => [
'locale' => $this->language['DEFAULT'],
],
'cli' => [
'locale' => $this->language['CLI'],
],
'url_shortener' => [
'domain' => [
'schema' => $this->urlShortener['SCHEMA'],
'hostname' => $this->urlShortener['HOSTNAME'],
],
'shortcode_chars' => $this->urlShortener['CHARS'],
],
];
// Build dynamic database config based on selected driver
if ($this->database['DRIVER'] === 'pdo_sqlite') {
$config['entity_manager']['connection']['path'] = self::SQLITE_DB_PATH;
} else {
$config['entity_manager']['connection']['user'] = $this->database['USER'];
$config['entity_manager']['connection']['password'] = $this->database['PASSWORD'];
$config['entity_manager']['connection']['dbname'] = $this->database['NAME'];
$config['entity_manager']['connection']['host'] = $this->database['HOST'];
$config['entity_manager']['connection']['port'] = $this->database['PORT'];
if ($this->database['DRIVER'] === 'pdo_mysql') {
$config['entity_manager']['connection']['driverOptions'] = [
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
];
}
}
return $config;
}
}

View File

@@ -0,0 +1,2 @@
<?php
return [];

View File

@@ -1,7 +1,7 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;

View File

@@ -1,7 +1,7 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;

View File

@@ -1,7 +1,7 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
use Shlinkio\Shlink\Rest\Entity\ApiKey;

View File

@@ -1,7 +1,7 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Config;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;

View File

@@ -0,0 +1,139 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Install;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManagerInterface;
use Shlinkio\Shlink\CLI\Install\Plugin\ConfigCustomizerPluginInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\Process;
use Zend\Config\Writer\WriterInterface;
class InstallCommandTest extends TestCase
{
/**
* @var InstallCommand
*/
protected $command;
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $configWriter;
/**
* @var ObjectProphecy
*/
protected $filesystem;
public function setUp()
{
$processMock = $this->prophesize(Process::class);
$processMock->isSuccessful()->willReturn(true);
$processHelper = $this->prophesize(ProcessHelper::class);
$processHelper->getName()->willReturn('process');
$processHelper->setHelperSet(Argument::any())->willReturn(null);
$processHelper->run(Argument::cetera())->willReturn($processMock->reveal());
$this->filesystem = $this->prophesize(Filesystem::class);
$this->filesystem->exists(Argument::cetera())->willReturn(false);
$this->configWriter = $this->prophesize(WriterInterface::class);
$configCustomizer = $this->prophesize(ConfigCustomizerPluginInterface::class);
$configCustomizers = $this->prophesize(ConfigCustomizerPluginManagerInterface::class);
$configCustomizers->get(Argument::cetera())->willReturn($configCustomizer->reveal());
$app = new Application();
$helperSet = $app->getHelperSet();
$helperSet->set($processHelper->reveal());
$app->setHelperSet($helperSet);
$this->command = new InstallCommand(
$this->configWriter->reveal(),
$this->filesystem->reveal(),
$configCustomizers->reveal()
);
$app->add($this->command);
$this->commandTester = new CommandTester($this->command);
}
/**
* @test
*/
public function generatedConfigIsProperlyPersisted()
{
$this->configWriter->toFile(Argument::any(), Argument::type('array'), false)->shouldBeCalledTimes(1);
$this->commandTester->execute([]);
}
/**
* @test
*/
public function cachedConfigIsDeletedIfExists()
{
/** @var MethodProphecy $appConfigExists */
$appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn(true);
/** @var MethodProphecy $appConfigRemove */
$appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willReturn(null);
$this->commandTester->execute([]);
$appConfigExists->shouldHaveBeenCalledTimes(1);
$appConfigRemove->shouldHaveBeenCalledTimes(1);
}
/**
* @test
*/
public function exceptionWhileDeletingCachedConfigCancelsProcess()
{
/** @var MethodProphecy $appConfigExists */
$appConfigExists = $this->filesystem->exists('data/cache/app_config.php')->willReturn(true);
/** @var MethodProphecy $appConfigRemove */
$appConfigRemove = $this->filesystem->remove('data/cache/app_config.php')->willThrow(IOException::class);
/** @var MethodProphecy $configToFile */
$configToFile = $this->configWriter->toFile(Argument::cetera())->willReturn(true);
$this->commandTester->execute([]);
$appConfigExists->shouldHaveBeenCalledTimes(1);
$appConfigRemove->shouldHaveBeenCalledTimes(1);
$configToFile->shouldNotHaveBeenCalled();
}
/**
* @test
*/
public function whenCommandIsUpdatePreviousConfigCanBeImported()
{
$ref = new \ReflectionObject($this->command);
$prop = $ref->getProperty('isUpdate');
$prop->setAccessible(true);
$prop->setValue($this->command, true);
/** @var MethodProphecy $importedConfigExists */
$importedConfigExists = $this->filesystem->exists(
__DIR__ . '/../../../test-resources/' . InstallCommand::GENERATED_CONFIG_PATH
)->willReturn(true);
$this->commandTester->setInputs([
'',
'/foo/bar/wrong_previous_shlink',
'',
__DIR__ . '/../../../test-resources',
]);
$this->commandTester->execute([]);
$importedConfigExists->shouldHaveBeenCalled();
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\GeneratePreviewCommand;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
class GeneratePreviewCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
private $previewGenerator;
/**
* @var ObjectProphecy
*/
private $shortUrlService;
public function setUp()
{
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
$command = new GeneratePreviewCommand(
$this->shortUrlService->reveal(),
$this->previewGenerator->reveal(),
Translator::factory([])
);
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function previewsForEveryUrlAreGenerated()
{
$paginator = $this->createPaginator([
(new ShortUrl())->setOriginalUrl('http://foo.com'),
(new ShortUrl())->setOriginalUrl('https://bar.com'),
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
]);
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
$this->previewGenerator->generatePreview('http://foo.com')->shouldBeCalledTimes(1);
$this->previewGenerator->generatePreview('https://bar.com')->shouldBeCalledTimes(1);
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:process-previews'
]);
}
/**
* @test
*/
public function exceptionWillOutputError()
{
$items = [
(new ShortUrl())->setOriginalUrl('http://foo.com'),
(new ShortUrl())->setOriginalUrl('https://bar.com'),
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
];
$paginator = $this->createPaginator($items);
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
$this->previewGenerator->generatePreview(Argument::any())->willThrow(PreviewGenerationException::class)
->shouldBeCalledTimes(count($items));
$this->commandTester->execute([
'command' => 'shortcode:process-previews'
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals(count($items), substr_count($output, 'Error'));
}
protected function createPaginator(array $items)
{
$paginator = new Paginator(new ArrayAdapter($items));
$paginator->setItemCountPerPage(count($items));
return $paginator;
}
}

View File

@@ -1,7 +1,7 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\GenerateShortcodeCommand;
@@ -39,8 +39,8 @@ class GenerateShortcodeCommandTest extends TestCase
*/
public function properShortCodeIsCreatedIfLongUrlIsCorrect()
{
$this->urlShortener->urlToShortCode(Argument::any())->willReturn('abc123')
->shouldBeCalledTimes(1);
$this->urlShortener->urlToShortCode(Argument::cetera())->willReturn('abc123')
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:generate',
@@ -55,8 +55,8 @@ class GenerateShortcodeCommandTest extends TestCase
*/
public function exceptionWhileParsingLongUrlOutputsError()
{
$this->urlShortener->urlToShortCode(Argument::any())->willThrow(new InvalidUrlException())
->shouldBeCalledTimes(1);
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:generate',

View File

@@ -1,7 +1,7 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\GetVisitsCommand;

View File

@@ -1,7 +1,7 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\ListShortcodesCommand;
@@ -46,8 +46,8 @@ class ListShortcodesCommandTest extends TestCase
public function noInputCallsListJustOnce()
{
$this->questionHelper->setInputStream($this->getInputStream('\n'));
$this->shortUrlService->listShortUrls(1)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$this->commandTester->execute(['command' => 'shortcode:list']);
}
@@ -66,7 +66,11 @@ class ListShortcodesCommandTest extends TestCase
$questionHelper = $this->questionHelper;
$that = $this;
$this->shortUrlService->listShortUrls(Argument::any())->will(function () use (&$data, $questionHelper, $that) {
$this->shortUrlService->listShortUrls(Argument::cetera())->will(function () use (
&$data,
$questionHelper,
$that
) {
$questionHelper->setInputStream($that->getInputStream('y'));
return new Paginator(new ArrayAdapter(array_shift($data)));
})->shouldBeCalledTimes(3);
@@ -86,8 +90,8 @@ class ListShortcodesCommandTest extends TestCase
}
$this->questionHelper->setInputStream($this->getInputStream('n'));
$this->shortUrlService->listShortUrls(Argument::any())->willReturn(new Paginator(new ArrayAdapter($data)))
->shouldBeCalledTimes(1);
$this->shortUrlService->listShortUrls(Argument::cetera())->willReturn(new Paginator(new ArrayAdapter($data)))
->shouldBeCalledTimes(1);
$this->commandTester->execute(['command' => 'shortcode:list']);
}
@@ -99,8 +103,8 @@ class ListShortcodesCommandTest extends TestCase
{
$page = 5;
$this->questionHelper->setInputStream($this->getInputStream('\n'));
$this->shortUrlService->listShortUrls($page)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$this->shortUrlService->listShortUrls($page, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:list',
@@ -108,6 +112,23 @@ class ListShortcodesCommandTest extends TestCase
]);
}
/**
* @test
*/
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
{
$this->questionHelper->setInputStream($this->getInputStream('\n'));
$this->shortUrlService->listShortUrls(1, null, [], null)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:list',
'--showTags' => true,
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'Tags') > 0);
}
protected function getInputStream($inputData)
{
$stream = fopen('php://memory', 'r+', false);

View File

@@ -1,7 +1,7 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;

View File

@@ -0,0 +1,67 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\CreateTagCommand;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class CreateTagCommandTest extends TestCase
{
/**
* @var CreateTagCommand
*/
private $command;
/**
* @var CommandTester
*/
private $commandTester;
/**
* @var ObjectProphecy
*/
private $tagService;
public function setUp()
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$command = new CreateTagCommand($this->tagService->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function errorIsReturnedWhenNoTagsAreProvided()
{
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('You have to provide at least one tag name', $output);
}
/**
* @test
*/
public function serviceIsInvokedOnSuccess()
{
$tagNames = ['foo', 'bar'];
/** @var MethodProphecy $createTags */
$createTags = $this->tagService->createTags($tagNames)->willReturn([]);
$this->commandTester->execute([
'--name' => $tagNames,
]);
$output = $this->commandTester->getDisplay();
$this->assertContains(sprintf('Created tags: ["%s"]', implode('", "', $tagNames)), $output);
$createTags->shouldHaveBeenCalled();
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\DeleteTagsCommand;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class DeleteTagsCommandTest extends TestCase
{
/**
* @var DeleteTagsCommand
*/
private $command;
/**
* @var CommandTester
*/
private $commandTester;
/**
* @var ObjectProphecy
*/
private $tagService;
public function setUp()
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$command = new DeleteTagsCommand($this->tagService->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function errorIsReturnedWhenNoTagsAreProvided()
{
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('You have to provide at least one tag name', $output);
}
/**
* @test
*/
public function serviceIsInvokedOnSuccess()
{
$tagNames = ['foo', 'bar'];
/** @var MethodProphecy $deleteTags */
$deleteTags = $this->tagService->deleteTags($tagNames)->will(function () {
});
$this->commandTester->execute([
'--name' => $tagNames,
]);
$output = $this->commandTester->getDisplay();
$this->assertContains(sprintf('Deleted tags: ["%s"]', implode('", "', $tagNames)), $output);
$deleteTags->shouldHaveBeenCalled();
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\ListTagsCommand;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class ListTagsCommandTest extends TestCase
{
/**
* @var ListTagsCommand
*/
private $command;
/**
* @var CommandTester
*/
private $commandTester;
/**
* @var ObjectProphecy
*/
private $tagService;
public function setUp()
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$command = new ListTagsCommand($this->tagService->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function noTagsPrintsEmptyMessage()
{
/** @var MethodProphecy $listTags */
$listTags = $this->tagService->listTags()->willReturn([]);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('No tags yet', $output);
$listTags->shouldHaveBeenCalled();
}
/**
* @test
*/
public function listOfTagsIsPrinted()
{
/** @var MethodProphecy $listTags */
$listTags = $this->tagService->listTags()->willReturn([
(new Tag())->setName('foo'),
(new Tag())->setName('bar'),
]);
$this->commandTester->execute([]);
$output = $this->commandTester->getDisplay();
$this->assertContains('foo', $output);
$this->assertContains('bar', $output);
$listTags->shouldHaveBeenCalled();
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Tag;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Tag\RenameTagCommand;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class RenameTagCommandTest extends TestCase
{
/**
* @var RenameTagCommand
*/
private $command;
/**
* @var CommandTester
*/
private $commandTester;
/**
* @var ObjectProphecy
*/
private $tagService;
public function setUp()
{
$this->tagService = $this->prophesize(TagServiceInterface::class);
$command = new RenameTagCommand($this->tagService->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function errorIsPrintedIfExceptionIsThrown()
{
$oldName = 'foo';
$newName = 'bar';
/** @var MethodProphecy $renameTag */
$renameTag = $this->tagService->renameTag($oldName, $newName)->willThrow(EntityDoesNotExistException::class);
$this->commandTester->execute([
'oldName' => $oldName,
'newName' => $newName,
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('A tag with name "foo" was not found', $output);
$renameTag->shouldHaveBeenCalled();
}
/**
* @test
*/
public function successIsPrintedIfNoErrorOccurs()
{
$oldName = 'foo';
$newName = 'bar';
/** @var MethodProphecy $renameTag */
$renameTag = $this->tagService->renameTag($oldName, $newName)->willReturn(new Tag());
$this->commandTester->execute([
'oldName' => $oldName,
'newName' => $newName,
]);
$output = $this->commandTester->getDisplay();
$this->assertContains('Tag properly renamed', $output);
$renameTag->shouldHaveBeenCalled();
}
}

View File

@@ -1,7 +1,7 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;

View File

@@ -1,7 +1,7 @@
<?php
namespace ShlinkioTest\Shlink\CLI;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\ConfigProvider;
class ConfigProviderTest extends TestCase

View File

@@ -1,10 +1,12 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Factory;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\ServiceManager;
class ApplicationFactoryTest extends TestCase
@@ -53,8 +55,10 @@ class ApplicationFactoryTest extends TestCase
{
return new ServiceManager(['services' => [
'config' => [
'cli' => $config,
'cli' => array_merge($config, ['locale' => 'en']),
],
AppOptions::class => new AppOptions(),
Translator::class => Translator::factory([]),
]]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Factory;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
use Symfony\Component\Console\Application;
use Symfony\Component\Filesystem\Filesystem;
use Zend\ServiceManager\ServiceManager;
class InstallApplicationFactoryTest extends TestCase
{
/**
* @var InstallApplicationFactory
*/
private $factory;
public function setUp()
{
$this->factory = new InstallApplicationFactory();
}
/**
* @test
*/
public function serviceIsCreated()
{
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
Filesystem::class => $this->prophesize(Filesystem::class)->reveal(),
]]), '');
$this->assertInstanceOf(Application::class, $instance);
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Install\Plugin\ApplicationConfigCustomizerPlugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class ApplicationConfigCustomizerPluginTest extends TestCase
{
/**
* @var ApplicationConfigCustomizerPlugin
*/
private $plugin;
/**
* @var ObjectProphecy
*/
private $questionHelper;
public function setUp()
{
$this->questionHelper = $this->prophesize(QuestionHelper::class);
$this->plugin = new ApplicationConfigCustomizerPlugin($this->questionHelper->reveal());
}
/**
* @test
*/
public function configIsRequestedToTheUser()
{
/** @var MethodProphecy $askSecret */
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('the_secret');
$config = new CustomizableAppConfig();
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertTrue($config->hasApp());
$this->assertEquals([
'SECRET' => 'the_secret',
], $config->getApp());
$askSecret->shouldHaveBeenCalledTimes(1);
}
/**
* @test
*/
public function overwriteIsRequestedIfValueIsAlreadySet()
{
/** @var MethodProphecy $ask */
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
$last = array_pop($args);
return $last instanceof ConfirmationQuestion ? false : 'the_new_secret';
});
$config = new CustomizableAppConfig();
$config->setApp([
'SECRET' => 'foo',
]);
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertEquals([
'SECRET' => 'the_new_secret',
], $config->getApp());
$ask->shouldHaveBeenCalledTimes(2);
}
/**
* @test
*/
public function existingValueIsKeptIfRequested()
{
/** @var MethodProphecy $ask */
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
$config = new CustomizableAppConfig();
$config->setApp([
'SECRET' => 'foo',
]);
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertEquals([
'SECRET' => 'foo',
], $config->getApp());
$ask->shouldHaveBeenCalledTimes(1);
}
}

View File

@@ -0,0 +1,152 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizerPlugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Filesystem\Filesystem;
class DatabaseConfigCustomizerPluginTest extends TestCase
{
/**
* @var DatabaseConfigCustomizerPlugin
*/
private $plugin;
/**
* @var ObjectProphecy
*/
private $questionHelper;
/**
* @var ObjectProphecy
*/
private $filesystem;
public function setUp()
{
$this->questionHelper = $this->prophesize(QuestionHelper::class);
$this->filesystem = $this->prophesize(Filesystem::class);
$this->plugin = new DatabaseConfigCustomizerPlugin(
$this->questionHelper->reveal(),
$this->filesystem->reveal()
);
}
/**
* @test
*/
public function configIsRequestedToTheUser()
{
/** @var MethodProphecy $askSecret */
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('MySQL');
$config = new CustomizableAppConfig();
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertTrue($config->hasDatabase());
$this->assertEquals([
'DRIVER' => 'pdo_mysql',
'NAME' => 'MySQL',
'USER' => 'MySQL',
'PASSWORD' => 'MySQL',
'HOST' => 'MySQL',
'PORT' => 'MySQL',
], $config->getDatabase());
$askSecret->shouldHaveBeenCalledTimes(6);
}
/**
* @test
*/
public function overwriteIsRequestedIfValueIsAlreadySet()
{
/** @var MethodProphecy $ask */
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
$last = array_pop($args);
return $last instanceof ConfirmationQuestion ? false : 'MySQL';
});
$config = new CustomizableAppConfig();
$config->setDatabase([
'DRIVER' => 'pdo_pgsql',
'NAME' => 'MySQL',
'USER' => 'MySQL',
'PASSWORD' => 'MySQL',
'HOST' => 'MySQL',
'PORT' => 'MySQL',
]);
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertEquals([
'DRIVER' => 'pdo_mysql',
'NAME' => 'MySQL',
'USER' => 'MySQL',
'PASSWORD' => 'MySQL',
'HOST' => 'MySQL',
'PORT' => 'MySQL',
], $config->getDatabase());
$ask->shouldHaveBeenCalledTimes(7);
}
/**
* @test
*/
public function existingValueIsKeptIfRequested()
{
/** @var MethodProphecy $ask */
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
$config = new CustomizableAppConfig();
$config->setDatabase([
'DRIVER' => 'pdo_pgsql',
'NAME' => 'MySQL',
'USER' => 'MySQL',
'PASSWORD' => 'MySQL',
'HOST' => 'MySQL',
'PORT' => 'MySQL',
]);
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertEquals([
'DRIVER' => 'pdo_pgsql',
'NAME' => 'MySQL',
'USER' => 'MySQL',
'PASSWORD' => 'MySQL',
'HOST' => 'MySQL',
'PORT' => 'MySQL',
], $config->getDatabase());
$ask->shouldHaveBeenCalledTimes(1);
}
/**
* @test
*/
public function sqliteDatabaseIsImportedWhenRequested()
{
/** @var MethodProphecy $ask */
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
/** @var MethodProphecy $copy */
$copy = $this->filesystem->copy(Argument::cetera())->willReturn(null);
$config = new CustomizableAppConfig();
$config->setDatabase([
'DRIVER' => 'pdo_sqlite',
]);
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertEquals([
'DRIVER' => 'pdo_sqlite',
], $config->getDatabase());
$ask->shouldHaveBeenCalledTimes(1);
$copy->shouldHaveBeenCalledTimes(1);
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Install\Plugin\Factory;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\Install\Plugin\ApplicationConfigCustomizerPlugin;
use Shlinkio\Shlink\CLI\Install\Plugin\Factory\DefaultConfigCustomizerPluginFactory;
use Shlinkio\Shlink\CLI\Install\Plugin\LanguageConfigCustomizerPlugin;
use Symfony\Component\Console\Helper\QuestionHelper;
use Zend\ServiceManager\ServiceManager;
class DefaultConfigCustomizerPluginFactoryTest extends TestCase
{
/**
* @var DefaultConfigCustomizerPluginFactory
*/
protected $factory;
public function setUp()
{
$this->factory = new DefaultConfigCustomizerPluginFactory();
}
/**
* @test
*/
public function createsProperService()
{
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
QuestionHelper::class => $this->prophesize(QuestionHelper::class)->reveal(),
]]), ApplicationConfigCustomizerPlugin::class);
$this->assertInstanceOf(ApplicationConfigCustomizerPlugin::class, $instance);
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
QuestionHelper::class => $this->prophesize(QuestionHelper::class)->reveal(),
]]), LanguageConfigCustomizerPlugin::class);
$this->assertInstanceOf(LanguageConfigCustomizerPlugin::class, $instance);
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Install\Plugin\LanguageConfigCustomizerPlugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class LanguageConfigCustomizerPluginTest extends TestCase
{
/**
* @var LanguageConfigCustomizerPlugin
*/
protected $plugin;
/**
* @var ObjectProphecy
*/
protected $questionHelper;
public function setUp()
{
$this->questionHelper = $this->prophesize(QuestionHelper::class);
$this->plugin = new LanguageConfigCustomizerPlugin($this->questionHelper->reveal());
}
/**
* @test
*/
public function configIsRequestedToTheUser()
{
/** @var MethodProphecy $askSecret */
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('en');
$config = new CustomizableAppConfig();
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertTrue($config->hasLanguage());
$this->assertEquals([
'DEFAULT' => 'en',
'CLI' => 'en',
], $config->getLanguage());
$askSecret->shouldHaveBeenCalledTimes(2);
}
/**
* @test
*/
public function overwriteIsRequestedIfValueIsAlreadySet()
{
/** @var MethodProphecy $ask */
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
$last = array_pop($args);
return $last instanceof ConfirmationQuestion ? false : 'es';
});
$config = new CustomizableAppConfig();
$config->setLanguage([
'DEFAULT' => 'en',
'CLI' => 'en',
]);
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertEquals([
'DEFAULT' => 'es',
'CLI' => 'es',
], $config->getLanguage());
$ask->shouldHaveBeenCalledTimes(3);
}
/**
* @test
*/
public function existingValueIsKeptIfRequested()
{
/** @var MethodProphecy $ask */
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
$config = new CustomizableAppConfig();
$config->setLanguage([
'DEFAULT' => 'es',
'CLI' => 'es',
]);
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertEquals([
'DEFAULT' => 'es',
'CLI' => 'es',
], $config->getLanguage());
$ask->shouldHaveBeenCalledTimes(1);
}
}

View File

@@ -0,0 +1,103 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Install\Plugin;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Install\Plugin\UrlShortenerConfigCustomizerPlugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Question\ConfirmationQuestion;
class UrlShortenerConfigCustomizerPluginTest extends TestCase
{
/**
* @var UrlShortenerConfigCustomizerPlugin
*/
private $plugin;
/**
* @var ObjectProphecy
*/
private $questionHelper;
public function setUp()
{
$this->questionHelper = $this->prophesize(QuestionHelper::class);
$this->plugin = new UrlShortenerConfigCustomizerPlugin($this->questionHelper->reveal());
}
/**
* @test
*/
public function configIsRequestedToTheUser()
{
/** @var MethodProphecy $askSecret */
$askSecret = $this->questionHelper->ask(Argument::cetera())->willReturn('something');
$config = new CustomizableAppConfig();
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertTrue($config->hasUrlShortener());
$this->assertEquals([
'SCHEMA' => 'something',
'HOSTNAME' => 'something',
'CHARS' => 'something',
], $config->getUrlShortener());
$askSecret->shouldHaveBeenCalledTimes(3);
}
/**
* @test
*/
public function overwriteIsRequestedIfValueIsAlreadySet()
{
/** @var MethodProphecy $ask */
$ask = $this->questionHelper->ask(Argument::cetera())->will(function (array $args) {
$last = array_pop($args);
return $last instanceof ConfirmationQuestion ? false : 'foo';
});
$config = new CustomizableAppConfig();
$config->setUrlShortener([
'SCHEMA' => 'bar',
'HOSTNAME' => 'bar',
'CHARS' => 'bar',
]);
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertEquals([
'SCHEMA' => 'foo',
'HOSTNAME' => 'foo',
'CHARS' => 'foo',
], $config->getUrlShortener());
$ask->shouldHaveBeenCalledTimes(4);
}
/**
* @test
*/
public function existingValueIsKeptIfRequested()
{
/** @var MethodProphecy $ask */
$ask = $this->questionHelper->ask(Argument::cetera())->willReturn(true);
$config = new CustomizableAppConfig();
$config->setUrlShortener([
'SCHEMA' => 'foo',
'HOSTNAME' => 'foo',
'CHARS' => 'foo',
]);
$this->plugin->process(new ArrayInput([]), new NullOutput(), $config);
$this->assertEquals([
'SCHEMA' => 'foo',
'HOSTNAME' => 'foo',
'CHARS' => 'foo',
], $config->getUrlShortener());
$ask->shouldHaveBeenCalledTimes(1);
}
}

View File

@@ -4,43 +4,44 @@ use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\ErrorHandler;
use Shlinkio\Shlink\Common\Factory\CacheFactory;
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
use Shlinkio\Shlink\Common\Factory\LoggerFactory;
use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
use Shlinkio\Shlink\Common\Factory;
use Shlinkio\Shlink\Common\Image;
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
use Shlinkio\Shlink\Common\Service;
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
use Symfony\Component\Filesystem\Filesystem;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
'dependencies' => [
'invokables' => [
Filesystem::class => Filesystem::class,
],
'factories' => [
EntityManager::class => EntityManagerFactory::class,
EntityManager::class => Factory\EntityManagerFactory::class,
GuzzleHttp\Client::class => InvokableFactory::class,
Cache::class => CacheFactory::class,
LoggerInterface::class => LoggerFactory::class,
'Logger_Shlink' => LoggerFactory::class,
Cache::class => Factory\CacheFactory::class,
'Logger_Shlink' => Factory\LoggerFactory::class,
Translator::class => TranslatorFactory::class,
Translator::class => Factory\TranslatorFactory::class,
TranslatorExtension::class => AnnotatedFactory::class,
LocaleMiddleware::class => AnnotatedFactory::class,
IpLocationResolver::class => AnnotatedFactory::class,
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
ErrorHandler\ContentBasedErrorHandler::class => AnnotatedFactory::class,
ErrorHandler\ErrorHandlerManager::class => ErrorHandler\ErrorHandlerManagerFactory::class,
Service\IpLocationResolver::class => AnnotatedFactory::class,
Service\PreviewGenerator::class => AnnotatedFactory::class,
],
'aliases' => [
'em' => EntityManager::class,
'httpClient' => GuzzleHttp\Client::class,
'translator' => Translator::class,
'logger' => LoggerInterface::class,
Logger::class => LoggerInterface::class,
AnnotatedFactory::CACHE_SERVICE => Cache::class,
Logger::class => 'Logger_Shlink',
LoggerInterface::class => 'Logger_Shlink',
],
],

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