Compare commits

...

206 Commits

Author SHA1 Message Date
Alejandro Celaya
6eef694315 Merge branch 'develop' 2016-08-21 21:24:31 +02:00
Alejandro Celaya
fe4a4aef34 Updated changelog 2016-08-21 21:24:00 +02:00
Alejandro Celaya
b13c95cf1a Placed cross domain middleware as the first one for rest requests 2016-08-21 21:21:31 +02:00
Alejandro Celaya
c1c325588e Merge branch 'develop' 2016-08-21 19:16:36 +02:00
Alejandro Celaya
faad60f79e Merge branch 'develop' of https://github.com/shlinkio/shlink into develop 2016-08-21 19:16:21 +02:00
Alejandro Celaya
cbb5a02b95 Kept symlincs while generating the dist file 2016-08-21 19:16:06 +02:00
Alejandro Celaya
96faafd31e Merge branch 'develop' 2016-08-21 18:31:45 +02:00
Alejandro Celaya
7aa42ada54 Merge pull request #61 from acelaya/develop
Develop
2016-08-21 18:31:32 +02:00
Alejandro Celaya
3b1545a761 Added v1.2.0 to changelog 2016-08-21 18:26:28 +02:00
Alejandro Celaya
536309afb6 Merge pull request #4 from acelaya/feature/45
Feature/45
2016-08-21 18:20:56 +02:00
Alejandro Celaya
ad6ef22b72 Updated languages 2016-08-21 18:17:39 +02:00
Alejandro Celaya
4185015bef Improved tests 2016-08-21 18:15:25 +02:00
Alejandro Celaya
3376725152 Created EditTagsActiontest 2016-08-21 18:06:34 +02:00
Alejandro Celaya
d7b18776f1 Improved error response in edit tag request 2016-08-21 17:55:26 +02:00
Alejandro Celaya
1da285a63a Created action to set the togas for a short url 2016-08-21 16:52:26 +02:00
Alejandro Celaya
372488cbb4 Created middleware to parse PUT request bodies in rest requests 2016-08-21 13:52:15 +02:00
Alejandro Celaya
b6fee0ebaf Added option to set tags while creating short code from rest API 2016-08-21 13:07:12 +02:00
Alejandro Celaya
322180bde4 Added tag property to json serialization of ShortUrl 2016-08-21 12:48:31 +02:00
Alejandro Celaya
1cf6c93007 Added option to pass tags when creating a short code from the command line 2016-08-21 10:39:00 +02:00
Alejandro Celaya
2b89556c09 Allowed to display tags in the shortcode:list command 2016-08-21 10:17:17 +02:00
Alejandro Celaya
e3021120e3 Checked tables do not exist before creating them 2016-08-21 09:45:36 +02:00
Alejandro Celaya
2ca7ab4ccf Created new entity Tag and migration to create new tables 2016-08-21 09:41:21 +02:00
Alejandro Celaya
a2c2bc166c Removed wrong spaces 2016-08-20 19:11:20 +02:00
Alejandro Celaya
5f0baab2ac Created script to update an already existing system 2016-08-19 15:50:08 +02:00
Alejandro Celaya
ea083e30b6 Improved InstallCommand, adding migrations and removing code duplication 2016-08-19 15:15:53 +02:00
Alejandro Celaya
7d49c1760c Added doctrine migrations and remove platform specific code from entities 2016-08-19 14:51:34 +02:00
Alejandro Celaya
7c42835cc1 Improved error management on install command 2016-08-18 18:02:24 +02:00
Alejandro Celaya
f77273ef93 Merge pull request #3 from acelaya/feature/7
Feature/7
2016-08-18 17:30:57 +02:00
Alejandro Celaya
e52983c5d9 Updated translations 2016-08-18 17:28:04 +02:00
Alejandro Celaya
4615bbaaf7 CreatedPreviewActionTest 2016-08-18 17:23:40 +02:00
Alejandro Celaya
b432ed2c1d Created GeneratePreviewCommandTest 2016-08-18 17:10:40 +02:00
Alejandro Celaya
34174b2fbe Added tests for Image classes 2016-08-18 16:37:50 +02:00
Alejandro Celaya
fac519699a Used filesystem check instead of cache check for preview generation 2016-08-18 13:20:57 +02:00
Alejandro Celaya
2c91ded514 Improved PreviewGenerator by composing an ImageBuilder that creates new Image objects fore each URL 2016-08-18 12:21:26 +02:00
Alejandro Celaya
15247e832e Improved QR code generation route 2016-08-18 11:31:04 +02:00
Alejandro Celaya
60c68c914b Managed error while generating URL previews by throwing an exception 2016-08-18 11:17:17 +02:00
Alejandro Celaya
277406c3b8 Created action to return preview images 2016-08-18 11:10:15 +02:00
Alejandro Celaya
26adf48b48 Added wkhtmltopdf stuff and created preview generator service 2016-08-18 10:19:33 +02:00
Alejandro Celaya
20e43aac90 Added bin dir to code sniffer checks 2016-08-17 11:47:08 +02:00
Alejandro Celaya
56c9abcfd0 Fixed build script to generate the distributable file just with project files from the project root 2016-08-16 11:51:05 +02:00
Alejandro Celaya
5ca4bc928d Created specific factory for AppOptions to prevent circular dependency with cache 2016-08-15 23:40:49 +02:00
Alejandro Celaya
ffa6c0d2ca Merge branch 'feature/install' into develop 2016-08-15 11:34:48 +02:00
Alejandro Celaya
395311eaad Added InstallCommandTest 2016-08-15 11:34:35 +02:00
Alejandro Celaya
5bbc7de4af Fixed namespace in some tests 2016-08-15 11:01:55 +02:00
Alejandro Celaya
d5516c7269 Finished build script to compress dist project file 2016-08-15 10:18:13 +02:00
Alejandro Celaya
f852f9d398 Created htaccess 2016-08-15 10:03:39 +02:00
Alejandro Celaya
0d9f964687 Fixed bug on build script 2016-08-15 10:03:17 +02:00
Alejandro Celaya
00c56ca594 Fixed tests 2016-08-15 09:52:44 +02:00
Alejandro Celaya
9e4fb68265 Created build script 2016-08-15 09:45:09 +02:00
Alejandro Celaya
804d99ebf7 Created CLI scripts for Windows OS 2016-08-15 09:21:14 +02:00
Alejandro Celaya
4bbdccf981 Added symfony process to run initialization commands 2016-08-14 23:41:42 +02:00
Alejandro Celaya
1f3e31d100 Fixed working directory and paths in InstallCommand 2016-08-14 23:25:04 +02:00
Alejandro Celaya
2617ef1547 Added cli locale to installation generated config file 2016-08-14 23:10:07 +02:00
Alejandro Celaya
25380e4727 Moved console Application creation to factory 2016-08-14 23:08:26 +02:00
Alejandro Celaya
12322b7368 Improved config loading by using only one config provider object 2016-08-14 22:56:22 +02:00
Alejandro Celaya
a608f7d0f4 Minor name change 2016-08-14 21:28:21 +02:00
Alejandro Celaya
9a42d70604 Added custom config params to merged confg 2016-08-14 18:15:10 +02:00
Alejandro Celaya
fe708333b1 Fixed path while generating config file 2016-08-14 10:43:16 +02:00
Alejandro Celaya
566ee7ef6f Finished custom config command 2016-08-14 10:30:43 +02:00
Alejandro Celaya
56af58fcb8 Created installation script and installation command 2016-08-14 09:11:46 +02:00
Alejandro Celaya
cffa43a155 Created installation script and installation command 2016-08-14 09:09:23 +02:00
Alejandro Celaya
2b2c0b7c13 Merge branch 'feature/29' into develop 2016-08-12 18:01:38 +02:00
Alejandro Celaya
faa8019fc5 Redefined logger services so that the error handler uses Shlink's logger 2016-08-12 18:01:09 +02:00
Alejandro Celaya
a8ea458649 Fixed distributable local config file 2016-08-12 17:55:07 +02:00
Alejandro Celaya
8f4d305982 Added ErrorHandler package dependency and remove local files 2016-08-12 17:54:32 +02:00
Alejandro Celaya
065cddc4b1 Merge branch 'develop' 2016-08-09 19:05:39 +02:00
Alejandro Celaya
3a3a16f46f Added changelog for v1.1.0 2016-08-09 19:05:13 +02:00
Alejandro Celaya
ba5bd6d98c Merge branch 'develop' 2016-08-09 19:00:42 +02:00
Alejandro Celaya
a1aa9c2031 Merge pull request #49 from acelaya/develop
v1.1.0
2016-08-09 19:00:22 +02:00
Alejandro Celaya
43c6b56e42 Fixed memcached test while comparing servers 2016-08-09 18:56:01 +02:00
Alejandro Celaya
39d2f5a38f Created travis config file to enable memcached extension 2016-08-09 18:50:14 +02:00
Alejandro Celaya
69cc30bce7 Allowed failures on PHP 7.1 environments 2016-08-09 18:38:48 +02:00
Alejandro Celaya
5913550eec Fixed build when memcached is not enabled in PHP 7.1 2016-08-09 18:29:47 +02:00
Alejandro Celaya
3140ab2ad7 Updated shlink website link to use https 2016-08-09 18:17:28 +02:00
Alejandro Celaya
090479fa62 Improved CacheFactory supporting more adapters 2016-08-09 17:58:47 +02:00
Alejandro Celaya
9ee2064ba1 Merge pull request #2 from acelaya/feature/46
Feature/46
2016-08-09 14:21:43 +02:00
Alejandro Celaya
90cef7d4d9 Removed unused import 2016-08-09 14:19:46 +02:00
Alejandro Celaya
12410e82d8 Created tests for QrCode middlewares 2016-08-09 14:18:20 +02:00
Alejandro Celaya
18084433c7 Created middleware to cache generated QR codes 2016-08-09 13:41:30 +02:00
Alejandro Celaya
8eb279fd28 Updated UrlShortener to namespace the cache entries 2016-08-09 13:32:33 +02:00
Alejandro Celaya
99b7c77997 Created action to generate QR codes 2016-08-09 10:25:30 +02:00
Alejandro Celaya
166c94cac7 Merge branch 'feature/40' into develop 2016-08-09 09:13:48 +02:00
Alejandro Celaya
7c5d8cf244 Fixed VisitsTracker to take into account the X-Forwarded-For header in case the server is behind a load balabncer or proxy 2016-08-09 09:13:39 +02:00
Alejandro Celaya
73a236b3d0 Updated VisitsTracker so that the track method expects a Request object to be provided 2016-08-09 08:52:06 +02:00
Alejandro Celaya
34753ca7d3 Added logger to classes that catch errors in order to log them 2016-08-08 12:33:58 +02:00
Alejandro Celaya
fff058f44b Created LoggerFactoryTest 2016-08-08 12:07:04 +02:00
Alejandro Celaya
b7f3c332e4 Created Logger factory and logger config, and added logger dependencies 2016-08-08 11:56:19 +02:00
Alejandro Celaya
cff9b7c0b5 Deleted docs which are now in Shlink's website 2016-08-08 11:17:14 +02:00
Alejandro Celaya
63e867cf4b Updated vendor name on error pages 2016-08-08 10:08:34 +02:00
Alejandro Celaya
f49e9064cd Added cache adapter to the UrlShortener service to cache shortcode-url maps 2016-08-08 10:02:52 +02:00
Alejandro Celaya
3bd4f506e0 Updated status returned in REST endpoints to be 404 when something is not found 2016-08-08 09:46:40 +02:00
Alejandro Celaya
93713689d7 Merge branch 'feature/35' into develop 2016-08-08 09:39:15 +02:00
Alejandro Celaya
ecd2e6e759 Updated namespace for Visit CLI commands 2016-08-08 09:38:50 +02:00
Alejandro Celaya
a65003803b Updated namespace for Shortcode CLI commands 2016-08-08 09:36:52 +02:00
Alejandro Celaya
0a4f8c3b0a Merge pull request #1 from acelaya/feature/13
Feature/13
2016-08-07 21:15:59 +02:00
Alejandro Celaya
80d8c32881 Removed rest auth env vars 2016-08-07 20:47:43 +02:00
Alejandro Celaya
57bc681b9e Created command to generate a random secret key string 2016-08-07 20:30:19 +02:00
Alejandro Celaya
2a089f05b1 Updated languages 2016-08-07 20:21:38 +02:00
Alejandro Celaya
258f954a38 Deleted rest token related classes 2016-08-07 19:57:23 +02:00
Alejandro Celaya
7b0beb3b8c Updated CheckAuthenticationMiddleware to work with JWT and the Authorization header 2016-08-07 19:53:14 +02:00
Alejandro Celaya
9573e9f4ef Updated AuthenticateAction to generate and return a JWT 2016-08-07 19:13:40 +02:00
Alejandro Celaya
a60080b1ce Created JWTService and related classes 2016-08-07 14:44:33 +02:00
Alejandro Celaya
1d92e87d50 Updated AuthenticateAction to use the APiKeyService instead of the RestTokenService 2016-08-07 10:26:34 +02:00
Alejandro Celaya
289db45f27 Created ListKeysCommand 2016-08-06 18:50:50 +02:00
Alejandro Celaya
c5382b2a7f Created DisableKeyCommand 2016-08-06 18:26:07 +02:00
Alejandro Celaya
dd1bc49b79 Added method to ApiKeyService to list api keys 2016-08-06 18:08:09 +02:00
Alejandro Celaya
74777c2234 Created command to generate a new api key 2016-08-06 18:07:48 +02:00
Alejandro Celaya
99d7e6dd7d Fixed AuthenticateAction not working with only one group of params 2016-08-06 13:24:06 +02:00
Alejandro Celaya
7b746f76b0 Created APiKeyService and tests 2016-08-06 13:18:27 +02:00
Alejandro Celaya
2767a14101 Created ApiKey entity 2016-08-06 12:50:44 +02:00
Alejandro Celaya
270dbc6028 Created new entity_manager configuration, dropping old database first level config key 2016-08-06 12:40:31 +02:00
Alejandro Celaya
7b1b00901a Created phpstorm meta fle to get ContainerInterop typehint based on service name 2016-08-05 07:20:40 +02:00
Alejandro Celaya
9ab4b9ab43 Merge branch 'develop' 2016-08-01 21:30:49 +02:00
Alejandro Celaya
5e493b435a Added badges to readme file 2016-08-01 21:26:47 +02:00
Alejandro Celaya
21dc9ac5a0 Updated changelog for version 1 2016-08-01 21:25:15 +02:00
Alejandro Celaya
40a3ba6203 Improved rest documentation 2016-08-01 21:15:30 +02:00
Alejandro Celaya
307dfc64b4 Created installation steps doc 2016-08-01 21:13:17 +02:00
Alejandro Celaya
9b9b1415fe Created GenerateCharsetCommand 2016-08-01 21:11:42 +02:00
Alejandro Celaya
ce2b28a0b4 Improved cli entry point so that the language is set to the translator based on the CLI_LANGUAGE env var 2016-08-01 20:44:45 +02:00
Alejandro Celaya
7b98527f2e Improved CacheFactory so that adapter can be set in config 2016-08-01 20:16:13 +02:00
Alejandro Celaya
30988b10d1 Added Laravel's env helper 2016-08-01 14:36:43 +02:00
Alejandro Celaya
d73d3049b7 Removed dependency on expressive-helpers package 2016-07-31 16:42:19 +02:00
Alejandro Celaya
7b1e855e7f Renamed services config files to dependencies 2016-07-31 16:32:27 +02:00
Alejandro Celaya
a957f66ed0 Renamed services first level config key to dependencies 2016-07-31 16:30:05 +02:00
Alejandro Celaya
ce00874dd1 Merge branch 'feature/12' into develop 2016-07-31 16:25:11 +02:00
Alejandro Celaya
3d5e5d5df9 Created ResolveUrlActionTest 2016-07-31 16:24:00 +02:00
Alejandro Celaya
08f6d2de78 Created ListShortcodesActionTest 2016-07-31 16:16:26 +02:00
Alejandro Celaya
c6b7515285 Created GetVisitsActionTest 2016-07-31 16:10:16 +02:00
Alejandro Celaya
04e0a192ad Created CreateShortcodeActionTest 2016-07-31 15:58:18 +02:00
Alejandro Celaya
878518ced7 Created AuthenticateActionTest 2016-07-31 13:33:55 +02:00
Alejandro Celaya
6f7e4f7e7f Created RestUtilsTest 2016-07-31 13:18:36 +02:00
Alejandro Celaya
f701e65f75 Created RestTokenServiceTest 2016-07-31 13:14:06 +02:00
Alejandro Celaya
ef6f4fba66 Improved CheckAuthenticationMiddlewareTest 2016-07-31 13:01:08 +02:00
Alejandro Celaya
f904f79c18 Created CheckAuthenticationMiddlewareTest 2016-07-30 23:26:49 +02:00
Alejandro Celaya
41939b790d Added more Rest module tests 2016-07-30 23:17:13 +02:00
Alejandro Celaya
8c446f0f3b Created Core\ConfigProviderTest 2016-07-30 23:07:44 +02:00
Alejandro Celaya
9c6420fe26 Created VisitServiceTest 2016-07-30 23:01:07 +02:00
Alejandro Celaya
ce4877d4ac Improved VisitsTrackerTest 2016-07-30 22:55:28 +02:00
Alejandro Celaya
ebeaa3c64a Created RedirectActionTest 2016-07-30 20:02:48 +02:00
Alejandro Celaya
fcdcfde04f Added missing tests for Common module 2016-07-30 17:45:48 +02:00
Alejandro Celaya
2ce6c1f44b Added more tests to Common module 2016-07-30 17:17:21 +02:00
Alejandro Celaya
00db8a7ea5 Created Common\ErrorHandler tests 2016-07-30 17:07:35 +02:00
Alejandro Celaya
4c6cc9cd11 Created ResolveUrlCommandTest 2016-07-30 16:48:02 +02:00
Alejandro Celaya
50f1549457 Created ProcessVisitsCommand 2016-07-30 14:42:09 +02:00
Alejandro Celaya
3923bf0604 Created GetVisitsCommandTest 2016-07-30 14:30:30 +02:00
Alejandro Celaya
bb8404040a Created GenerateShortcodeCommandTest 2016-07-30 14:13:28 +02:00
Alejandro Celaya
5cdd782ce1 Removed whiteline 2016-07-30 13:54:00 +02:00
Alejandro Celaya
e345c2bbfe Moved error handler classes from Expressive namespace to ErrorHandler namespace 2016-07-30 13:51:52 +02:00
Alejandro Celaya
2a018f5415 Fixed ContentBasedErrorHandler fatching error handlers from the composed plugin manager 2016-07-30 10:47:29 +02:00
Alejandro Celaya
7394424a43 Merge branch 'develop' into feature/12 2016-07-30 10:41:05 +02:00
Alejandro Celaya
2f5119d0b3 Split ContentBasedErrorHandler responsibilities into two separated classes 2016-07-30 10:08:34 +02:00
Alejandro Celaya
c569cef239 Fixed ContentBased error handler not using the default content if accepted contents are not valid 2016-07-29 11:25:53 +02:00
Alejandro Celaya
ab6aa99a6d Created more tests on CLI module 2016-07-28 20:49:27 +02:00
Alejandro Celaya
f4532c3015 Replaced exclusive ifs by if-else 2016-07-28 09:56:44 +02:00
Alejandro Celaya
af9193f721 Removed duplicated error handling for 404 errors 2016-07-28 09:40:36 +02:00
Alejandro Celaya
36259588db Fixed Action prefix on routable middlewares 2016-07-27 20:22:50 +02:00
Alejandro Celaya
a3554db1c3 Merge branch 'feature/15' into develop 2016-07-27 20:18:28 +02:00
Alejandro Celaya
75e744838c Created content based error handler which allows managing errors in a different way depending on the Accepted content type from the client 2016-07-27 20:17:23 +02:00
Alejandro Celaya
f3d2cf5e15 Deleted ResponseTypeMiddleware which is not ussable anymore 2016-07-26 19:10:43 +02:00
Alejandro Celaya
a81dba58bd Defined custom NotFoundMiddleware for rest routes 2016-07-26 19:10:01 +02:00
Alejandro Celaya
83f29080c6 Improved the way rest errors are catched 2016-07-26 11:05:17 +02:00
Alejandro Celaya
0ef1e416c6 Created middleware to catch rest errors and return JSON responses 2016-07-26 09:54:13 +02:00
Alejandro Celaya
57d81115de Added PHP 7.1 to the CI matrix 2016-07-21 18:46:52 +02:00
Alejandro Celaya
5c8353da02 Set collation to utf8_bin in shortCode column of ShortUrl so that the UNIQUE key is case sensitive 2016-07-21 18:46:15 +02:00
Alejandro Celaya
95346ebb7e Merge branch 'feature/23' into develop 2016-07-21 17:00:42 +02:00
Alejandro Celaya
cd5bbcd60a Reused middleware to check Accept-Language header on any HTTP related middleware 2016-07-21 16:59:27 +02:00
Alejandro Celaya
fb9c7f8eec Used twig extension to load translations on twig templates 2016-07-21 16:54:00 +02:00
Alejandro Celaya
e42469b090 Added translations for error messages returned by the REST API 2016-07-21 16:41:16 +02:00
Alejandro Celaya
06868f782b Created middleware for rest that reads the language from the Accept-Language header 2016-07-21 16:20:27 +02:00
Alejandro Celaya
73a35a8f44 Added translator and translations to ResolveUrlCommand 2016-07-21 16:01:16 +02:00
Alejandro Celaya
6a05265a48 Added translator and translations to ProcessVisitsCommand 2016-07-21 15:50:27 +02:00
Alejandro Celaya
8e51b51cae Added translator and translations to ListShortcodesCommand 2016-07-21 15:43:19 +02:00
Alejandro Celaya
545fe7da70 Added translator and translations to GetVisitsCommand 2016-07-21 15:35:52 +02:00
Alejandro Celaya
cb99130c1e Created translator and used inside one of the commands 2016-07-21 15:08:46 +02:00
Alejandro Celaya
b413b16c86 Merge branch 'feature/21' into develop 2016-07-21 10:16:52 +02:00
Alejandro Celaya
3ba51c5390 Improved visits REST endpoint path 2016-07-21 10:16:36 +02:00
Alejandro Celaya
0ef9db0bdf Added option to filter by date in visits REST endpoint 2016-07-21 10:13:09 +02:00
Alejandro Celaya
bdd2d6f8b2 Improved DateRange to check if both wrapped dates are empty 2016-07-21 10:03:37 +02:00
Alejandro Celaya
45d194aced Added option to filter by date in shortcode:views CLI command 2016-07-21 09:58:33 +02:00
Alejandro Celaya
0a57f52309 Fixed typo 2016-07-21 09:46:12 +02:00
Alejandro Celaya
d97287ab0c Added option to filter by date the visits list 2016-07-21 09:36:38 +02:00
Alejandro Celaya
84c4021b24 Merge branch 'feature/22' into develop 2016-07-20 19:04:48 +02:00
Alejandro Celaya
d39a949992 Fixed wrong exception name 2016-07-20 19:04:38 +02:00
Alejandro Celaya
dbe1281d2a Created services and command to process visits 2016-07-20 19:00:23 +02:00
Alejandro Celaya
d3c2f4ed2a Created service to resolve IP locations 2016-07-20 12:37:48 +02:00
Alejandro Celaya
06fa33877b Moved some exceptions from core to common 2016-07-20 10:13:53 +02:00
Alejandro Celaya
c290bed354 Created VisitLocation entity 2016-07-20 09:35:46 +02:00
Alejandro Celaya
31af0eea04 Improved main config file and fixed tests whitelist 2016-07-19 23:35:47 +02:00
Alejandro Celaya
aaf4f1dfe5 Improved config loading so that autoloaded overrides module-specific 2016-07-19 23:30:23 +02:00
Alejandro Celaya
e28e984278 Improved CrossDomainMiddleware by allowing the same origin that was requested 2016-07-19 22:38:14 +02:00
Alejandro Celaya
839329d627 Improved CrossDomainMiddleware preventing headers to be injected on non-CORS requests 2016-07-19 20:20:18 +02:00
Alejandro Celaya
0daa24739d Merge branch 'feature/11' into develop 2016-07-19 18:34:02 +02:00
Alejandro Celaya
7ca52ecff9 Removed wrong use statement from old namespace 2016-07-19 18:33:45 +02:00
Alejandro Celaya
39598d8608 Moved templates and templates config to Core module 2016-07-19 18:32:59 +02:00
Alejandro Celaya
923abdf4d2 Added first tests for CLI module 2016-07-19 18:28:21 +02:00
Alejandro Celaya
f917697b8e Added first tests to Rest module 2016-07-19 18:19:05 +02:00
Alejandro Celaya
fe8ef2030f Fixed elements broken on module separation 2016-07-19 18:05:06 +02:00
Alejandro Celaya
ab9c2f728a Created Core module 2016-07-19 18:01:39 +02:00
Alejandro Celaya
ba06ad44bd Created Common module 2016-07-19 17:38:41 +02:00
Alejandro Celaya
8fc88171ee Moved AuthenticationException to Rest module 2016-07-19 17:27:55 +02:00
Alejandro Celaya
7efb3b3a86 Created cli-specific services config file 2016-07-19 17:17:37 +02:00
Alejandro Celaya
170c96b8ef Fixed config files names 2016-07-19 17:12:50 +02:00
Alejandro Celaya
55f954f50f Created Rest module 2016-07-19 17:07:59 +02:00
Alejandro Celaya
95d0beea3c Created CLI module 2016-07-19 16:50:02 +02:00
Alejandro Celaya
5eefaf3071 Added config manager package 2016-07-19 16:30:48 +02:00
Alejandro Celaya
5536d05e99 Added idea folder to gitignore 2016-07-19 16:14:57 +02:00
233 changed files with 9374 additions and 1788 deletions

View File

@@ -1,14 +1,15 @@
# Application
APP_ENV=
SECRET_KEY=
SHORTENED_URL_SCHEMA=
SHORTENED_URL_HOSTNAME=
SHORTCODE_CHARS=
# Language
DEFAULT_LOCALE=
CLI_LOCALE=
# Database
DB_USER=
DB_PASSWORD=
DB_NAME=
# Rest authentication
REST_USER=
REST_PASSWORD=

2
.gitignore vendored
View File

@@ -1,4 +1,6 @@
.idea
build
composer.lock
vendor/
.env
data/database.sqlite

19
.phpstorm.meta.php Normal file
View File

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

1
.travis-php.ini Normal file
View File

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

View File

@@ -8,7 +8,9 @@ branches:
php:
- 5.6
- 7
- hhvm
- 7.1
before_install: phpenv config-add .travis-php.ini
before_script:
- composer self-update

View File

@@ -1,5 +1,86 @@
## CHANGELOG
### 1.2.1
**Bugs**
* [62: Fix cross-domain requests in REST API](https://github.com/acelaya/url-shortener/issues/62)
### 1.2.0
**Features**
* [45: Allow to define tags on short codes, to improve filtering and classification](https://github.com/acelaya/url-shortener/issues/45)
* [7: Add website previews while listing available URLs](https://github.com/acelaya/url-shortener/issues/7)
**Enhancements:**
* [57: Add database migrations system to improve updating between versions](https://github.com/acelaya/url-shortener/issues/57)
* [31: Add support for other database management systems by improving the EntityManager factory](https://github.com/acelaya/url-shortener/issues/31)
* [51: Generate build process to paquetize the app and ease distribution](https://github.com/acelaya/url-shortener/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/acelaya/url-shortener/issues/38)
**Tasks**
* [55: Create update script which does not try to create a new database](https://github.com/acelaya/url-shortener/issues/55)
* [54: Add cache namespace to prevent name collisions with other apps in the same environment](https://github.com/acelaya/url-shortener/issues/54)
* [29: Use the acelaya/ze-content-based-error-handler package instead of custom error handler implementation](https://github.com/acelaya/url-shortener/issues/29)
**Bugs**
* [53: Fix entities database interoperability](https://github.com/acelaya/url-shortener/issues/53)
* [52: Add missing htaccess file for apache environments](https://github.com/acelaya/url-shortener/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)
**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)
**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)
**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)
### 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)
**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)
**Bugs**
* [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/acelaya/url-shortener/issues/24)
### 0.2.0
**Enhancements:**

View File

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

View File

@@ -6,5 +6,6 @@ use Symfony\Component\Console\Application as CliApp;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/../config/container.php';
/** @var CliApp $app */
$app = $container->get(CliApp::class);
$app->run();

14
bin/install Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env php
<?php
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
use Symfony\Component\Console\Application;
use Zend\Config\Writer\PhpArray;
chdir(dirname(__DIR__));
require __DIR__ . '/../vendor/autoload.php';
$app = new Application();
$app->add(new InstallCommand(new PhpArray()));
$app->setDefaultCommand('shlink:install');
$app->run();

14
bin/update Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env php
<?php
use Shlinkio\Shlink\CLI\Command\Install\UpdateCommand;
use Symfony\Component\Console\Application;
use Zend\Config\Writer\PhpArray;
chdir(dirname(__DIR__));
require __DIR__ . '/../vendor/autoload.php';
$app = new Application();
$app->add(new UpdateCommand(new PhpArray()));
$app->setDefaultCommand('shlink:install');
$app->run();

BIN
bin/wkhtmltoimage Executable file

Binary file not shown.

44
build.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/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_build_tmp')
projectdir=$(pwd)
# Copy project content to temp dir
echo 'Copying project files...'
rm -rf "${builtcontent}"
mkdir "${builtcontent}"
cp -R "${projectdir}"/* "${builtcontent}"
cd "${builtcontent}"
# Install dependencies
rm -r vendor
rm composer.lock
composer self-update
composer install --no-dev --optimize-autoloader
# Delete development files
echo 'Deleting dev files...'
rm build.sh
rm CHANGELOG.md
rm composer.*
rm LICENSE
rm php*
rm README.md
rm -r build
rm -f data/database.sqlite
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 .
rm -rf "${builtcontent}"

View File

@@ -1,11 +1,12 @@
{
"name": "acelaya/url-shortener",
"name": "shlinkio/shlink",
"type": "project",
"homepage": "https://github.com/acelaya/url-shortener",
"homepage": "http://shlink.io",
"description": "A self-hosted and PHP-based URL shortener application with CLI and REST interfaces",
"license": "MIT",
"authors": [
{
"name": "Alejandro Celaya ALastrué",
"name": "Alejandro Celaya Alastrué",
"homepage": "http://www.alejandrocelaya.com",
"email": "alejandro@alejandrocelaya.com"
}
@@ -13,16 +14,27 @@
"require": {
"php": "^5.6 || ^7.0",
"zendframework/zend-expressive": "^1.0",
"zendframework/zend-expressive-helpers": "^2.0",
"zendframework/zend-expressive-fastroute": "^1.1",
"zendframework/zend-expressive-twigrenderer": "^1.0",
"zendframework/zend-stdlib": "^2.7",
"zendframework/zend-servicemanager": "^3.0",
"zendframework/zend-paginator": "^2.6",
"zendframework/zend-config": "^2.6",
"zendframework/zend-i18n": "^2.7",
"mtymek/expressive-config-manager": "^0.4",
"acelaya/zsm-annotated-services": "^0.2.0",
"acelaya/ze-content-based-error-handler": "^1.0",
"doctrine/orm": "^2.5",
"guzzlehttp/guzzle": "^6.2",
"acelaya/zsm-annotated-services": "^0.2.0",
"symfony/console": "^3.0"
"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",
"mikehaertl/phpwkhtmltopdf": "^2.2",
"doctrine/migrations": "^1.4"
},
"require-dev": {
"phpunit/phpunit": "^5.0",
@@ -34,12 +46,21 @@
},
"autoload": {
"psr-4": {
"Acelaya\\UrlShortener\\": "src"
}
"Shlinkio\\Shlink\\CLI\\": "module/CLI/src",
"Shlinkio\\Shlink\\Rest\\": "module/Rest/src",
"Shlinkio\\Shlink\\Core\\": "module/Core/src",
"Shlinkio\\Shlink\\Common\\": "module/Common/src"
},
"files": [
"module/Common/functions/functions.php"
]
},
"autoload-dev": {
"psr-4": {
"AcelayaTest\\UrlShortener\\": "tests"
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
"ShlinkioTest\\Shlink\\Common\\": "module/Common/test"
}
},
"scripts": {

View File

@@ -0,0 +1,10 @@
<?php
return [
'app_options' => [
'name' => 'Shlink',
'version' => '1.2.0',
'secret_key' => env('SECRET_KEY'),
],
];

View File

@@ -1,15 +0,0 @@
<?php
use Acelaya\UrlShortener\CLI\Command;
return [
'cli' => [
'commands' => [
Command\GenerateShortcodeCommand::class,
Command\ResolveUrlCommand::class,
Command\ListShortcodesCommand::class,
Command\GetVisitsCommand::class,
]
],
];

View File

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

View File

@@ -0,0 +1,22 @@
<?php
use Zend\Expressive;
use Zend\Expressive\Container;
use Zend\Expressive\Router;
use Zend\Expressive\Template;
use Zend\Expressive\Twig;
use Zend\ServiceManager\Factory\InvokableFactory;
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,
],
],
];

View File

@@ -0,0 +1,20 @@
<?php
return [
'entity_manager' => [
'orm' => [
'proxies_dir' => 'data/proxies',
],
'connection' => [
'driver' => 'pdo_mysql',
'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'dbname' => env('DB_NAME', 'shlink'),
'charset' => 'utf8',
'driverOptions' => [
PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
],
],
],
];

View File

@@ -1,14 +1,13 @@
<?php
use Acelaya\ExpressiveErrorHandler\ErrorHandler\ContentBasedErrorHandler;
use Zend\Expressive\Container\WhoopsErrorHandlerFactory;
return [
'services' => [
'dependencies' => [
'invokables' => [
'Zend\Expressive\Whoops' => Whoops\Run::class,
'Zend\Expressive\WhoopsPageHandler' => Whoops\Handler\PrettyPageHandler::class,
],
'factories' => [
'Zend\Expressive\FinalHandler' => Zend\Expressive\Container\WhoopsErrorHandlerFactory::class,
],
],
'whoops' => [
@@ -18,4 +17,12 @@ return [
'ajax_only' => true,
],
],
'error_handler' => [
'plugins' => [
'factories' => [
ContentBasedErrorHandler::DEFAULT_CONTENT => WhoopsErrorHandlerFactory::class,
],
],
],
];

View File

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

View File

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

View File

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

View File

@@ -1,18 +1,9 @@
<?php
use Acelaya\UrlShortener\Middleware;
use Zend\Expressive\Container\ApplicationFactory;
use Zend\Expressive\Helper;
return [
'middleware_pipeline' => [
'always' => [
'middleware' => [
Helper\ServerUrlMiddleware::class,
],
'priority' => 10000,
],
'routing' => [
'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE,
@@ -20,27 +11,11 @@ return [
'priority' => 10,
],
'rest' => [
'path' => '/rest',
'middleware' => [
Middleware\CheckAuthenticationMiddleware::class,
Middleware\CrossDomainMiddleware::class,
],
'priority' => 5,
],
'post-routing' => [
'middleware' => [
Helper\UrlHelperMiddleware::class,
ApplicationFactory::DISPATCH_MIDDLEWARE,
],
'priority' => 1,
],
'error' => [
'middleware' => [],
'error' => true,
'priority' => -10000,
],
],
];

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

@@ -1,9 +0,0 @@
<?php
return [
'rest' => [
'username' => getenv('REST_USER'),
'password' => getenv('REST_PASSWORD'),
],
];

View File

@@ -1,70 +0,0 @@
<?php
use Acelaya\UrlShortener\CLI;
use Acelaya\UrlShortener\Factory\CacheFactory;
use Acelaya\UrlShortener\Factory\EntityManagerFactory;
use Acelaya\UrlShortener\Middleware;
use Acelaya\UrlShortener\Service;
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Console;
use Zend\Expressive;
use Zend\Expressive\Container;
use Zend\Expressive\Helper;
use Zend\Expressive\Router;
use Zend\Expressive\Template;
use Zend\Expressive\Twig;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
'services' => [
'factories' => [
Expressive\Application::class => Container\ApplicationFactory::class,
Console\Application::class => CLI\Factory\ApplicationFactory::class,
// Url helpers
Helper\UrlHelper::class => Helper\UrlHelperFactory::class,
Helper\ServerUrlMiddleware::class => Helper\ServerUrlMiddlewareFactory::class,
Helper\UrlHelperMiddleware::class => Helper\UrlHelperMiddlewareFactory::class,
Helper\ServerUrlHelper::class => InvokableFactory::class,
Router\FastRouteRouter::class => InvokableFactory::class,
// View
'Zend\Expressive\FinalHandler' => Container\TemplatedErrorHandlerFactory::class,
Template\TemplateRendererInterface::class => Twig\TwigRendererFactory::class,
// Services
EntityManager::class => EntityManagerFactory::class,
GuzzleHttp\Client::class => InvokableFactory::class,
Service\UrlShortener::class => AnnotatedFactory::class,
Service\VisitsTracker::class => AnnotatedFactory::class,
Service\ShortUrlService::class => AnnotatedFactory::class,
Service\RestTokenService::class => AnnotatedFactory::class,
Cache::class => CacheFactory::class,
// Cli commands
CLI\Command\GenerateShortcodeCommand::class => AnnotatedFactory::class,
CLI\Command\ResolveUrlCommand::class => AnnotatedFactory::class,
CLI\Command\ListShortcodesCommand::class => AnnotatedFactory::class,
CLI\Command\GetVisitsCommand::class => AnnotatedFactory::class,
// Middleware
Middleware\Routable\RedirectMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\AuthenticateMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\CreateShortcodeMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\ResolveUrlMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\GetVisitsMiddleware::class => AnnotatedFactory::class,
Middleware\Rest\ListShortcodesMiddleware::class => AnnotatedFactory::class,
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class,
],
'aliases' => [
'em' => EntityManager::class,
'httpClient' => GuzzleHttp\Client::class,
Router\RouterInterface::class => Router\FastRouteRouter::class,
AnnotatedFactory::CACHE_SERVICE => Cache::class,
]
],
];

View File

@@ -2,12 +2,6 @@
return [
'templates' => [
'paths' => [
'templates'
],
],
'twig' => [
'cache_dir' => 'data/cache/twig',
'extensions' => [

View File

@@ -0,0 +1,8 @@
<?php
return [
'translator' => [
'locale' => env('DEFAULT_LOCALE', 'en'),
],
];

View File

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

View File

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

View File

@@ -4,7 +4,7 @@ use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Interop\Container\ContainerInterface;
/** @var ContainerInterface $container */
$container = include __DIR__ . '/config/container.php';
$container = include __DIR__ . '/container.php';
/** @var EntityManager $em */
$em = $container->get(EntityManager::class);

View File

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

View File

@@ -16,6 +16,6 @@ if (class_exists(Dotenv::class)) {
// Build container
$config = require __DIR__ . '/config.php';
$container = new ServiceManager($config['services']);
$container = new ServiceManager($config['dependencies']);
$container->setService('config', $config);
return $container;

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

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

View File

@@ -1,278 +0,0 @@
# REST API documentation
## Error management
Statuses:
* 400 -> controlled error
* 401 -> authentication error
* 500 -> unexpected error
[TODO]
## Authentication
[TODO]
## Endpoints
#### Authenticate
**REQUEST**
* `POST` -> `/rest/authenticate`
* Params:
* username: `string`
* password: `string`
**SUCCESS RESPONSE**
```json
{
"token": "9f741eb0-33d7-4c56-b8f7-3719e9929946"
}
```
**ERROR RESPONSE**
```json
{
"error": "INVALID_ARGUMENT",
"message": "You have to provide both \"username\" and \"password\""
}
```
Posible errors:
* **INVALID_ARGUMENT**: Username or password were not provided.
* **INVALID_CREDENTIALS**: Username or password are incorrect.
#### Create shortcode
**REQUEST**
* `POST` -> `/rest/short-codes`
* Params:
* longUrl: `string` -> The URL to shorten
* Headers:
* X-Auth-Token: `string` -> The token provided in the authentication request
**SUCCESS RESPONSE**
```json
{
"longUrl": "https://www.facebook.com/something/something",
"shortUrl": "https://doma.in/rY9Kr",
"shortCode": "rY9Kr"
}
```
**ERROR RESPONSE**
```json
{
"error": "INVALID_URL",
"message": "Provided URL \"wfwef\" is invalid. Try with a different one."
}
```
Posible errors:
* **INVALID_ARGUMENT**: The longUrl was not provided.
* **INVALID_URL**: Provided longUrl has an invalid format or does not resolve.
* **UNKNOWN_ERROR**: Something unexpected happened.
#### Resolve URL
**REQUEST**
* `GET` -> `/rest/short-codes/{shortCode}`
* Route params:
* shortCode: `string` -> The short code we want to resolve
* Headers:
* X-Auth-Token: `string` -> The token provided in the authentication request
**SUCCESS RESPONSE**
```json
{
"longUrl": "https://www.facebook.com/something/something"
}
```
**ERROR RESPONSE**
```json
{
"error": "INVALID_SHORTCODE",
"message": "Provided short code \"abc123\" has an invalid format"
}
```
Posible errors:
* **INVALID_ARGUMENT**: No longUrl was found for provided shortCode.
* **INVALID_SHORTCODE**: Provided shortCode does not match the character set used by the app to generate short codes.
* **UNKNOWN_ERROR**: Something unexpected happened.
#### List shortened URLs
**REQUEST**
* `GET` -> `/rest/short-codes`
* Query params:
* page: `integer` -> The page to list. Defaults to 1 if not provided.
* Headers:
* X-Auth-Token: `string` -> The token provided in the authentication request
**SUCCESS RESPONSE**
```json
{
"shortUrls": {
"data": [
{
"shortCode": "abc123",
"originalUrl": "http://www.alejandrocelaya.com",
"dateCreated": "2016-04-30T18:01:47+0200",
"visitsCount": 4
},
{
"shortCode": "def456",
"originalUrl": "http://www.alejandrocelaya.com/en",
"dateCreated": "2016-04-30T18:03:43+0200",
"visitsCount": 0
},
{
"shortCode": "ghi789",
"originalUrl": "http://www.alejandrocelaya.com/es",
"dateCreated": "2016-04-30T18:10:38+0200",
"visitsCount": 0
},
{
"shortCode": "jkl987",
"originalUrl": "http://www.alejandrocelaya.com/es/",
"dateCreated": "2016-04-30T18:10:57+0200",
"visitsCount": 0
},
{
"shortCode": "mno654",
"originalUrl": "http://blog.alejandrocelaya.com/2016/04/09/improving-zend-service-manager-workflow-with-annotations/",
"dateCreated": "2016-04-30T19:21:05+0200",
"visitsCount": 1
},
{
"shortCode": "pqr321",
"originalUrl": "http://www.google.com",
"dateCreated": "2016-05-01T11:19:53+0200",
"visitsCount": 0
},
{
"shortCode": "stv159",
"originalUrl": "http://www.acelaya.com",
"dateCreated": "2016-06-12T17:49:21+0200",
"visitsCount": 0
},
{
"shortCode": "wxy753",
"originalUrl": "http://www.atomic-reader.com",
"dateCreated": "2016-06-12T17:50:27+0200",
"visitsCount": 0
},
{
"shortCode": "zab852",
"originalUrl": "http://foo.com",
"dateCreated": "2016-07-03T09:07:36+0200",
"visitsCount": 0
},
{
"shortCode": "cde963",
"originalUrl": "https://www.facebook.com.com",
"dateCreated": "2016-07-03T09:12:35+0200",
"visitsCount": 0
}
],
"pagination": {
"currentPage": 4,
"pagesCount": 15
}
}
}
```
**ERROR RESPONSE**
```json
{
"error": "UNKNOWN_ERROR",
"message": "Unexpected error occured"
}
```
Posible errors:
* **UNKNOWN_ERROR**: Something unexpected happened.
#### Get visits
**REQUEST**
* `GET` -> `/rest/visits/{shortCode}`
* Route params:
* shortCode: `string` -> The shortCode from which we eant to get the visits.
* Headers:
* X-Auth-Token: `string` -> The token provided in the authentication request
**SUCCESS RESPONSE**
```json
{
"shortUrls": {
"data": [
{
"referer": null,
"date": "2016-06-18T09:32:22+0200",
"remoteAddr": "127.0.0.1",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
},
{
"referer": null,
"date": "2016-04-30T19:20:06+0200",
"remoteAddr": "127.0.0.1",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
},
{
"referer": "google.com",
"date": "2016-04-30T19:19:57+0200",
"remoteAddr": "1.2.3.4",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
},
{
"referer": null,
"date": "2016-04-30T19:17:35+0200",
"remoteAddr": "127.0.0.1",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/49.0.2623.87 Safari/537.36"
}
],
}
}
```
**ERROR RESPONSE**
```json
{
"error": "INVALID_ARGUMENT",
"message": "Provided short code \"abc123\" is invalid"
}
```
Posible errors:
* **INVALID_ARGUMENT**: The shortcode does not belong to any short URL
* **UNKNOWN_ERROR**: Something unexpected happened.

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

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

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

4
migrations.yml Normal file
View File

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

View File

@@ -0,0 +1,23 @@
<?php
use Shlinkio\Shlink\CLI\Command;
return [
'cli' => [
'locale' => 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,
]
],
];

View File

@@ -0,0 +1,27 @@
<?php
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Shlinkio\Shlink\CLI\Command;
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
use Symfony\Component\Console\Application;
return [
'dependencies' => [
'factories' => [
Application::class => ApplicationFactory::class,
Command\Shortcode\GenerateShortcodeCommand::class => AnnotatedFactory::class,
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,
],
],
];

View File

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

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

Binary file not shown.

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

@@ -0,0 +1,224 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2016-08-21 18:16+0200\n"
"PO-Revision-Date: 2016-08-21 18:16+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-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
"X-Poedit-KeywordsList: translate;translatePlural\n"
"X-Poedit-SearchPath-0: src\n"
"X-Poedit-SearchPath-1: config\n"
msgid "Disables an API key."
msgstr "Desahbilita una clave de API."
msgid "The API key to disable"
msgstr "La clave de API a deshabilitar"
#, php-format
msgid "API key %s properly disabled"
msgstr "Clave de API %s deshabilitada correctamente"
#, php-format
msgid "API key \"%s\" does not exist."
msgstr "La clave de API \"%s\" no existe."
msgid "Generates a new valid API key."
msgstr "Genera una nueva clave de API válida."
msgid "The date in which the API key should expire. Use any valid PHP format."
msgstr ""
"La fecha en la que la clave de API debe expirar. Utiliza cualquier valor "
"válido en PHP."
msgid "Generated API key"
msgstr "Generada clave de API"
msgid "Lists all the available API keys."
msgstr "Lista todas las claves de API disponibles."
msgid "Tells if only enabled API keys should be returned."
msgstr "Define si sólo las claves de API habilitadas deben ser devueltas."
msgid "Key"
msgstr "Clave"
msgid "Expiration date"
msgstr "Fecha de caducidad"
msgid "Is enabled"
msgstr "Está habilitada"
#, php-format
msgid ""
"Generates a character set sample just by shuffling the default one, \"%s\". "
"Then it can be set in the SHORTCODE_CHARS environment variable"
msgstr ""
"Genera un grupo de caracteres simplemente mexclando el grupo por defecto \"%s"
"\". Después puede ser utilizado en la variable de entrono SHORTCODE_CHARS"
msgid "Character set:"
msgstr "Grupo de caracteres:"
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"
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?"
msgid "A URL was not provided!"
msgstr "¡No se ha proporcionado una URL!"
msgid "Processed URL:"
msgstr "URL procesada:"
msgid "Generated URL:"
msgstr "URL generada:"
#, php-format
msgid "Provided URL \"%s\" is invalid. Try with a different one."
msgstr "La URL proporcionada \"%s\" e inválida. Prueba con una diferente."
msgid "Returns the detailed visits information for provided short code"
msgstr ""
"Devuelve la información detallada de visitas para el código corto "
"proporcionado"
msgid "The short code which visits we want to get"
msgstr "El código corto del cual queremos obtener las visitas"
msgid "Allows to filter visits, returning only those older than start date"
msgstr ""
"Permite filtrar las visitas, devolviendo sólo aquellas más antiguas que "
"startDate"
msgid "Allows to filter visits, returning only those newer than end date"
msgstr ""
"Permite filtrar las visitas, devolviendo sólo aquellas más nuevas que endDate"
msgid "A short code was not provided. Which short code do you want to use?:"
msgstr "No se prporcionó un código corto. ¿Qué código corto deseas usar?"
msgid "Referer"
msgstr "Origen"
msgid "Date"
msgstr "Fecha"
msgid "Remote Address"
msgstr "Dirección remota"
msgid "User agent"
msgstr "Agente de usuario"
msgid "List all short URLs"
msgstr "Listar todas las URLs cortas"
#, php-format
msgid "The first page to list (%s items per page)"
msgstr "La primera página a listar (%s elementos por página)"
msgid "Whether to display the tags or not"
msgstr "Si se desea mostrar las etiquetas o no"
msgid "Short code"
msgstr "Código corto"
msgid "Original URL"
msgstr "URL original"
msgid "Date created"
msgstr "Fecha de creación"
msgid "Visits count"
msgstr "Número de visitas"
msgid "Tags"
msgstr "Etiquetas"
msgid "You have reached last page"
msgstr "Has alcanzado la última página"
msgid "Continue with page"
msgstr "Continuar con la página"
msgid "Returns the long URL behind a short code"
msgstr "Devuelve la URL larga detrás de un código corto"
msgid "The short code to parse"
msgstr "El código corto a convertir"
msgid "A short code was not provided. Which short code do you want to parse?:"
msgstr ""
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?"
#, php-format
msgid "No URL found for short code \"%s\""
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
msgid "Long URL:"
msgstr "URL larga:"
#, php-format
msgid "Provided short code \"%s\" has an invalid format."
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
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

@@ -0,0 +1,62 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Api;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Zend\I18n\Translator\TranslatorInterface;
class DisableKeyCommand extends Command
{
/**
* @var ApiKeyServiceInterface
*/
private $apiKeyService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* DisableKeyCommand constructor.
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
* @param TranslatorInterface $translator
*
* @Inject({ApiKeyService::class, "translator"})
*/
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
{
$this->apiKeyService = $apiKeyService;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('api-key:disable')
->setDescription($this->translator->translate('Disables an API key.'))
->addArgument('apiKey', InputArgument::REQUIRED, $this->translator->translate('The API key to disable'));
}
public function execute(InputInterface $input, OutputInterface $output)
{
$apiKey = $input->getArgument('apiKey');
try {
$this->apiKeyService->disable($apiKey);
$output->writeln(sprintf(
$this->translator->translate('API key %s properly disabled'),
'<info>' . $apiKey . '</info>'
));
} catch (\InvalidArgumentException $e) {
$output->writeln(sprintf(
'<error>' . $this->translator->translate('API key "%s" does not exist.') . '</error>',
$apiKey
));
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Api;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Zend\I18n\Translator\TranslatorInterface;
class GenerateKeyCommand extends Command
{
/**
* @var ApiKeyServiceInterface
*/
private $apiKeyService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* GenerateKeyCommand constructor.
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
* @param TranslatorInterface $translator
*
* @Inject({ApiKeyService::class, "translator"})
*/
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
{
$this->apiKeyService = $apiKeyService;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('api-key:generate')
->setDescription($this->translator->translate('Generates a new valid API key.'))
->addOption(
'expirationDate',
'e',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('The date in which the API key should expire. Use any valid PHP format.')
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$expirationDate = $input->getOption('expirationDate');
$apiKey = $this->apiKeyService->create(isset($expirationDate) ? new \DateTime($expirationDate) : null);
$output->writeln($this->translator->translate('Generated API key') . sprintf(': <info>%s</info>', $apiKey));
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Api;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Zend\I18n\Translator\TranslatorInterface;
class ListKeysCommand extends Command
{
/**
* @var ApiKeyServiceInterface
*/
private $apiKeyService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ListKeysCommand constructor.
* @param ApiKeyServiceInterface|ApiKeyService $apiKeyService
* @param TranslatorInterface $translator
*
* @Inject({ApiKeyService::class, "translator"})
*/
public function __construct(ApiKeyServiceInterface $apiKeyService, TranslatorInterface $translator)
{
$this->apiKeyService = $apiKeyService;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('api-key:list')
->setDescription($this->translator->translate('Lists all the available API keys.'))
->addOption(
'enabledOnly',
null,
InputOption::VALUE_NONE,
$this->translator->translate('Tells if only enabled API keys should be returned.')
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$enabledOnly = $input->getOption('enabledOnly');
$list = $this->apiKeyService->listKeys($enabledOnly);
$table = new Table($output);
if ($enabledOnly) {
$table->setHeaders([
$this->translator->translate('Key'),
$this->translator->translate('Expiration date'),
]);
} else {
$table->setHeaders([
$this->translator->translate('Key'),
$this->translator->translate('Is enabled'),
$this->translator->translate('Expiration date'),
]);
}
/** @var ApiKey $row */
foreach ($list as $row) {
$key = $row->getKey();
$expiration = $row->getExpirationDate();
$rowData = [];
if ($enabledOnly) {
$rowData[] = $key;
} else {
$rowData[] = $row->isEnabled() ? $this->getSuccessString($key) : $this->getErrorString($key);
$rowData[] = $row->isEnabled() ? $this->getSuccessString('+++') : $this->getErrorString('---');
}
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ISO8601) : '-';
$table->addRow($rowData);
}
$table->render();
}
/**
* @param string $string
* @return string
*/
protected function getErrorString($string)
{
return sprintf('<fg=red>%s</>', $string);
}
/**
* @param string $string
* @return string
*/
protected function getSuccessString($string)
{
return sprintf('<info>%s</info>', $string);
}
}

View File

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

View File

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

View File

@@ -0,0 +1,307 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Install;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Command\Command;
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\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
use Zend\Config\Writer\WriterInterface;
class InstallCommand extends Command
{
use StringUtilsTrait;
const DATABASE_DRIVERS = [
'MySQL' => 'pdo_mysql',
'PostgreSQL' => 'pdo_pgsql',
'SQLite' => 'pdo_sqlite',
];
const SUPPORTED_LANGUAGES = ['en', 'es'];
/**
* @var InputInterface
*/
private $input;
/**
* @var OutputInterface
*/
private $output;
/**
* @var QuestionHelper
*/
private $questionHelper;
/**
* @var ProcessHelper
*/
private $processHelper;
/**
* @var WriterInterface
*/
private $configWriter;
/**
* InstallCommand constructor.
* @param WriterInterface $configWriter
* @param callable|null $databaseCreationLogic
*/
public function __construct(WriterInterface $configWriter)
{
parent::__construct(null);
$this->configWriter = $configWriter;
}
public function configure()
{
$this->setName('shlink:install')
->setDescription('Installs Shlink');
}
public function execute(InputInterface $input, OutputInterface $output)
{
$this->input = $input;
$this->output = $output;
$this->questionHelper = $this->getHelper('question');
$this->processHelper = $this->getHelper('process');
$params = [];
$output->writeln([
'<info>Welcome to Shlink!!</info>',
'This process will guide you through the installation.',
]);
// Check if a cached config file exists and drop it if so
if (file_exists('data/cache/app_config.php')) {
$output->write('Deleting old cached config...');
if (unlink('data/cache/app_config.php')) {
$output->writeln(' <info>Success</info>');
} else {
$output->writeln(
' <error>Failed!</error> You will have to manually delete the data/cache/app_config.php file to get'
. ' new config applied.'
);
}
}
// Ask for custom config params
$params['DATABASE'] = $this->askDatabase();
$params['URL_SHORTENER'] = $this->askUrlShortener();
$params['LANGUAGE'] = $this->askLanguage();
$params['APP'] = $this->askApplication();
// Generate config params files
$config = $this->buildAppConfig($params);
$this->configWriter->toFile('config/params/generated_config.php', $config, false);
$output->writeln(['<info>Custom configuration properly generated!</info>', '']);
// Generate database
if (! $this->createDatabase()) {
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;
}
}
protected function askDatabase()
{
$params = [];
$this->printTitle('DATABASE');
// Select database type
$databases = array_keys(self::DATABASE_DRIVERS);
$dbType = $this->questionHelper->ask($this->input, $this->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('Database name', 'shlink');
$params['USER'] = $this->ask('Database username');
$params['PASSWORD'] = $this->ask('Database password');
}
return $params;
}
protected function askUrlShortener()
{
$this->printTitle('URL SHORTENER');
// Ask for URL shortener params
return [
'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
'<question>Select schema for generated short URLs (defaults to http):</question>',
['http', 'https'],
0
)),
'HOSTNAME' => $this->ask('Hostname for generated URLs'),
'CHARS' => $this->ask(
'Character set for generated short codes (leave empty to autogenerate one)',
null,
true
) ?: str_shuffle(UrlShortener::DEFAULT_CHARS)
];
}
protected function askLanguage()
{
$this->printTitle('LANGUAGE');
return [
'DEFAULT' => $this->questionHelper->ask($this->input, $this->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($this->input, $this->output, new ChoiceQuestion(
'<question>Select default language for CLI executions (defaults to '
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
self::SUPPORTED_LANGUAGES,
0
)),
];
}
protected function askApplication()
{
$this->printTitle('APPLICATION');
return [
'SECRET' => $this->ask(
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
null,
true
) ?: $this->generateRandomString(32),
];
}
/**
* @param string $text
*/
protected function printTitle($text)
{
$text = trim($text);
$length = strlen($text) + 4;
$header = str_repeat('*', $length);
$this->output->writeln([
'',
'<info>' . $header . '</info>',
'<info>* ' . strtoupper($text) . ' *</info>',
'<info>' . $header . '</info>',
]);
}
/**
* @param string $text
* @param string|null $default
* @param bool $allowEmpty
* @return string
*/
protected function ask($text, $default = null, $allowEmpty = false)
{
if (isset($default)) {
$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) && empty($default) && ! $allowEmpty);
return $value;
}
/**
* @param array $params
* @return array
*/
protected function buildAppConfig(array $params)
{
// Build simple config
$config = [
'app_options' => [
'secret_key' => $params['APP']['SECRET'],
],
'entity_manager' => [
'connection' => [
'driver' => $params['DATABASE']['DRIVER'],
],
],
'translator' => [
'locale' => $params['LANGUAGE']['DEFAULT'],
],
'cli' => [
'locale' => $params['LANGUAGE']['CLI'],
],
'url_shortener' => [
'domain' => [
'schema' => $params['URL_SHORTENER']['SCHEMA'],
'hostname' => $params['URL_SHORTENER']['HOSTNAME'],
],
'shortcode_chars' => $params['URL_SHORTENER']['CHARS'],
],
];
// Build dynamic database config
if ($params['DATABASE']['DRIVER'] === 'pdo_sqlite') {
$config['entity_manager']['connection']['path'] = 'data/database.sqlite';
} else {
$config['entity_manager']['connection']['user'] = $params['DATABASE']['USER'];
$config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD'];
$config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME'];
}
return $config;
}
protected function createDatabase()
{
$this->output->writeln('Initializing database...');
return $this->runCommand('php vendor/bin/doctrine.php orm:schema-tool:create', 'Error generating database.');
}
/**
* @param string $command
* @param string $errorMessage
* @return bool
*/
protected function runCommand($command, $errorMessage)
{
$process = $this->processHelper->run($this->output, $command);
if ($process->isSuccessful()) {
$this->output->writeln(' <info>Success!</info>');
return true;
} else {
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,12 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Install;
use Zend\Config\Writer\WriterInterface;
class UpdateCommand extends InstallCommand
{
public function createDatabase()
{
return true;
}
}

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

@@ -0,0 +1,122 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
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;
use Zend\I18n\Translator\TranslatorInterface;
class GenerateShortcodeCommand extends Command
{
/**
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var array
*/
private $domainConfig;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* GenerateShortcodeCommand constructor.
* @param UrlShortenerInterface $urlShortener
* @param TranslatorInterface $translator
* @param array $domainConfig
*
* @Inject({UrlShortener::class, "translator", "config.url_shortener.domain"})
*/
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
array $domainConfig
) {
$this->urlShortener = $urlShortener;
$this->translator = $translator;
$this->domainConfig = $domainConfig;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:generate')
->setDescription(
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
)
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
->addOption(
'tags',
't',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
$this->translator->translate('Tags to apply to the new short URL')
);
}
public function interact(InputInterface $input, OutputInterface $output)
{
$longUrl = $input->getArgument('longUrl');
if (! empty($longUrl)) {
return;
}
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new Question(sprintf(
'<question>%s</question> ',
$this->translator->translate('A long URL was not provided. Which URL do you want to shorten?:')
));
$longUrl = $helper->ask($input, $output, $question);
if (! empty($longUrl)) {
$input->setArgument('longUrl', $longUrl);
}
}
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)) {
$output->writeln(sprintf('<error>%s</error>', $this->translator->translate('A URL was not provided!')));
return;
}
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl), $tags);
$shortUrl = (new Uri())->withPath($shortCode)
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
$output->writeln([
sprintf('%s <info>%s</info>', $this->translator->translate('Processed URL:'), $longUrl),
sprintf('%s <info>%s</info>', $this->translator->translate('Generated URL:'), $shortUrl),
]);
} catch (InvalidUrlException $e) {
$output->writeln(sprintf(
'<error>' . $this->translator->translate(
'Provided URL "%s" is invalid. Try with a different one.'
) . '</error>',
$longUrl
));
}
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\Table;
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\I18n\Translator\TranslatorInterface;
class GetVisitsCommand extends Command
{
/**
* @var VisitsTrackerInterface
*/
private $visitsTracker;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* GetVisitsCommand constructor.
* @param VisitsTrackerInterface $visitsTracker
* @param TranslatorInterface $translator
*
* @Inject({VisitsTracker::class, "translator"})
*/
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
{
$this->visitsTracker = $visitsTracker;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:visits')
->setDescription(
$this->translator->translate('Returns the detailed visits information for provided short code')
)
->addArgument(
'shortCode',
InputArgument::REQUIRED,
$this->translator->translate('The short code which visits we want to get')
)
->addOption(
'startDate',
's',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('Allows to filter visits, returning only those older than start date')
)
->addOption(
'endDate',
'e',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('Allows to filter visits, returning only those newer than end date')
);
}
public function interact(InputInterface $input, OutputInterface $output)
{
$shortCode = $input->getArgument('shortCode');
if (! empty($shortCode)) {
return;
}
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new Question(sprintf(
'<question>%s</question> ',
$this->translator->translate('A short code was not provided. Which short code do you want to use?:')
));
$shortCode = $helper->ask($input, $output, $question);
if (! empty($shortCode)) {
$input->setArgument('shortCode', $shortCode);
}
}
public function execute(InputInterface $input, OutputInterface $output)
{
$shortCode = $input->getArgument('shortCode');
$startDate = $this->getDateOption($input, 'startDate');
$endDate = $this->getDateOption($input, 'endDate');
$visits = $this->visitsTracker->info($shortCode, new DateRange($startDate, $endDate));
$table = new Table($output);
$table->setHeaders([
$this->translator->translate('Referer'),
$this->translator->translate('Date'),
$this->translator->translate('Remote Address'),
$this->translator->translate('User agent'),
]);
foreach ($visits as $row) {
$rowData = $row->jsonSerialize();
// Unset location info
unset($rowData['visitLocation']);
$table->addRow(array_values($rowData));
}
$table->render();
}
protected function getDateOption(InputInterface $input, $key)
{
$value = $input->getOption($key);
if (isset($value)) {
$value = new \DateTime($value);
}
return $value;
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Zend\I18n\Translator\TranslatorInterface;
class ListShortcodesCommand extends Command
{
use PaginatorUtilsTrait;
/**
* @var ShortUrlServiceInterface
*/
private $shortUrlService;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ListShortcodesCommand constructor.
* @param ShortUrlServiceInterface $shortUrlService
* @param TranslatorInterface $translator
*
* @Inject({ShortUrlService::class, "translator"})
*/
public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
{
$this->shortUrlService = $shortUrlService;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:list')
->setDescription($this->translator->translate('List all short URLs'))
->addOption(
'page',
'p',
InputOption::VALUE_OPTIONAL,
sprintf(
$this->translator->translate('The first page to list (%s items per page)'),
PaginableRepositoryAdapter::ITEMS_PER_PAGE
),
1
)
->addOption(
'tags',
't',
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'));
$showTags = $input->getOption('tags');
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
do {
$result = $this->shortUrlService->listShortUrls($page);
$page++;
$table = new Table($output);
$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) {
$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();
if ($this->isLastPage($result)) {
$continue = false;
$output->writeln(
sprintf('<info>%s</info>', $this->translator->translate('You have reached last page'))
);
} else {
$continue = $helper->ask($input, $output, new ConfirmationQuestion(
sprintf('<question>' . $this->translator->translate(
'Continue with page'
) . ' <bg=cyan;options=bold>%s</>? (y/N)</question> ', $page),
false
));
}
} while ($continue);
}
}

View File

@@ -1,16 +1,17 @@
<?php
namespace Acelaya\UrlShortener\CLI\Command;
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\UrlShortener\Exception\InvalidShortCodeException;
use Acelaya\UrlShortener\Service\UrlShortener;
use Acelaya\UrlShortener\Service\UrlShortenerInterface;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
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\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Zend\I18n\Translator\TranslatorInterface;
class ResolveUrlCommand extends Command
{
@@ -18,24 +19,34 @@ class ResolveUrlCommand extends Command
* @var UrlShortenerInterface
*/
private $urlShortener;
/**
* @var TranslatorInterface
*/
private $translator;
/**
* ResolveUrlCommand constructor.
* @param UrlShortenerInterface|UrlShortener $urlShortener
* @param UrlShortenerInterface $urlShortener
* @param TranslatorInterface $translator
*
* @Inject({UrlShortener::class})
* @Inject({UrlShortener::class, "translator"})
*/
public function __construct(UrlShortenerInterface $urlShortener)
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
{
parent::__construct(null);
$this->urlShortener = $urlShortener;
$this->translator = $translator;
parent::__construct(null);
}
public function configure()
{
$this->setName('shortcode:parse')
->setDescription('Returns the long URL behind a short code')
->addArgument('shortCode', InputArgument::REQUIRED, 'The short code to parse');
->setDescription($this->translator->translate('Returns the long URL behind a short code'))
->addArgument(
'shortCode',
InputArgument::REQUIRED,
$this->translator->translate('The short code to parse')
);
}
public function interact(InputInterface $input, OutputInterface $output)
@@ -47,9 +58,10 @@ class ResolveUrlCommand extends Command
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
$question = new Question(
'<question>A short code was not provided. Which short code do you want to parse?:</question> '
);
$question = new Question(sprintf(
'<question>%s</question> ',
$this->translator->translate('A short code was not provided. Which short code do you want to parse?:')
));
$shortCode = $helper->ask($input, $output, $question);
if (! empty($shortCode)) {
@@ -64,15 +76,18 @@ class ResolveUrlCommand extends Command
try {
$longUrl = $this->urlShortener->shortCodeToUrl($shortCode);
if (! isset($longUrl)) {
$output->writeln(sprintf('<error>No URL found for short code "%s"</error>', $shortCode));
$output->writeln(sprintf(
'<error>' . $this->translator->translate('No URL found for short code "%s"') . '</error>',
$shortCode
));
return;
}
$output->writeln(sprintf('Long URL <info>%s</info>', $longUrl));
$output->writeln(sprintf('%s <info>%s</info>', $this->translator->translate('Long URL:'), $longUrl));
} catch (InvalidShortCodeException $e) {
$output->writeln(
sprintf('<error>Provided short code "%s" has an invalid format.</error>', $shortCode)
);
$output->writeln(sprintf('<error>' . $this->translator->translate(
'Provided short code "%s" has an invalid format.'
) . '</error>', $shortCode));
}
}
}

View File

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

View File

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

View File

@@ -1,9 +1,11 @@
<?php
namespace Acelaya\UrlShortener\CLI\Factory;
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();
$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,62 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\DisableKeyCommand;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class DisableKeyCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $apiKeyService;
public function setUp()
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$command = new DisableKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function providedApiKeyIsDisabled()
{
$apiKey = 'abcd1234';
$this->apiKeyService->disable($apiKey)->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'api-key:disable',
'apiKey' => $apiKey,
]);
}
/**
* @test
*/
public function errorIsReturnedIfServiceThrowsException()
{
$apiKey = 'abcd1234';
$this->apiKeyService->disable($apiKey)->willThrow(InvalidArgumentException::class)
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'api-key:disable',
'apiKey' => $apiKey,
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals('API key "abcd1234" does not exist.' . PHP_EOL, $output);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\GenerateKeyCommand;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class GenerateKeyCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $apiKeyService;
public function setUp()
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$command = new GenerateKeyCommand($this->apiKeyService->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function noExpirationDateIsDefinedIfNotProvided()
{
$this->apiKeyService->create(null)->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'api-key:generate',
]);
}
/**
* @test
*/
public function expirationDateIsDefinedIfWhenProvided()
{
$this->apiKeyService->create(Argument::type(\DateTime::class))->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'api-key:generate',
'--expirationDate' => '2016-01-01',
]);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Api\ListKeysCommand;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class ListKeysCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $apiKeyService;
public function setUp()
{
$this->apiKeyService = $this->prophesize(ApiKeyService::class);
$command = new ListKeysCommand($this->apiKeyService->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function ifEnabledOnlyIsNotProvidedEverythingIsListed()
{
$this->apiKeyService->listKeys(false)->willReturn([
new ApiKey(),
new ApiKey(),
new ApiKey(),
])->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'api-key:list',
]);
}
/**
* @test
*/
public function ifEnabledOnlyIsProvidedOnlyThoseKeysAreListed()
{
$this->apiKeyService->listKeys(true)->willReturn([
new ApiKey(),
new ApiKey(),
])->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'api-key:list',
'--enabledOnly' => true,
]);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Config;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\CLI\Command\Config\GenerateCharsetCommand;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class GenerateCharsetCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
public function setUp()
{
$command = new GenerateCharsetCommand(Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function charactersAreGeneratedFromDefault()
{
$prefix = 'Character set: ';
$prefixLength = strlen($prefix);
$this->commandTester->execute([
'command' => 'config:generate-charset',
]);
$output = $this->commandTester->getDisplay();
// Both default character set and the new one should have the same length
$this->assertEquals($prefixLength + strlen(UrlShortener::DEFAULT_CHARS) + 1, strlen($output));
// Both default character set and the new one should have the same characters
$charset = substr($output, $prefixLength, strlen(UrlShortener::DEFAULT_CHARS));
$orderedDefault = $this->orderStringLetters(UrlShortener::DEFAULT_CHARS);
$orderedCharset = $this->orderStringLetters($charset);
$this->assertEquals($orderedDefault, $orderedCharset);
}
protected function orderStringLetters($string)
{
$letters = str_split($string);
sort($letters);
return implode('', $letters);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Install;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Process\Process;
use Zend\Config\Writer\WriterInterface;
class InstallCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $configWriter;
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());
$app = new Application();
$helperSet = $app->getHelperSet();
$helperSet->set($processHelper->reveal());
$app->setHelperSet($helperSet);
$this->configWriter = $this->prophesize(WriterInterface::class);
$command = new InstallCommand($this->configWriter->reveal());
$app->add($command);
$questionHelper = $command->getHelper('question');
$questionHelper->setInputStream($this->createInputStream());
$this->commandTester = new CommandTester($command);
}
protected function createInputStream()
{
$stream = fopen('php://memory', 'r+', false);
fputs($stream, <<<CLI_INPUT
shlink_db
alejandro
1234
0
doma.in
abc123BCA
1
my_secret
CLI_INPUT
);
rewind($stream);
return $stream;
}
/**
* @test
*/
public function testInputIsProperlyParsed()
{
$this->configWriter->toFile(Argument::any(), [
'app_options' => [
'secret_key' => 'my_secret',
],
'entity_manager' => [
'connection' => [
'driver' => 'pdo_mysql',
'dbname' => 'shlink_db',
'user' => 'alejandro',
'password' => '1234',
],
],
'translator' => [
'locale' => 'en',
],
'cli' => [
'locale' => 'es',
],
'url_shortener' => [
'domain' => [
'schema' => 'http',
'hostname' => 'doma.in',
],
'shortcode_chars' => 'abc123BCA',
],
], false)->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shlink:install',
]);
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as 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

@@ -0,0 +1,70 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\GenerateShortcodeCommand;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class GenerateShortcodeCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $urlShortener;
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$command = new GenerateShortcodeCommand($this->urlShortener->reveal(), Translator::factory([]), [
'schema' => 'http',
'hostname' => 'foo.com'
]);
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function properShortCodeIsCreatedIfLongUrlIsCorrect()
{
$this->urlShortener->urlToShortCode(Argument::cetera())->willReturn('abc123')
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:generate',
'longUrl' => 'http://domain.com/foo/bar'
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'http://foo.com/abc123') > 0);
}
/**
* @test
*/
public function exceptionWhileParsingLongUrlOutputsError()
{
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:generate',
'longUrl' => 'http://domain.com/invalid'
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(
strpos($output, 'Provided URL "http://domain.com/invalid" is invalid. Try with a different one.') === 0
);
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\GetVisitsCommand;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class GetVisitsCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $visitsTracker;
public function setUp()
{
$this->visitsTracker = $this->prophesize(VisitsTrackerInterface::class);
$command = new GetVisitsCommand($this->visitsTracker->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function noDateFlagsTriesToListWithoutDateRange()
{
$shortCode = 'abc123';
$this->visitsTracker->info($shortCode, new DateRange(null, null))->willReturn([])
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:visits',
'shortCode' => $shortCode,
]);
}
/**
* @test
*/
public function providingDateFlagsTheListGetsFiltered()
{
$shortCode = 'abc123';
$startDate = '2016-01-01';
$endDate = '2016-02-01';
$this->visitsTracker->info($shortCode, new DateRange(new \DateTime($startDate), new \DateTime($endDate)))
->willReturn([])
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:visits',
'shortCode' => $shortCode,
'--startDate' => $startDate,
'--endDate' => $endDate,
]);
}
/**
* @test
*/
public function outputIsProperlyGenerated()
{
$shortCode = 'abc123';
$this->visitsTracker->info($shortCode, Argument::any())->willReturn([
(new Visit())->setReferer('foo')
->setRemoteAddr('1.2.3.4')
->setUserAgent('bar'),
])->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:visits',
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'foo') > 0);
$this->assertTrue(strpos($output, '1.2.3.4') > 0);
$this->assertTrue(strpos($output, 'bar') > 0);
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\ListShortcodesCommand;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
class ListShortcodesCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var QuestionHelper
*/
protected $questionHelper;
/**
* @var ObjectProphecy
*/
protected $shortUrlService;
public function setUp()
{
$this->shortUrlService = $this->prophesize(ShortUrlServiceInterface::class);
$app = new Application();
$command = new ListShortcodesCommand($this->shortUrlService->reveal(), Translator::factory([]));
$app->add($command);
$this->questionHelper = $command->getHelper('question');
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function noInputCallsListJustOnce()
{
$this->questionHelper->setInputStream($this->getInputStream('\n'));
$this->shortUrlService->listShortUrls(1)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$this->commandTester->execute(['command' => 'shortcode:list']);
}
/**
* @test
*/
public function loadingMorePagesCallsListMoreTimes()
{
// The paginator will return more than one page for the first 3 times
$data = [];
for ($i = 0; $i < 30; $i++) {
$data[] = new ShortUrl();
}
$data = array_chunk($data, 11);
$questionHelper = $this->questionHelper;
$that = $this;
$this->shortUrlService->listShortUrls(Argument::any())->will(function () use (&$data, $questionHelper, $that) {
$questionHelper->setInputStream($that->getInputStream('y'));
return new Paginator(new ArrayAdapter(array_shift($data)));
})->shouldBeCalledTimes(3);
$this->commandTester->execute(['command' => 'shortcode:list']);
}
/**
* @test
*/
public function havingMorePagesButAnsweringNoCallsListJustOnce()
{
// The paginator will return more than one page
$data = [];
for ($i = 0; $i < 30; $i++) {
$data[] = new ShortUrl();
}
$this->questionHelper->setInputStream($this->getInputStream('n'));
$this->shortUrlService->listShortUrls(Argument::any())->willReturn(new Paginator(new ArrayAdapter($data)))
->shouldBeCalledTimes(1);
$this->commandTester->execute(['command' => 'shortcode:list']);
}
/**
* @test
*/
public function passingPageWillMakeListStartOnThatPage()
{
$page = 5;
$this->questionHelper->setInputStream($this->getInputStream('\n'));
$this->shortUrlService->listShortUrls($page)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:list',
'--page' => $page,
]);
}
/**
* @test
*/
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
{
$this->questionHelper->setInputStream($this->getInputStream('\n'));
$this->shortUrlService->listShortUrls(1)->willReturn(new Paginator(new ArrayAdapter()))
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:list',
'--tags' => true,
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'Tags') > 0);
}
protected function getInputStream($inputData)
{
$stream = fopen('php://memory', 'r+', false);
fputs($stream, $inputData);
rewind($stream);
return $stream;
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class ResolveUrlCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $urlShortener;
public function setUp()
{
$this->urlShortener = $this->prophesize(UrlShortener::class);
$command = new ResolveUrlCommand($this->urlShortener->reveal(), Translator::factory([]));
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function correctShortCodeResolvesUrl()
{
$shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($expectedUrl)
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:parse',
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals('Long URL: ' . $expectedUrl . PHP_EOL, $output);
}
/**
* @test
*/
public function incorrectShortCodeOutputsErrorMessage()
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:parse',
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals('No URL found for short code "' . $shortCode . '"' . PHP_EOL, $output);
}
/**
* @test
*/
public function wrongShortCodeFormatOutputsErrorMessage()
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(new InvalidShortCodeException())
->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:parse',
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals('Provided short code "' . $shortCode . '" has an invalid format.' . PHP_EOL, $output);
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit_Framework_TestCase as TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Visit\ProcessVisitsCommand;
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Service\VisitService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
class ProcessVisitsCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
protected $visitService;
/**
* @var ObjectProphecy
*/
protected $ipResolver;
public function setUp()
{
$this->visitService = $this->prophesize(VisitService::class);
$this->ipResolver = $this->prophesize(IpLocationResolver::class);
$command = new ProcessVisitsCommand(
$this->visitService->reveal(),
$this->ipResolver->reveal(),
Translator::factory([])
);
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function allReturnedVisitsIpsAreProcessed()
{
$visits = [
(new Visit())->setRemoteAddr('1.2.3.4'),
(new Visit())->setRemoteAddr('4.3.2.1'),
(new Visit())->setRemoteAddr('12.34.56.78'),
];
$this->visitService->getUnlocatedVisits()->willReturn($visits)
->shouldBeCalledTimes(1);
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits));
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
->shouldBeCalledTimes(count($visits));
$this->commandTester->execute([
'command' => 'visit:process',
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'Processing IP 1.2.3.4') === 0);
$this->assertTrue(strpos($output, 'Processing IP 4.3.2.1') > 0);
$this->assertTrue(strpos($output, 'Processing IP 12.34.56.78') > 0);
}
/**
* @test
*/
public function localhostAddressIsIgnored()
{
$visits = [
(new Visit())->setRemoteAddr('1.2.3.4'),
(new Visit())->setRemoteAddr('4.3.2.1'),
(new Visit())->setRemoteAddr('12.34.56.78'),
(new Visit())->setRemoteAddr('127.0.0.1'),
(new Visit())->setRemoteAddr('127.0.0.1'),
];
$this->visitService->getUnlocatedVisits()->willReturn($visits)
->shouldBeCalledTimes(1);
$this->visitService->saveVisit(Argument::any())->shouldBeCalledTimes(count($visits) - 2);
$this->ipResolver->resolveIpLocation(Argument::any())->willReturn([])
->shouldBeCalledTimes(count($visits) - 2);
$this->commandTester->execute([
'command' => 'visit:process',
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'Ignored localhost address') > 0);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace ShlinkioTest\Shlink\CLI;
use PHPUnit_Framework_TestCase as TestCase;
use Shlinkio\Shlink\CLI\ConfigProvider;
class ConfigProviderTest extends TestCase
{
/**
* @var ConfigProvider
*/
protected $configProvider;
public function setUp()
{
$this->configProvider = new ConfigProvider();
}
/**
* @test
*/
public function confiIsProperlyReturned()
{
$config = $this->configProvider->__invoke();
$this->assertArrayHasKey('cli', $config);
$this->assertArrayHasKey('dependencies', $config);
$this->assertArrayHasKey('translator', $config);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Factory;
use PHPUnit_Framework_TestCase as 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
{
/**
* @var ApplicationFactory
*/
protected $factory;
public function setUp()
{
$this->factory = new ApplicationFactory();
}
/**
* @test
*/
public function serviceIsCreated()
{
$instance = $this->factory->__invoke($this->createServiceManager(), '');
$this->assertInstanceOf(Application::class, $instance);
}
/**
* @test
*/
public function allCommandsWhichAreServicesAreAdded()
{
$sm = $this->createServiceManager([
'commands' => [
'foo',
'bar',
'baz',
],
]);
$sm->setService('foo', $this->prophesize(Command::class)->reveal());
$sm->setService('baz', $this->prophesize(Command::class)->reveal());
/** @var Application $instance */
$instance = $this->factory->__invoke($sm, '');
$this->assertInstanceOf(Application::class, $instance);
$this->assertCount(2, $instance->all());
}
protected function createServiceManager($config = [])
{
return new ServiceManager(['services' => [
'config' => [
'cli' => array_merge($config, ['locale' => 'en']),
],
AppOptions::class => new AppOptions(),
Translator::class => Translator::factory([]),
]]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Shlinkio\Shlink\Common\Factory;
use Shlinkio\Shlink\Common\Image;
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
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 => Factory\EntityManagerFactory::class,
GuzzleHttp\Client::class => InvokableFactory::class,
Cache::class => Factory\CacheFactory::class,
'Logger_Shlink' => Factory\LoggerFactory::class,
Translator::class => Factory\TranslatorFactory::class,
TranslatorExtension::class => AnnotatedFactory::class,
LocaleMiddleware::class => AnnotatedFactory::class,
Image\ImageBuilder::class => Image\ImageBuilderFactory::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,
AnnotatedFactory::CACHE_SERVICE => Cache::class,
Logger::class => 'Logger_Shlink',
LoggerInterface::class => 'Logger_Shlink',
],
],
];

View File

@@ -0,0 +1,15 @@
<?php
use Shlinkio\Shlink\Common\Middleware;
return [
'middleware_pipeline' => [
'pre-routing' => [
'middleware' => [
Middleware\LocaleMiddleware::class,
],
'priority' => 5,
],
],
];

View File

@@ -0,0 +1,12 @@
<?php
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
return [
'twig' => [
'extensions' => [
TranslatorExtension::class,
],
],
];

View File

@@ -0,0 +1,36 @@
<?php
if (! function_exists('env')) {
/**
* Gets the value of an environment variable. Supports boolean, empty and null.
* This is basically Laravel's env helper
*
* @param string $key
* @param mixed $default
* @return mixed
* @link https://github.com/laravel/framework/blob/5.2/src/Illuminate/Foundation/helpers.php#L369
*/
function env($key, $default = null)
{
$value = getenv($key);
if ($value === false) {
return $default;
}
switch (strtolower($value)) {
case 'true':
case '(true)':
return true;
case 'false':
case '(false)':
return false;
case 'empty':
case '(empty)':
return '';
case 'null':
case '(null)':
return null;
}
return trim($value);
}
}

View File

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

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Entity;
namespace Shlinkio\Shlink\Common\Entity;
use Doctrine\ORM\Mapping as ORM;

View File

@@ -0,0 +1,6 @@
<?php
namespace Shlinkio\Shlink\Common\Exception;
interface ExceptionInterface
{
}

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Exception;
namespace Shlinkio\Shlink\Common\Exception;
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
{

View File

@@ -0,0 +1,10 @@
<?php
namespace Shlinkio\Shlink\Common\Exception;
class PreviewGenerationException extends RuntimeException
{
public static function fromImageError($error)
{
return new self(sprintf('Error generating a preview image with error: %s', $error));
}
}

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Exception;
namespace Shlinkio\Shlink\Common\Exception;
class RuntimeException extends \RuntimeException implements ExceptionInterface
{

View File

@@ -0,0 +1,10 @@
<?php
namespace Shlinkio\Shlink\Common\Exception;
class WrongIpException extends RuntimeException
{
public static function fromIpAddress($ipAddress, \Exception $prev = null)
{
return new self(sprintf('Provided IP "%s" is invalid', $ipAddress), 0, $prev);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Shlinkio\Shlink\Common\Factory;
use Doctrine\Common\Cache;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Shlinkio\Shlink\Core\Options\AppOptions;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class CacheFactory implements FactoryInterface
{
const VALID_CACHE_ADAPTERS = [
Cache\ApcuCache::class,
Cache\ArrayCache::class,
Cache\FilesystemCache::class,
Cache\PhpFileCache::class,
Cache\MemcachedCache::class,
];
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @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)
{
$appOptions = $container->get(AppOptions::class);
$adapter = $this->getAdapter($container);
$adapter->setNamespace($appOptions->__toString());
return $adapter;
}
/**
* @param ContainerInterface $container
* @return Cache\CacheProvider
*/
protected function getAdapter(ContainerInterface $container)
{
// Try to get the adapter from config
$config = $container->get('config');
if (isset($config['cache'])
&& isset($config['cache']['adapter'])
&& in_array($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS)
) {
return $this->resolveCacheAdapter($config['cache']);
}
// If the adapter has not been set in config, create one based on environment
return env('APP_ENV', 'pro') === 'pro' ? new Cache\ApcuCache() : new Cache\ArrayCache();
}
/**
* @param array $cacheConfig
* @return Cache\CacheProvider
*/
protected function resolveCacheAdapter(array $cacheConfig)
{
switch ($cacheConfig['adapter']) {
case Cache\ArrayCache::class:
case Cache\ApcuCache::class:
return new $cacheConfig['adapter']();
case Cache\FilesystemCache::class:
case Cache\PhpFileCache::class:
return new $cacheConfig['adapter']($cacheConfig['options']['dir']);
case Cache\MemcachedCache::class:
$memcached = new \Memcached();
$servers = isset($cacheConfig['options']['servers']) ? $cacheConfig['options']['servers'] : [];
foreach ($servers as $server) {
if (! isset($server['host'])) {
continue;
}
$port = isset($server['port']) ? intval($server['port']) : 11211;
$memcached->addServer($server['host'], $port);
}
$cache = new Cache\MemcachedCache();
$cache->setMemcached($memcached);
return $cache;
default:
return new Cache\ArrayCache();
}
}
}

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Factory;
namespace Shlinkio\Shlink\Common\Factory;
use Doctrine\Common\Cache\ArrayCache;
use Doctrine\Common\Cache\Cache;
@@ -30,12 +30,14 @@ class EntityManagerFactory implements FactoryInterface
$globalConfig = $container->get('config');
$isDevMode = isset($globalConfig['debug']) ? ((bool) $globalConfig['debug']) : false;
$cache = $container->has(Cache::class) ? $container->get(Cache::class) : new ArrayCache();
$dbConfig = isset($globalConfig['database']) ? $globalConfig['database'] : [];
$emConfig = isset($globalConfig['entity_manager']) ? $globalConfig['entity_manager'] : [];
$connecitonConfig = isset($emConfig['connection']) ? $emConfig['connection'] : [];
$ormConfig = isset($emConfig['orm']) ? $emConfig['orm'] : [];
return EntityManager::create($dbConfig, Setup::createAnnotationMetadataConfiguration(
['src/Entity'],
return EntityManager::create($connecitonConfig, Setup::createAnnotationMetadataConfiguration(
isset($ormConfig['entities_paths']) ? $ormConfig['entities_paths'] : [],
$isDevMode,
'data/proxies',
isset($ormConfig['proxies_dir']) ? $ormConfig['proxies_dir'] : null,
$cache,
false
));

View File

@@ -0,0 +1,39 @@
<?php
namespace Shlinkio\Shlink\Common\Factory;
use Cascade\Cascade;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class LoggerFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @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)
{
$config = $container->has('config') ? $container->get('config') : [];
Cascade::fileConfig(isset($config['logger']) ? $config['logger'] : ['loggers' => []]);
// Compose requested logger name
$loggerName = isset($options) & isset($options['logger_name']) ? $options['logger_name'] : 'Logger';
$nameParts = explode('_', $requestedName);
if (count($nameParts) > 1) {
$loggerName = $nameParts[1];
}
return Cascade::getLogger($loggerName);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Shlinkio\Shlink\Common\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class TranslatorFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @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)
{
$config = $container->get('config');
return Translator::factory(isset($config['translator']) ? $config['translator'] : []);
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace Shlinkio\Shlink\Common\Image;
use mikehaertl\wkhtmlto\Image;
use Zend\ServiceManager\AbstractPluginManager;
class ImageBuilder extends AbstractPluginManager implements ImageBuilderInterface
{
protected $instanceOf = Image::class;
}

View File

@@ -1,15 +1,14 @@
<?php
namespace Acelaya\UrlShortener\Factory;
namespace Shlinkio\Shlink\Common\Image;
use Doctrine\Common\Cache\ApcuCache;
use Doctrine\Common\Cache\ArrayCache;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use mikehaertl\wkhtmlto\Image;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class CacheFactory implements FactoryInterface
class ImageBuilderFactory implements FactoryInterface
{
/**
* Create an object
@@ -25,6 +24,8 @@ class CacheFactory implements FactoryInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return getenv('APP_ENV') === 'pro' ? new ApcuCache() : new ArrayCache();
return new ImageBuilder($container, ['factories' => [
Image::class => ImageFactory::class,
]]);
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace Shlinkio\Shlink\Common\Image;
use Zend\ServiceManager\ServiceLocatorInterface;
interface ImageBuilderInterface extends ServiceLocatorInterface
{
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Shlinkio\Shlink\Common\Image;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use mikehaertl\wkhtmlto\Image;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class ImageFactory implements FactoryInterface
{
/**
* Create an object
*
* @param ContainerInterface $container
* @param string $requestedName
* @param null|array $options
* @return object
* @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)
{
$config = $container->get('config')['phpwkhtmltopdf'];
$image = new Image(isset($config['images']) ? $config['images'] : null);
if (isset($options) && isset($options['url'])) {
$image->setPage($options['url']);
}
return $image;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Shlinkio\Shlink\Common\Middleware;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Zend\I18n\Translator\Translator;
use Zend\Stratigility\MiddlewareInterface;
class LocaleMiddleware implements MiddlewareInterface
{
/**
* @var Translator
*/
private $translator;
/**
* LocaleMiddleware constructor.
* @param Translator $translator
*
* @Inject({"translator"})
*/
public function __construct(Translator $translator)
{
$this->translator = $translator;
}
/**
* Process an incoming request and/or response.
*
* Accepts a server-side request and a response instance, and does
* something with them.
*
* If the response is not complete and/or further processing would not
* interfere with the work done in the middleware, or if the middleware
* wants to delegate to another process, it can use the `$out` callable
* if present.
*
* If the middleware does not return a value, execution of the current
* request is considered complete, and the response instance provided will
* be considered the response to return.
*
* Alternately, the middleware may return a response instance.
*
* Often, middleware will `return $out();`, with the assumption that a
* later middleware will return a response.
*
* @param Request $request
* @param Response $response
* @param null|callable $out
* @return null|Response
*/
public function __invoke(Request $request, Response $response, callable $out = null)
{
if (! $request->hasHeader('Accept-Language')) {
return $out($request, $response);
}
$locale = $request->getHeaderLine('Accept-Language');
$this->translator->setLocale($this->normalizeLocale($locale));
return $out($request, $response);
}
/**
* @param string $locale
* @return string
*/
protected function normalizeLocale($locale)
{
$parts = explode('_', $locale);
if (count($parts) > 1) {
return $parts[0];
}
$parts = explode('-', $locale);
if (count($parts) > 1) {
return $parts[0];
}
return $locale;
}
}

View File

@@ -1,7 +1,7 @@
<?php
namespace Acelaya\UrlShortener\Paginator\Adapter;
namespace Shlinkio\Shlink\Common\Paginator\Adapter;
use Acelaya\UrlShortener\Repository\PaginableRepositoryInterface;
use Shlinkio\Shlink\Common\Repository\PaginableRepositoryInterface;
use Zend\Paginator\Adapter\AdapterInterface;
class PaginableRepositoryAdapter implements AdapterInterface

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Paginator\Util;
namespace Shlinkio\Shlink\Common\Paginator\Util;
use Zend\Paginator\Paginator;
use Zend\Stdlib\ArrayUtils;

View File

@@ -1,5 +1,5 @@
<?php
namespace Acelaya\UrlShortener\Repository;
namespace Shlinkio\Shlink\Common\Repository;
interface PaginableRepositoryInterface
{

View File

@@ -0,0 +1,35 @@
<?php
namespace Shlinkio\Shlink\Common\Response;
use Endroid\QrCode\QrCode;
use Psr\Http\Message\StreamInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Stream;
class QrCodeResponse extends Response
{
use Response\InjectContentTypeTrait;
public function __construct(QrCode $qrCode, $status = 200, array $headers = [])
{
parent::__construct(
$this->createBody($qrCode),
$status,
$this->injectContentType($qrCode->getContentType(), $headers)
);
}
/**
* Create the message body.
*
* @param QrCode $qrCode
* @return StreamInterface
*/
private function createBody(QrCode $qrCode)
{
$body = new Stream('php://temp', 'wb+');
$body->write($qrCode->get());
$body->rewind();
return $body;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Shlinkio\Shlink\Common\Service;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
class IpLocationResolver implements IpLocationResolverInterface
{
const SERVICE_PATTERN = 'http://freegeoip.net/json/%s';
/**
* @var Client
*/
private $httpClient;
/**
* IpLocationResolver constructor.
* @param Client $httpClient
*
* @Inject({"httpClient"})
*/
public function __construct(Client $httpClient)
{
$this->httpClient = $httpClient;
}
/**
* @param $ipAddress
* @return array
*/
public function resolveIpLocation($ipAddress)
{
try {
$response = $this->httpClient->get(sprintf(self::SERVICE_PATTERN, $ipAddress));
return json_decode($response->getBody(), true);
} catch (GuzzleException $e) {
throw WrongIpException::fromIpAddress($ipAddress, $e);
}
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace Shlinkio\Shlink\Common\Service;
interface IpLocationResolverInterface
{
/**
* @param $ipAddress
* @return array
*/
public function resolveIpLocation($ipAddress);
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Shlinkio\Shlink\Common\Service;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use mikehaertl\wkhtmlto\Image;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Image\ImageBuilder;
use Shlinkio\Shlink\Common\Image\ImageBuilderInterface;
use Symfony\Component\Filesystem\Filesystem;
class PreviewGenerator implements PreviewGeneratorInterface
{
/**
* @var string
*/
private $location;
/**
* @var ImageBuilderInterface
*/
private $imageBuilder;
/**
* @var Filesystem
*/
private $filesystem;
/**
* PreviewGenerator constructor.
* @param ImageBuilderInterface $imageBuilder
* @param Filesystem $filesystem
* @param string $location
*
* @Inject({ImageBuilder::class, Filesystem::class, "config.preview_generation.files_location"})
*/
public function __construct(ImageBuilderInterface $imageBuilder, Filesystem $filesystem, $location)
{
$this->location = $location;
$this->imageBuilder = $imageBuilder;
$this->filesystem = $filesystem;
}
/**
* Generates and stores preview for provided website and returns the path to the image file
*
* @param string $url
* @return string
* @throws PreviewGenerationException
*/
public function generatePreview($url)
{
/** @var Image $image */
$image = $this->imageBuilder->build(Image::class, ['url' => $url]);
// If the file already exists, return its path
$cacheId = sprintf('preview_%s.%s', urlencode($url), $image->type);
$path = $this->location . '/' . $cacheId;
if ($this->filesystem->exists($path)) {
return $path;
}
// Save and check if an error occurred
$image->saveAs($path);
$error = $image->getError();
if (! empty($error)) {
throw PreviewGenerationException::fromImageError($error);
}
// Cache the path and return it
return $path;
}
}

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