Compare commits

...

181 Commits

Author SHA1 Message Date
Alejandro Celaya
b53e51de33 Merge branch 'develop' 2017-03-25 10:13:11 +01:00
Alejandro Celaya
9478c71af1 Merge pull request #90 from acelaya/feature/expressive-2
Feature/expressive 2
2017-03-25 10:11:46 +01:00
Alejandro Celaya
a2c4eebec8 Updated CHANGELOG 2017-03-25 10:09:30 +01:00
Alejandro Celaya
2e5a7d76df Migrated rest actions to psr-15 middleware 2017-03-25 10:04:48 +01:00
Alejandro Celaya
288249d0b8 Renamed JsonErrorHandler to JsonErrorResponseGenerator 2017-03-25 09:46:29 +01:00
Alejandro Celaya
cd47aae902 Migrated CrossDomainMiddleware to psr-15 middleware 2017-03-25 09:44:34 +01:00
Alejandro Celaya
9bd18ee041 Migrated CheckAuthenticationMiddleware to psr-15 middleware 2017-03-25 09:37:13 +01:00
Alejandro Celaya
22c76df8e6 Migrated BodyParserMiddleware to psr-15 middleware 2017-03-25 09:22:00 +01:00
Alejandro Celaya
6c87436a96 Migrated QrCodeCacheMiddleware to psr-15 middleware 2017-03-24 23:34:17 +01:00
Alejandro Celaya
734dac9456 Migrated RedirectAction to psr-15 middleware 2017-03-24 23:24:11 +01:00
Alejandro Celaya
85ca366893 Migrated QrCodeAction to psr-15 middleware 2017-03-24 23:19:42 +01:00
Alejandro Celaya
46db736af8 Migrated PreviewAction to psr-15 middleware 2017-03-24 22:07:28 +01:00
Alejandro Celaya
7530048fbd Removed exception catch that used to return a 500, and now returns a 404 due to a behavior change 2017-03-24 21:59:45 +01:00
Alejandro Celaya
c3c03a3a3b Migrated LocaleMiddleware to psr-15 middleware 2017-03-24 21:49:31 +01:00
Alejandro Celaya
d1018b6da7 Fixed tests 2017-03-24 21:38:43 +01:00
Alejandro Celaya
fe7928ae0e Fixed JsonErrorHandler and prevented AuthorizationMiddleware to eat exceptions 2017-03-24 21:31:55 +01:00
Alejandro Celaya
f6c39285c9 Updated to expressive 2 and used new error handling system 2017-03-24 21:10:25 +01:00
Alejandro Celaya
0e2a289f9f Updated to phpunit 6 2017-03-24 20:34:18 +01:00
Alejandro Celaya
f7424da16b Merge branch 'develop' 2017-01-22 11:37:35 +01:00
Alejandro Celaya
2e10ee66b7 Merge pull request #87 from acelaya/feature/1.3.1
Feature/1.3.1
2017-01-22 11:36:04 +01:00
Alejandro Celaya
7e442671c3 Updated build script 2017-01-22 11:27:56 +01:00
Alejandro Celaya
ca646ec2b7 Updated changelog including v1.3.1 2017-01-22 11:17:44 +01:00
Alejandro Celaya
4df1af5fd8 Fixed searching short URLs list not querying tag names 2017-01-22 11:14:25 +01:00
Alejandro Celaya
b4548f3401 Added configs to enable fastroute cache 2017-01-22 11:07:18 +01:00
Alejandro Celaya
1819481710 Updated license year 2017-01-22 10:59:35 +01:00
Alejandro Celaya
e59ae654c0 Increased number of followed redirects to 15 2017-01-22 10:53:41 +01:00
Alejandro Celaya
a8bf699f2d Fixed error with non-registered service on latest expressive-twig-renderer version 2017-01-21 20:19:30 +01:00
Alejandro Celaya
de9d9d8667 Updated PathVersionMiddleware so that it is only applied to rest routes 2017-01-21 20:12:12 +01:00
Alejandro Celaya
869865f22a Added option to customize database hostname and port 2017-01-21 13:45:28 +01:00
Alejandro Celaya
29fd313337 Added memcached to php docker image 2017-01-21 13:33:51 +01:00
Alejandro Celaya
7781f07352 Added local entity manager config that allows db host name to be set 2017-01-21 09:16:00 +01:00
Alejandro Celaya
072371d459 Created docker-related files 2017-01-19 22:56:45 +01:00
Alejandro Celaya
51bf948458 Fixed schema definition on order by argument 2016-10-29 12:42:36 +02:00
Alejandro Celaya
48acded6ed Merge branch 'develop' 2016-10-23 10:30:36 +02:00
Alejandro Celaya
6821f5cf97 Merge branch 'develop' of https://github.com/shlinkio/shlink into develop 2016-10-23 10:30:11 +02:00
Alejandro Celaya
a15b17e08b Fixed regression bug while processing versionning for rest paths 2016-10-23 10:29:54 +02:00
Alejandro Celaya
275b17e1e8 Merge pull request #74 from acelaya/develop
Develop
2016-10-23 00:18:22 +02:00
Alejandro Celaya
27b08ff47b Merge branch 'develop' 2016-10-23 00:17:00 +02:00
Alejandro Celaya
bf7c760ca9 Updated changelog with last versions 2016-10-23 00:13:00 +02:00
Alejandro Celaya
8af9b0ee02 Tagged and summarized all endpoints in swagger docs 2016-10-23 00:07:31 +02:00
Alejandro Celaya
b225c03ef1 Improved swagger definition 2016-10-23 00:02:13 +02:00
Alejandro Celaya
8a12ed6b8c Separated swagger specification into multiple files 2016-10-22 23:44:14 +02:00
Alejandro Celaya
c3fd433446 Merge branch 'feature/67' into develop 2016-10-22 23:15:57 +02:00
Alejandro Celaya
0b9753582d Documented how to order results 2016-10-22 23:13:54 +02:00
Alejandro Celaya
9ac48bfbc5 Added support for ordering in shortcode:list command 2016-10-22 23:10:30 +02:00
Alejandro Celaya
85146e5676 Added support to order short URL lists 2016-10-22 23:02:12 +02:00
Alejandro Celaya
18ae541c93 Improved body parsing on BodyParserMiddleware 2016-10-22 22:17:04 +02:00
Alejandro Celaya
7a665ec26f Merge branch 'feature/72' into develop 2016-10-22 22:11:45 +02:00
Alejandro Celaya
e3cbac38ce Improved output on api-key:list command 2016-10-22 22:11:36 +02:00
Alejandro Celaya
31594d47b3 Created PathVersionMiddlewareTest 2016-10-22 18:52:40 +02:00
Alejandro Celaya
42f86a4a24 Added versioning to API endpoints, allowing not to pass the version which will default to v1 2016-10-22 18:46:53 +02:00
Alejandro Celaya
850ce152cd Merge branch 'feature/58' into develop 2016-10-22 13:16:13 +02:00
Alejandro Celaya
c22bbecdc5 Updated languages 2016-10-22 13:15:35 +02:00
Alejandro Celaya
230f2d155b Documented tags param in GET /short-codes endpoint 2016-10-22 13:13:50 +02:00
Alejandro Celaya
47a2c18c7e Added the ability to filter by tag in shotcodes:list command 2016-10-22 13:11:24 +02:00
Alejandro Celaya
52bb14bd66 Implemented filtering by tags in ListShortcodesAction 2016-10-22 13:04:17 +02:00
Alejandro Celaya
8b9caf02d2 Added tags param to paginable repository adapter 2016-10-22 12:57:24 +02:00
Alejandro Celaya
4580d11d32 Noticed in swagger docs that the searchTerm param is only available from v 1.3.0 of shlink 2016-10-22 12:50:35 +02:00
Alejandro Celaya
8610a158d4 Added searchTerm param to shortcode:list command 2016-10-22 12:48:24 +02:00
Alejandro Celaya
0a6030b35d Documented searchTerm query param for GET /short-codes endpoint 2016-10-22 12:43:22 +02:00
Alejandro Celaya
543c0e62d0 Added search term filtering to short codes list 2016-10-22 12:40:51 +02:00
Alejandro Celaya
4c76e17178 Changed swagger file format from yaml to json 2016-10-22 12:11:31 +02:00
Alejandro Celaya
611a314cdf Merge branch 'develop' 2016-08-29 13:07:25 +02:00
Alejandro Celaya
e7b4d24e5d Merge pull request #68 from acelaya/develop
Develop
2016-08-29 13:07:01 +02:00
Alejandro Celaya
cf60440288 Fixed possible PHP errors being missed while checking REST auth 2016-08-29 12:43:02 +02:00
Alejandro Celaya
15896045f3 Removed logic making visits to be returned for 2 days only if no start or end date were provided 2016-08-28 19:32:07 +02:00
Alejandro Celaya
a9f480ca99 Fixed error while checking an API key that doesn't exist 2016-08-28 09:46:11 +02:00
Alejandro Celaya
4bd67d5f98 Fixed cross domain middleware not exposing the Authorization header 2016-08-27 13:00:41 +02:00
Alejandro Celaya
924ba58f73 Added swagger documentation file 2016-08-26 11:51:51 +02:00
Alejandro Celaya
6eef694315 Merge branch 'develop' 2016-08-21 21:24:31 +02:00
Alejandro Celaya
fe4a4aef34 Updated changelog 2016-08-21 21:24:00 +02:00
Alejandro Celaya
b13c95cf1a Placed cross domain middleware as the first one for rest requests 2016-08-21 21:21:31 +02:00
Alejandro Celaya
c1c325588e Merge branch 'develop' 2016-08-21 19:16:36 +02:00
Alejandro Celaya
faad60f79e Merge branch 'develop' of https://github.com/shlinkio/shlink into develop 2016-08-21 19:16:21 +02:00
Alejandro Celaya
cbb5a02b95 Kept symlincs while generating the dist file 2016-08-21 19:16:06 +02:00
Alejandro Celaya
96faafd31e Merge branch 'develop' 2016-08-21 18:31:45 +02:00
Alejandro Celaya
7aa42ada54 Merge pull request #61 from acelaya/develop
Develop
2016-08-21 18:31:32 +02:00
Alejandro Celaya
3b1545a761 Added v1.2.0 to changelog 2016-08-21 18:26:28 +02:00
Alejandro Celaya
536309afb6 Merge pull request #4 from acelaya/feature/45
Feature/45
2016-08-21 18:20:56 +02:00
Alejandro Celaya
ad6ef22b72 Updated languages 2016-08-21 18:17:39 +02:00
Alejandro Celaya
4185015bef Improved tests 2016-08-21 18:15:25 +02:00
Alejandro Celaya
3376725152 Created EditTagsActiontest 2016-08-21 18:06:34 +02:00
Alejandro Celaya
d7b18776f1 Improved error response in edit tag request 2016-08-21 17:55:26 +02:00
Alejandro Celaya
1da285a63a Created action to set the togas for a short url 2016-08-21 16:52:26 +02:00
Alejandro Celaya
372488cbb4 Created middleware to parse PUT request bodies in rest requests 2016-08-21 13:52:15 +02:00
Alejandro Celaya
b6fee0ebaf Added option to set tags while creating short code from rest API 2016-08-21 13:07:12 +02:00
Alejandro Celaya
322180bde4 Added tag property to json serialization of ShortUrl 2016-08-21 12:48:31 +02:00
Alejandro Celaya
1cf6c93007 Added option to pass tags when creating a short code from the command line 2016-08-21 10:39:00 +02:00
Alejandro Celaya
2b89556c09 Allowed to display tags in the shortcode:list command 2016-08-21 10:17:17 +02:00
Alejandro Celaya
e3021120e3 Checked tables do not exist before creating them 2016-08-21 09:45:36 +02:00
Alejandro Celaya
2ca7ab4ccf Created new entity Tag and migration to create new tables 2016-08-21 09:41:21 +02:00
Alejandro Celaya
a2c2bc166c Removed wrong spaces 2016-08-20 19:11:20 +02:00
Alejandro Celaya
5f0baab2ac Created script to update an already existing system 2016-08-19 15:50:08 +02:00
Alejandro Celaya
ea083e30b6 Improved InstallCommand, adding migrations and removing code duplication 2016-08-19 15:15:53 +02:00
Alejandro Celaya
7d49c1760c Added doctrine migrations and remove platform specific code from entities 2016-08-19 14:51:34 +02:00
Alejandro Celaya
7c42835cc1 Improved error management on install command 2016-08-18 18:02:24 +02:00
Alejandro Celaya
f77273ef93 Merge pull request #3 from acelaya/feature/7
Feature/7
2016-08-18 17:30:57 +02:00
Alejandro Celaya
e52983c5d9 Updated translations 2016-08-18 17:28:04 +02:00
Alejandro Celaya
4615bbaaf7 CreatedPreviewActionTest 2016-08-18 17:23:40 +02:00
Alejandro Celaya
b432ed2c1d Created GeneratePreviewCommandTest 2016-08-18 17:10:40 +02:00
Alejandro Celaya
34174b2fbe Added tests for Image classes 2016-08-18 16:37:50 +02:00
Alejandro Celaya
fac519699a Used filesystem check instead of cache check for preview generation 2016-08-18 13:20:57 +02:00
Alejandro Celaya
2c91ded514 Improved PreviewGenerator by composing an ImageBuilder that creates new Image objects fore each URL 2016-08-18 12:21:26 +02:00
Alejandro Celaya
15247e832e Improved QR code generation route 2016-08-18 11:31:04 +02:00
Alejandro Celaya
60c68c914b Managed error while generating URL previews by throwing an exception 2016-08-18 11:17:17 +02:00
Alejandro Celaya
277406c3b8 Created action to return preview images 2016-08-18 11:10:15 +02:00
Alejandro Celaya
26adf48b48 Added wkhtmltopdf stuff and created preview generator service 2016-08-18 10:19:33 +02:00
Alejandro Celaya
20e43aac90 Added bin dir to code sniffer checks 2016-08-17 11:47:08 +02:00
Alejandro Celaya
56c9abcfd0 Fixed build script to generate the distributable file just with project files from the project root 2016-08-16 11:51:05 +02:00
Alejandro Celaya
5ca4bc928d Created specific factory for AppOptions to prevent circular dependency with cache 2016-08-15 23:40:49 +02:00
Alejandro Celaya
ffa6c0d2ca Merge branch 'feature/install' into develop 2016-08-15 11:34:48 +02:00
Alejandro Celaya
395311eaad Added InstallCommandTest 2016-08-15 11:34:35 +02:00
Alejandro Celaya
5bbc7de4af Fixed namespace in some tests 2016-08-15 11:01:55 +02:00
Alejandro Celaya
d5516c7269 Finished build script to compress dist project file 2016-08-15 10:18:13 +02:00
Alejandro Celaya
f852f9d398 Created htaccess 2016-08-15 10:03:39 +02:00
Alejandro Celaya
0d9f964687 Fixed bug on build script 2016-08-15 10:03:17 +02:00
Alejandro Celaya
00c56ca594 Fixed tests 2016-08-15 09:52:44 +02:00
Alejandro Celaya
9e4fb68265 Created build script 2016-08-15 09:45:09 +02:00
Alejandro Celaya
804d99ebf7 Created CLI scripts for Windows OS 2016-08-15 09:21:14 +02:00
Alejandro Celaya
4bbdccf981 Added symfony process to run initialization commands 2016-08-14 23:41:42 +02:00
Alejandro Celaya
1f3e31d100 Fixed working directory and paths in InstallCommand 2016-08-14 23:25:04 +02:00
Alejandro Celaya
2617ef1547 Added cli locale to installation generated config file 2016-08-14 23:10:07 +02:00
Alejandro Celaya
25380e4727 Moved console Application creation to factory 2016-08-14 23:08:26 +02:00
Alejandro Celaya
12322b7368 Improved config loading by using only one config provider object 2016-08-14 22:56:22 +02:00
Alejandro Celaya
a608f7d0f4 Minor name change 2016-08-14 21:28:21 +02:00
Alejandro Celaya
9a42d70604 Added custom config params to merged confg 2016-08-14 18:15:10 +02:00
Alejandro Celaya
fe708333b1 Fixed path while generating config file 2016-08-14 10:43:16 +02:00
Alejandro Celaya
566ee7ef6f Finished custom config command 2016-08-14 10:30:43 +02:00
Alejandro Celaya
56af58fcb8 Created installation script and installation command 2016-08-14 09:11:46 +02:00
Alejandro Celaya
cffa43a155 Created installation script and installation command 2016-08-14 09:09:23 +02:00
Alejandro Celaya
2b2c0b7c13 Merge branch 'feature/29' into develop 2016-08-12 18:01:38 +02:00
Alejandro Celaya
faa8019fc5 Redefined logger services so that the error handler uses Shlink's logger 2016-08-12 18:01:09 +02:00
Alejandro Celaya
a8ea458649 Fixed distributable local config file 2016-08-12 17:55:07 +02:00
Alejandro Celaya
8f4d305982 Added ErrorHandler package dependency and remove local files 2016-08-12 17:54:32 +02:00
Alejandro Celaya
065cddc4b1 Merge branch 'develop' 2016-08-09 19:05:39 +02:00
Alejandro Celaya
3a3a16f46f Added changelog for v1.1.0 2016-08-09 19:05:13 +02:00
Alejandro Celaya
ba5bd6d98c Merge branch 'develop' 2016-08-09 19:00:42 +02:00
Alejandro Celaya
a1aa9c2031 Merge pull request #49 from acelaya/develop
v1.1.0
2016-08-09 19:00:22 +02:00
Alejandro Celaya
43c6b56e42 Fixed memcached test while comparing servers 2016-08-09 18:56:01 +02:00
Alejandro Celaya
39d2f5a38f Created travis config file to enable memcached extension 2016-08-09 18:50:14 +02:00
Alejandro Celaya
69cc30bce7 Allowed failures on PHP 7.1 environments 2016-08-09 18:38:48 +02:00
Alejandro Celaya
5913550eec Fixed build when memcached is not enabled in PHP 7.1 2016-08-09 18:29:47 +02:00
Alejandro Celaya
3140ab2ad7 Updated shlink website link to use https 2016-08-09 18:17:28 +02:00
Alejandro Celaya
090479fa62 Improved CacheFactory supporting more adapters 2016-08-09 17:58:47 +02:00
Alejandro Celaya
9ee2064ba1 Merge pull request #2 from acelaya/feature/46
Feature/46
2016-08-09 14:21:43 +02:00
Alejandro Celaya
90cef7d4d9 Removed unused import 2016-08-09 14:19:46 +02:00
Alejandro Celaya
12410e82d8 Created tests for QrCode middlewares 2016-08-09 14:18:20 +02:00
Alejandro Celaya
18084433c7 Created middleware to cache generated QR codes 2016-08-09 13:41:30 +02:00
Alejandro Celaya
8eb279fd28 Updated UrlShortener to namespace the cache entries 2016-08-09 13:32:33 +02:00
Alejandro Celaya
99b7c77997 Created action to generate QR codes 2016-08-09 10:25:30 +02:00
Alejandro Celaya
166c94cac7 Merge branch 'feature/40' into develop 2016-08-09 09:13:48 +02:00
Alejandro Celaya
7c5d8cf244 Fixed VisitsTracker to take into account the X-Forwarded-For header in case the server is behind a load balabncer or proxy 2016-08-09 09:13:39 +02:00
Alejandro Celaya
73a236b3d0 Updated VisitsTracker so that the track method expects a Request object to be provided 2016-08-09 08:52:06 +02:00
Alejandro Celaya
34753ca7d3 Added logger to classes that catch errors in order to log them 2016-08-08 12:33:58 +02:00
Alejandro Celaya
fff058f44b Created LoggerFactoryTest 2016-08-08 12:07:04 +02:00
Alejandro Celaya
b7f3c332e4 Created Logger factory and logger config, and added logger dependencies 2016-08-08 11:56:19 +02:00
Alejandro Celaya
cff9b7c0b5 Deleted docs which are now in Shlink's website 2016-08-08 11:17:14 +02:00
Alejandro Celaya
63e867cf4b Updated vendor name on error pages 2016-08-08 10:08:34 +02:00
Alejandro Celaya
f49e9064cd Added cache adapter to the UrlShortener service to cache shortcode-url maps 2016-08-08 10:02:52 +02:00
Alejandro Celaya
3bd4f506e0 Updated status returned in REST endpoints to be 404 when something is not found 2016-08-08 09:46:40 +02:00
Alejandro Celaya
93713689d7 Merge branch 'feature/35' into develop 2016-08-08 09:39:15 +02:00
Alejandro Celaya
ecd2e6e759 Updated namespace for Visit CLI commands 2016-08-08 09:38:50 +02:00
Alejandro Celaya
a65003803b Updated namespace for Shortcode CLI commands 2016-08-08 09:36:52 +02:00
Alejandro Celaya
0a4f8c3b0a Merge pull request #1 from acelaya/feature/13
Feature/13
2016-08-07 21:15:59 +02:00
Alejandro Celaya
80d8c32881 Removed rest auth env vars 2016-08-07 20:47:43 +02:00
Alejandro Celaya
57bc681b9e Created command to generate a random secret key string 2016-08-07 20:30:19 +02:00
Alejandro Celaya
2a089f05b1 Updated languages 2016-08-07 20:21:38 +02:00
Alejandro Celaya
258f954a38 Deleted rest token related classes 2016-08-07 19:57:23 +02:00
Alejandro Celaya
7b0beb3b8c Updated CheckAuthenticationMiddleware to work with JWT and the Authorization header 2016-08-07 19:53:14 +02:00
Alejandro Celaya
9573e9f4ef Updated AuthenticateAction to generate and return a JWT 2016-08-07 19:13:40 +02:00
Alejandro Celaya
a60080b1ce Created JWTService and related classes 2016-08-07 14:44:33 +02:00
Alejandro Celaya
1d92e87d50 Updated AuthenticateAction to use the APiKeyService instead of the RestTokenService 2016-08-07 10:26:34 +02:00
Alejandro Celaya
289db45f27 Created ListKeysCommand 2016-08-06 18:50:50 +02:00
Alejandro Celaya
c5382b2a7f Created DisableKeyCommand 2016-08-06 18:26:07 +02:00
Alejandro Celaya
dd1bc49b79 Added method to ApiKeyService to list api keys 2016-08-06 18:08:09 +02:00
Alejandro Celaya
74777c2234 Created command to generate a new api key 2016-08-06 18:07:48 +02:00
Alejandro Celaya
99d7e6dd7d Fixed AuthenticateAction not working with only one group of params 2016-08-06 13:24:06 +02:00
Alejandro Celaya
7b746f76b0 Created APiKeyService and tests 2016-08-06 13:18:27 +02:00
Alejandro Celaya
2767a14101 Created ApiKey entity 2016-08-06 12:50:44 +02:00
Alejandro Celaya
270dbc6028 Created new entity_manager configuration, dropping old database first level config key 2016-08-06 12:40:31 +02:00
Alejandro Celaya
7b1b00901a Created phpstorm meta fle to get ContainerInterop typehint based on service name 2016-08-05 07:20:40 +02:00
210 changed files with 6202 additions and 1809 deletions

View File

@@ -1,5 +1,6 @@
# Application
APP_ENV=
SECRET_KEY=
SHORTENED_URL_SCHEMA=
SHORTENED_URL_HOSTNAME=
SHORTCODE_CHARS=
@@ -12,7 +13,3 @@ CLI_LOCALE=
DB_USER=
DB_PASSWORD=
DB_NAME=
# Rest authentication
REST_USER=
REST_PASSWORD=

2
.gitignore vendored
View File

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

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

@@ -10,6 +10,8 @@ php:
- 7
- 7.1
before_install: phpenv config-add .travis-php.ini
before_script:
- composer self-update
- composer install --no-interaction

View File

@@ -1,40 +1,137 @@
## CHANGELOG
### 1.4.0
**Enhancements:**
* [89: Update to expressive 2](https://github.com/shlinkio/shlink/issues/89)
### 1.3.1
**Tasks**
* [82: Enable FastRoute routes cache](https://github.com/shlinkio/shlink/issues/82)
* [85: Update year in license file](https://github.com/shlinkio/shlink/issues/85)
* [81: Add docker containers config](https://github.com/shlinkio/shlink/issues/81)
**Bugs**
* [83: Short codes list: search in tags when filtering by query string](https://github.com/shlinkio/shlink/issues/83)
* [79: Increase the number of followed redirects](https://github.com/shlinkio/shlink/issues/79)
* [75: Apply PathVersionMiddleware only to rest routes defining it by configuration instead of code](https://github.com/shlinkio/shlink/issues/75)
* [77: Allow defining database server hostname and port](https://github.com/shlinkio/shlink/issues/77)
### 1.3.0
**Enhancements:**
* [67: Allow to order the short codes list](https://github.com/shlinkio/shlink/issues/67)
* [60: Accept JSON requests in REST and use a body parser middleware to set the parsedBody](https://github.com/shlinkio/shlink/issues/60)
* [72: When listing API keys from CLI, display in yellow color enabled keys that have expired](https://github.com/shlinkio/shlink/issues/72)
* [58: Allow to filter short URLs by tag](https://github.com/shlinkio/shlink/issues/58)
* [69: Allow to filter short codes by text query](https://github.com/shlinkio/shlink/issues/69)
**Tasks**
* [73: Tag endpoints in swagger file](https://github.com/shlinkio/shlink/issues/73)
* [71: Separate swagger docs into multiple files](https://github.com/shlinkio/shlink/issues/71)
* [63: Add path versioning to REST API routes](https://github.com/shlinkio/shlink/issues/63)
### 1.2.2
**Bugs**
* Fixed minor bugs on CORS requests
### 1.2.1
**Bugs**
* [62: Fix cross-domain requests in REST API](https://github.com/shlinkio/shlink/issues/62)
### 1.2.0
**Features**
* [45: Allow to define tags on short codes, to improve filtering and classification](https://github.com/shlinkio/shlink/issues/45)
* [7: Add website previews while listing available URLs](https://github.com/shlinkio/shlink/issues/7)
**Enhancements:**
* [57: Add database migrations system to improve updating between versions](https://github.com/shlinkio/shlink/issues/57)
* [31: Add support for other database management systems by improving the EntityManager factory](https://github.com/shlinkio/shlink/issues/31)
* [51: Generate build process to paquetize the app and ease distribution](https://github.com/shlinkio/shlink/issues/51)
* [38: Define installation script. It will request dynamic data on the fly so that there is no need to define env vars](https://github.com/shlinkio/shlink/issues/38)
**Tasks**
* [55: Create update script which does not try to create a new database](https://github.com/shlinkio/shlink/issues/55)
* [54: Add cache namespace to prevent name collisions with other apps in the same environment](https://github.com/shlinkio/shlink/issues/54)
* [29: Use the acelaya/ze-content-based-error-handler package instead of custom error handler implementation](https://github.com/shlinkio/shlink/issues/29)
**Bugs**
* [53: Fix entities database interoperability](https://github.com/shlinkio/shlink/issues/53)
* [52: Add missing htaccess file for apache environments](https://github.com/shlinkio/shlink/issues/52)
### 1.1.0
**Features**
* [46: Define a route that returns a QR code representing the shortened URL](https://github.com/shlinkio/shlink/issues/46)
**Enhancements:**
* [32: Add support for other cache adapters by improving the Cache factory](https://github.com/shlinkio/shlink/issues/32)
* [14: https://github.com/shlinkio/shlink/issues/14](https://github.com/shlinkio/shlink/issues/14)
* [41: Cache the "short code" => "URL" map to prevent extra DB hits](https://github.com/shlinkio/shlink/issues/41)
* [13: Improve REST authentication](https://github.com/shlinkio/shlink/issues/13)
**Tasks**
* [39: Change copyright from "Alejandro Celaya" to "Shlink" in error pages](https://github.com/shlinkio/shlink/issues/39)
* [42: Make REST endpoints that need to find something return a 404 when "something" is not found](https://github.com/shlinkio/shlink/issues/42)
* [35: Make CLI commands to use the same PHP namespace as the one used for the command name](https://github.com/shlinkio/shlink/issues/35)
**Bugs**
* [40: Take into account the X-Forwarded-For header in order to get the visitor information, in case the server is behind a load balancer or proxy](https://github.com/shlinkio/shlink/issues/40)
### 1.0.0
**Enhancements:**
* [33: Create a command to generate a short code charset by randomizing the default one](https://github.com/acelaya/url-shortener/issues/33)
* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/acelaya/url-shortener/issues/15)
* [23: Translate application literals](https://github.com/acelaya/url-shortener/issues/23)
* [21: Allow to filter visits by date range](https://github.com/acelaya/url-shortener/issues/21)
* [22: Save visits locations data on a visit_locations table](https://github.com/acelaya/url-shortener/issues/22)
* [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/acelaya/url-shortener/issues/20)
* [11: Separate code into multiple modules](https://github.com/acelaya/url-shortener/issues/11)
* [18: Group routable middleware in an Action namespace](https://github.com/acelaya/url-shortener/issues/18)
* [33: Create a command to generate a short code charset by randomizing the default one](https://github.com/shlinkio/shlink/issues/33)
* [15: Return JSON/HTML responses for errors (4xx and 5xx) based on accept header (content negotiation)](https://github.com/shlinkio/shlink/issues/15)
* [23: Translate application literals](https://github.com/shlinkio/shlink/issues/23)
* [21: Allow to filter visits by date range](https://github.com/shlinkio/shlink/issues/21)
* [22: Save visits locations data on a visit_locations table](https://github.com/shlinkio/shlink/issues/22)
* [20: Inject cross domain headers in response only if the Origin header is present in the request](https://github.com/shlinkio/shlink/issues/20)
* [11: Separate code into multiple modules](https://github.com/shlinkio/shlink/issues/11)
* [18: Group routable middleware in an Action namespace](https://github.com/shlinkio/shlink/issues/18)
**Tasks**
* [36: Remove hhvm from the CI matrix since it doesn't support array constants and will fail](https://github.com/acelaya/url-shortener/issues/36)
* [4: Installation steps](https://github.com/acelaya/url-shortener/issues/4)
* [6: Remove dependency on expressive helpers package](https://github.com/acelaya/url-shortener/issues/6)
* [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/acelaya/url-shortener/issues/30)
* [12: Improve code coverage](https://github.com/acelaya/url-shortener/issues/12)
* [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/acelaya/url-shortener/issues/25)
* [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/acelaya/url-shortener/issues/19)
* [36: Remove hhvm from the CI matrix since it doesn't support array constants and will fail](https://github.com/shlinkio/shlink/issues/36)
* [4: Installation steps](https://github.com/shlinkio/shlink/issues/4)
* [6: Remove dependency on expressive helpers package](https://github.com/shlinkio/shlink/issues/6)
* [30: Replace the "services" first level config entry by "dependencies", in order to fulfill default Expressive name](https://github.com/shlinkio/shlink/issues/30)
* [12: Improve code coverage](https://github.com/shlinkio/shlink/issues/12)
* [25: Replace "Middleware" suffix on routable middlewares by "Action"](https://github.com/shlinkio/shlink/issues/25)
* [19: Update the vendor and app namespace from Acelaya\UrlShortener to Shlinkio\Shlink](https://github.com/shlinkio/shlink/issues/19)
**Bugs**
* [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/acelaya/url-shortener/issues/24)
* [24: Prevent duplicated shortcodes errors because of the case insensitive behavior on MySQL](https://github.com/shlinkio/shlink/issues/24)
### 0.2.0
**Enhancements:**
* [9: Use symfony/console to dispatch console requests, instead of trying to integrate the process with expressive](https://github.com/acelaya/url-shortener/issues/9)
* [8: Create a REST API](https://github.com/acelaya/url-shortener/issues/8)
* [10: Add more CLI functionality](https://github.com/acelaya/url-shortener/issues/10)
* [9: Use symfony/console to dispatch console requests, instead of trying to integrate the process with expressive](https://github.com/shlinkio/shlink/issues/9)
* [8: Create a REST API](https://github.com/shlinkio/shlink/issues/8)
* [10: Add more CLI functionality](https://github.com/shlinkio/shlink/issues/10)
**Tasks**
* [5: Create CHANGELOG file](https://github.com/acelaya/url-shortener/issues/5)
* [5: Create CHANGELOG file](https://github.com/shlinkio/shlink/issues/5)

View File

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

View File

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

46
build.sh Executable file
View File

@@ -0,0 +1,46 @@
#!/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}"
sudo chmod -R 777 "${projectdir}"/data/infra/{database,nginx}
cp -R "${projectdir}"/* "${builtcontent}"
cd "${builtcontent}"
# Install dependencies
rm -r vendor
rm composer.lock
composer self-update
composer install --no-dev --optimize-autoloader --no-progress --no-interaction
# Delete development files
echo 'Deleting dev files...'
rm build.sh
rm CHANGELOG.md
rm composer.*
rm LICENSE
rm php*
rm README.md
rm -r build
rm -f data/database.sqlite
rm -rf data/infra
rm -rf data/{cache,log,proxies}/{*,.gitignore}
rm -rf config/params/{*,.gitignore}
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}
# Compressing file
rm -f "${projectdir}"/build/shlink_${version}_dist.zip
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip .
rm -rf "${builtcontent}"

View File

@@ -1,34 +1,43 @@
{
"name": "shlinkio/shlink",
"type": "project",
"homepage": "http://shlink.io",
"homepage": "https://shlink.io",
"description": "A self-hosted and PHP-based URL shortener application with CLI and REST interfaces",
"license": "MIT",
"authors": [
{
"name": "Alejandro Celaya Alastrué",
"homepage": "http://www.alejandrocelaya.com",
"homepage": "https://www.alejandrocelaya.com",
"email": "alejandro@alejandrocelaya.com"
}
],
"require": {
"php": "^5.6 || ^7.0",
"zendframework/zend-expressive": "^1.0",
"zendframework/zend-expressive-fastroute": "^1.1",
"zendframework/zend-expressive-twigrenderer": "^1.0",
"zendframework/zend-stdlib": "^2.7",
"zendframework/zend-expressive": "^2.0",
"zendframework/zend-expressive-fastroute": "^2.0",
"zendframework/zend-expressive-twigrenderer": "^1.4",
"zendframework/zend-stdlib": "^3.0",
"zendframework/zend-servicemanager": "^3.0",
"zendframework/zend-paginator": "^2.6",
"zendframework/zend-config": "^2.6",
"zendframework/zend-config": "^3.0",
"zendframework/zend-i18n": "^2.7",
"mtymek/expressive-config-manager": "^0.4",
"acelaya/zsm-annotated-services": "^0.2.0",
"zendframework/zend-config-aggregator": "^0.1",
"acelaya/zsm-annotated-services": "^1.0",
"acelaya/ze-content-based-error-handler": "^2.0",
"doctrine/orm": "^2.5",
"guzzlehttp/guzzle": "^6.2",
"symfony/console": "^3.0"
"symfony/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",
"phpunit/phpunit": "^5.7 || ^6.0",
"squizlabs/php_codesniffer": "^2.3",
"roave/security-advisories": "dev-master",
"filp/whoops": "^2.0",
@@ -64,5 +73,8 @@
"serve": "php -S 0.0.0.0:8000 -t public/",
"test": "phpunit --coverage-clover build/clover.xml",
"pretty-test": "phpunit --coverage-html build/coverage"
},
"config": {
"process-timeout": 0
}
}

View File

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

View File

@@ -0,0 +1,16 @@
<?php
return [
'entity_manager' => [
'orm' => [
'proxies_dir' => 'data/proxies',
],
'connection' => [
'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'dbname' => env('DB_NAME', 'shlink'),
'charset' => 'utf8',
],
],
];

View File

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

View File

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

View File

@@ -1,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,9 +1,30 @@
<?php
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware;
use Zend\Expressive\Container\ApplicationFactory;
use Zend\Stratigility\Middleware\ErrorHandler;
return [
'middleware_pipeline' => [
'pre-routing' => [
'middleware' => [
ErrorHandler::class,
LocaleMiddleware::class,
],
'priority' => 11,
],
'pre-routing-rest' => [
'path' => '/rest',
'middleware' => [
PathVersionMiddleware::class,
],
'priority' => 11,
],
'routing' => [
'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE,
@@ -11,6 +32,16 @@ return [
'priority' => 10,
],
'rest' => [
'path' => '/rest',
'middleware' => [
CrossDomainMiddleware::class,
BodyParserMiddleware::class,
CheckAuthenticationMiddleware::class,
],
'priority' => 5,
],
'post-routing' => [
'middleware' => [
ApplicationFactory::DISPATCH_MIDDLEWARE,

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -1,19 +0,0 @@
### Installation steps
- Define ENV vars in apache or nginx:
- SHORTENED_URL_SCHEMA: http|https
- SHORTENED_URL_HOSTNAME: Short domain
- SHORTCODE_CHARS: The char set used to generate short codes (defaults to **123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ**, but a new one can be generated with the `config:generate-charset` command)
- DB_USER: MySQL database user
- DB_PASSWORD: MySQL database password
- REST_USER: Username for REST authentication
- REST_PASSWORD: Password for REST authentication
- DB_NAME: MySQL database name (defaults to **shlink**)
- DEFAULT_LOCALE: Language in which web requests (browser and REST) will be returned if no `Accept-Language` header is sent (defaults to **en**)
- CLI_LOCALE: Language in which console command messages will be displayed (defaults to **en**)
- Create database (`vendor/bin/doctrine orm:schema-tool:create`)
- Add write permissions to `data` directory
- Create doctrine proxies (`vendor/bin/doctrine orm:generate-proxies`)
- Create symlink to bin/cli as `shlink` in /usr/local/bin (linux only. Optional)
Supported languages: es and en

View File

@@ -1,289 +0,0 @@
# REST API documentation
## Error management
Statuses:
* 400 -> controlled error
* 401 -> authentication error
* 500 -> unexpected error
[TODO]
## Authentication
Once you have called to the authentication endpoint for the first time (see below) yopu will get an authentication token.
You will have to send that token in the `X-Auth-Token` header on any later request or you will get an authentication error.
## Language
In order to set the application language, you have to pass it by using the `Accept-Language` header.
If not provided or provided language is not supported, english (en_US) will be used.
## 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/short-codes/{shortCode}/visits`
* Route params:
* shortCode: `string` -> The shortCode from which we eant to get the visits.
* Query params:
* startDate: `string` -> If provided, only visits older that this date will be returned
* endDate: `string` -> If provided, only visits newer that this date will be returned
* 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/infra/database/.gitignore vendored Normal file
View File

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

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

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

View File

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

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

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

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

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

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

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

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

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

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

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

41
docker-compose.yml Normal file
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,29 @@
{
"type": "object",
"properties": {
"shortCode": {
"type": "string",
"description": "The short code for this short URL."
},
"originalUrl": {
"type": "string",
"description": "The original long URL."
},
"dateCreated": {
"type": "string",
"format": "date-time",
"description": "The date in which the short URL was created in ISO format."
},
"visitsCount": {
"type": "integer",
"description": "The number of visits that this short URL has recieved."
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "A list of tags applied to this short URL"
}
}
}

View File

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

View File

@@ -0,0 +1,7 @@
{
"name": "Authorization",
"in": "header",
"description": "The authorization token with Bearer type",
"required": true,
"type": "string"
}

View File

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

View File

@@ -0,0 +1,144 @@
{
"get": {
"tags": [
"ShortCodes"
],
"summary": "List short URLs",
"description": "Returns the list of short codes",
"parameters": [
{
"name": "page",
"in": "query",
"description": "The page to be displayed. Defaults to 1",
"required": false,
"type": "integer"
},
{
"name": "searchTerm",
"in": "query",
"description": "A query used to filter results by searching for it on the longUrl and shortCode fields. (Since v1.3.0)",
"required": false,
"type": "string"
},
{
"name": "tags",
"in": "query",
"description": "A list of tags used to filter the resultset. Only short URLs tagged with at least one of the provided tags will be returned. (Since v1.3.0)",
"required": false,
"type": "array",
"items": {
"type": "string"
}
},
{
"name": "orderBy",
"in": "query",
"description": "The field from which you want to order the result. (Since v1.3.0)",
"enum": [
"originalUrl",
"shortCode",
"dateCreated",
"visits"
],
"required": false,
"type": "string"
},
{
"$ref": "../parameters/Authorization.json"
}
],
"responses": {
"200": {
"description": "The list of short URLs",
"schema": {
"type": "object",
"properties": {
"shortUrls": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "../definitions/ShortUrl.json"
}
},
"pagination": {
"$ref": "../definitions/Pagination.json"
}
}
}
}
}
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
},
"post": {
"tags": [
"ShortCodes"
],
"summary": "Create short URL",
"description": "Creates a new short code",
"parameters": [
{
"name": "longUrl",
"in": "formData",
"description": "The URL to parse",
"required": true,
"type": "string"
},
{
"name": "tags",
"in": "formData",
"description": "The URL to parse",
"required": false,
"type": "array",
"items": {
"type": "string"
}
},
{
"$ref": "../parameters/Authorization.json"
}
],
"responses": {
"200": {
"description": "The result of parsing the long URL",
"schema": {
"type": "object",
"properties": {
"longUrl": {
"type": "string",
"description": "The original long URL that has been parsed"
},
"shortUrl": {
"type": "string",
"description": "The generated short URL"
},
"shortCode": {
"type": "string",
"description": "the short code that is being used in the short URL"
}
}
}
},
"400": {
"description": "The long URL was not provided or is invalid.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}

View File

@@ -0,0 +1,53 @@
{
"get": {
"tags": [
"ShortCodes"
],
"summary": "Parse short code",
"description": "Get the long URL behind a short code.",
"parameters": [
{
"name": "shortCode",
"in": "path",
"type": "string",
"description": "The short code to resolve.",
"required": true
},
{
"$ref": "../parameters/Authorization.json"
}
],
"responses": {
"200": {
"description": "The long URL behind a short code.",
"schema": {
"type": "object",
"properties": {
"longUrl": {
"type": "string",
"description": "The original long URL behind the short code."
}
}
}
},
"400": {
"description": "Provided shortCode does not match the character set currently used by the app to generate short codes.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"404": {
"description": "No URL was found for provided short code.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}

View File

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

View File

@@ -0,0 +1,55 @@
{
"get": {
"tags": [
"ShortCodes",
"Visits"
],
"summary": "List visits for short URL",
"description": "Get the list of visits on provided short code.",
"parameters": [
{
"name": "shortCode",
"in": "path",
"type": "string",
"description": "The shortCode from which we want to get the visits.",
"required": true
},
{
"$ref": "../parameters/Authorization.json"
}
],
"responses": {
"200": {
"description": "List of visits.",
"schema": {
"type": "object",
"properties": {
"visits": {
"type": "object",
"properties": {
"data": {
"type": "array",
"items": {
"$ref": "../definitions/Visit.json"
}
}
}
}
}
}
},
"404": {
"description": "The short code does not belong to any short URL.",
"schema": {
"$ref": "../definitions/Error.json"
}
},
"500": {
"description": "Unexpected error.",
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}

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

@@ -0,0 +1,38 @@
{
"swagger": "2.0",
"info": {
"title": "Shlink",
"description": "Shlink, the self-hosted URL shortener",
"version": "1.2.0"
},
"schemes": [
"http",
"https"
],
"basePath": "/rest",
"produces": [
"application/json"
],
"consumes": [
"application/x-www-form-urlencoded",
"application/json"
],
"paths": {
"/v1/authenticate": {
"$ref": "paths/v1_authenticate.json"
},
"/v1/short-codes": {
"$ref": "paths/v1_short-codes.json"
},
"/v1/short-codes/{shortCode}": {
"$ref": "paths/v1_short-codes_{shortCode}.json"
},
"/v1/short-codes/{shortCode}/visits": {
"$ref": "paths/v1_short-codes_{shortCode}_visits.json"
},
"/v1/short-codes/{shortCode}/tags": {
"$ref": "paths/v1_short-codes_{shortCode}_tags.json"
}
}
}

2
indocker Executable file
View File

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

4
migrations.yml Normal file
View File

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

View File

@@ -4,13 +4,19 @@ use Shlinkio\Shlink\CLI\Command;
return [
'cli' => [
'locale' => env('CLI_LOCALE', 'en'),
'commands' => [
Command\GenerateShortcodeCommand::class,
Command\ResolveUrlCommand::class,
Command\ListShortcodesCommand::class,
Command\GetVisitsCommand::class,
Command\ProcessVisitsCommand::class,
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

@@ -10,13 +10,17 @@ return [
'factories' => [
Application::class => ApplicationFactory::class,
Command\GenerateShortcodeCommand::class => AnnotatedFactory::class,
Command\ResolveUrlCommand::class => AnnotatedFactory::class,
Command\ListShortcodesCommand::class => AnnotatedFactory::class,
Command\GetVisitsCommand::class => AnnotatedFactory::class,
Command\ProcessVisitsCommand::class => AnnotatedFactory::class,
Command\ProcessVisitsCommand::class => AnnotatedFactory::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,
],
],

Binary file not shown.

View File

@@ -1,8 +1,8 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2016-08-01 21:21+0200\n"
"PO-Revision-Date: 2016-08-01 21:22+0200\n"
"POT-Creation-Date: 2016-10-22 23:12+0200\n"
"PO-Revision-Date: 2016-10-22 23:13+0200\n"
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
"Language-Team: \n"
"Language: es_ES\n"
@@ -17,6 +17,46 @@ msgstr ""
"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\". "
@@ -28,7 +68,35 @@ msgstr ""
msgid "Character set:"
msgstr "Grupo de caracteres:"
#, fuzzy
msgid ""
"Generates a random secret string that can be used for JWT token encryption"
msgstr ""
"Genera una cadena de caracteres aleatoria que puede ser usada para cifrar "
"tokens JWT"
msgid "Secret key:"
msgstr "Clave secreta:"
msgid ""
"Processes and generates the previews for every URL, improving performance "
"for later web requests."
msgstr ""
"Procesa y genera las vistas previas para cada URL, mejorando el rendimiento "
"para peticiones web posteriores."
msgid "Finished processing all URLs"
msgstr "Finalizado el procesado de todas las URLs"
#, php-format
msgid "Processing URL %s..."
msgstr "Procesando URL %s..."
msgid " <info>Success!</info>"
msgstr "<info>¡Correcto!</info>"
msgid "Error"
msgstr "Error"
msgid "Generates a short code for provided URL and returns the short URL"
msgstr ""
"Genera un código corto para la URL proporcionada y devuelve la URL acortada"
@@ -36,6 +104,9 @@ msgstr ""
msgid "The long URL to parse"
msgstr "La URL larga a procesar"
msgid "Tags to apply to the new short URL"
msgstr "Etiquetas a aplicar a la nueva URL acortada"
msgid "A long URL was not provided. Which URL do you want to shorten?:"
msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?"
@@ -91,6 +162,25 @@ msgstr "Listar todas las URLs cortas"
msgid "The first page to list (%s items per page)"
msgstr "La primera página a listar (%s elementos por página)"
msgid ""
"A query used to filter results by searching for it on the longUrl and "
"shortCode fields"
msgstr ""
"Una consulta usada para filtrar el resultado buscándola en los campos "
"longUrl y shortCode"
msgid "A comma-separated list of tags to filter results"
msgstr "Una lista de etiquetas separadas por coma para filtrar el resultado"
msgid ""
"The field from which we want to order by. Pass ASC or DESC separated by a "
"comma"
msgstr ""
"El campo por el cual queremos ordernar. Pasa ASC o DESC separado por una coma"
msgid "Whether to display the tags or not"
msgstr "Si se desea mostrar las etiquetas o no"
msgid "Short code"
msgstr "Código corto"
@@ -103,28 +193,15 @@ msgstr "Fecha de creación"
msgid "Visits count"
msgstr "Número de visitas"
msgid "Tags"
msgstr "Etiquetas"
msgid "You have reached last page"
msgstr "Has alcanzado la última página"
msgid "Continue with page"
msgstr "Continuar con la página"
msgid "Processes visits where location is not set yet"
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
msgid "Processing IP"
msgstr "Procesando IP"
msgid "Ignored localhost address"
msgstr "Ignorada IP de localhost"
#, php-format
msgid "Address located at \"%s\""
msgstr "Dirección localizada en \"%s\""
msgid "Finished processing all IPs"
msgstr "Finalizado el procesado de todas las IPs"
msgid "Returns the long URL behind a short code"
msgstr "Devuelve la URL larga detrás de un código corto"
@@ -145,3 +222,19 @@ 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,129 @@
<?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 = [];
$formatMethod = ! $row->isEnabled()
? 'getErrorString'
: ($row->isExpired() ? 'getWarningString' : 'getSuccessString');
if ($enabledOnly) {
$rowData[] = $this->{$formatMethod}($key);
} else {
$rowData[] = $this->{$formatMethod}($key);
$rowData[] = $this->{$formatMethod}($this->getEnabledSymbol($row));
}
$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);
}
/**
* @param $string
* @return string
*/
protected function getWarningString($string)
{
return sprintf('<comment>%s</comment>', $string);
}
/**
* @param ApiKey $apiKey
* @return string
*/
protected function getEnabledSymbol(ApiKey $apiKey)
{
return ! $apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++';
}
}

View File

@@ -0,0 +1,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,322 @@
<?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');
$params['HOST'] = $this->ask('Database host', 'localhost');
$params['PORT'] = $this->ask('Database port', $this->getDefaultDbPort($params['DRIVER']));
}
return $params;
}
protected function getDefaultDbPort($driver)
{
return $driver === 'pdo_mysql' ? '3306' : '5432';
}
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'];
$config['entity_manager']['connection']['host'] = $params['DATABASE']['HOST'];
$config['entity_manager']['connection']['port'] = $params['DATABASE']['PORT'];
if ($params['DATABASE']['DRIVER'] === 'pdo_mysql') {
$config['entity_manager']['connection']['driverOptions'] = [
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
];
}
}
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

@@ -1,5 +1,5 @@
<?php
namespace Shlinkio\Shlink\CLI\Command;
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
@@ -9,6 +9,7 @@ use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Zend\Diactoros\Uri;
@@ -31,7 +32,7 @@ class GenerateShortcodeCommand extends Command
/**
* GenerateShortcodeCommand constructor.
* @param UrlShortenerInterface|UrlShortener $urlShortener
* @param UrlShortenerInterface $urlShortener
* @param TranslatorInterface $translator
* @param array $domainConfig
*
@@ -54,7 +55,13 @@ class GenerateShortcodeCommand extends Command
->setDescription(
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
)
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'));
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
->addOption(
'tags',
't',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
$this->translator->translate('Tags to apply to the new short URL')
);
}
public function interact(InputInterface $input, OutputInterface $output)
@@ -80,6 +87,13 @@ class GenerateShortcodeCommand extends Command
public function execute(InputInterface $input, OutputInterface $output)
{
$longUrl = $input->getArgument('longUrl');
$tags = $input->getOption('tags');
$processedTags = [];
foreach ($tags as $key => $tag) {
$explodedTags = explode(',', $tag);
$processedTags = array_merge($processedTags, $explodedTags);
}
$tags = $processedTags;
try {
if (! isset($longUrl)) {
@@ -87,10 +101,10 @@ class GenerateShortcodeCommand extends Command
return;
}
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl));
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl), $tags);
$shortUrl = (new Uri())->withPath($shortCode)
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
$output->writeln([
sprintf('%s <info>%s</info>', $this->translator->translate('Processed URL:'), $longUrl),

View File

@@ -1,5 +1,5 @@
<?php
namespace Shlinkio\Shlink\CLI\Command;
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Util\DateRange;
@@ -28,7 +28,7 @@ class GetVisitsCommand extends Command
/**
* GetVisitsCommand constructor.
* @param VisitsTrackerInterface|VisitsTracker $visitsTracker
* @param VisitsTrackerInterface $visitsTracker
* @param TranslatorInterface $translator
*
* @Inject({VisitsTracker::class, "translator"})

View File

@@ -1,5 +1,5 @@
<?php
namespace Shlinkio\Shlink\CLI\Command;
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
@@ -30,7 +30,7 @@ class ListShortcodesCommand extends Command
/**
* ListShortcodesCommand constructor.
* @param ShortUrlServiceInterface|ShortUrlService $shortUrlService
* @param ShortUrlServiceInterface $shortUrlService
* @param TranslatorInterface $translator
*
* @Inject({ShortUrlService::class, "translator"})
@@ -55,28 +55,78 @@ class ListShortcodesCommand extends Command
PaginableRepositoryAdapter::ITEMS_PER_PAGE
),
1
)
->addOption(
'searchTerm',
's',
InputOption::VALUE_OPTIONAL,
$this->translator->translate(
'A query used to filter results by searching for it on the longUrl and shortCode fields'
)
)
->addOption(
'tags',
't',
InputOption::VALUE_OPTIONAL,
$this->translator->translate('A comma-separated list of tags to filter results')
)
->addOption(
'orderBy',
'o',
InputOption::VALUE_OPTIONAL,
$this->translator->translate(
'The field from which we want to order by. Pass ASC or DESC separated by a comma'
)
)
->addOption(
'showTags',
null,
InputOption::VALUE_NONE,
$this->translator->translate('Whether to display the tags or not')
);
}
public function execute(InputInterface $input, OutputInterface $output)
{
$page = intval($input->getOption('page'));
$searchTerm = $input->getOption('searchTerm');
$tags = $input->getOption('tags');
$tags = ! empty($tags) ? explode(',', $tags) : [];
$showTags = $input->getOption('showTags');
$orderBy = $input->getOption('orderBy');
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
do {
$result = $this->shortUrlService->listShortUrls($page);
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
$page++;
$table = new Table($output);
$table->setHeaders([
$headers = [
$this->translator->translate('Short code'),
$this->translator->translate('Original URL'),
$this->translator->translate('Date created'),
$this->translator->translate('Visits count'),
]);
];
if ($showTags) {
$headers[] = $this->translator->translate('Tags');
}
$table->setHeaders($headers);
foreach ($result as $row) {
$table->addRow(array_values($row->jsonSerialize()));
$shortUrl = $row->jsonSerialize();
if ($showTags) {
$shortUrl['tags'] = [];
foreach ($row->getTags() as $tag) {
$shortUrl['tags'][] = $tag->getName();
}
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
} else {
unset($shortUrl['tags']);
}
$table->addRow(array_values($shortUrl));
}
$table->render();
@@ -95,4 +145,15 @@ class ListShortcodesCommand extends Command
}
} while ($continue);
}
protected function processOrderBy(InputInterface $input)
{
$orderBy = $input->getOption('orderBy');
if (empty($orderBy)) {
return null;
}
$orderBy = explode(',', $orderBy);
return count($orderBy) === 1 ? $orderBy[0] : [$orderBy[0] => $orderBy[1]];
}
}

View File

@@ -1,5 +1,5 @@
<?php
namespace Shlinkio\Shlink\CLI\Command;
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
@@ -26,7 +26,7 @@ class ResolveUrlCommand extends Command
/**
* ResolveUrlCommand constructor.
* @param UrlShortenerInterface|UrlShortener $urlShortener
* @param UrlShortenerInterface $urlShortener
* @param TranslatorInterface $translator
*
* @Inject({UrlShortener::class, "translator"})

View File

@@ -1,5 +1,5 @@
<?php
namespace Shlinkio\Shlink\CLI\Command;
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
@@ -32,8 +32,8 @@ class ProcessVisitsCommand extends Command
/**
* ProcessVisitsCommand constructor.
* @param VisitServiceInterface|VisitService $visitService
* @param IpLocationResolverInterface|IpLocationResolver $ipLocationResolver
* @param VisitServiceInterface $visitService
* @param IpLocationResolverInterface $ipLocationResolver
* @param TranslatorInterface $translator
*
* @Inject({VisitService::class, IpLocationResolver::class, "translator"})

View File

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

View File

@@ -0,0 +1,62 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit\Framework\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;
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;
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

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

View File

@@ -0,0 +1,111 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command\Install;
use PHPUnit\Framework\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', 'rb+', false);
fwrite($stream, <<<CLI_INPUT
shlink_db
alejandro
1234
0
doma.in
abc123BCA
1
my_secret
CLI_INPUT
);
rewind($stream);
return $stream;
}
/**
* @test
*/
public function inputIsProperlyParsed()
{
$this->configWriter->toFile(Argument::any(), [
'app_options' => [
'secret_key' => 'my_secret',
],
'entity_manager' => [
'connection' => [
'driver' => 'pdo_mysql',
'dbname' => 'shlink_db',
'user' => 'alejandro',
'password' => '1234',
'host' => 'localhost',
'port' => '3306',
'driverOptions' => [
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',
]
],
],
'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;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\GeneratePreviewCommand;
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Tester\CommandTester;
use Zend\I18n\Translator\Translator;
use Zend\Paginator\Adapter\ArrayAdapter;
use Zend\Paginator\Paginator;
class GeneratePreviewCommandTest extends TestCase
{
/**
* @var CommandTester
*/
protected $commandTester;
/**
* @var ObjectProphecy
*/
private $previewGenerator;
/**
* @var ObjectProphecy
*/
private $shortUrlService;
public function setUp()
{
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
$command = new GeneratePreviewCommand(
$this->shortUrlService->reveal(),
$this->previewGenerator->reveal(),
Translator::factory([])
);
$app = new Application();
$app->add($command);
$this->commandTester = new CommandTester($command);
}
/**
* @test
*/
public function previewsForEveryUrlAreGenerated()
{
$paginator = $this->createPaginator([
(new ShortUrl())->setOriginalUrl('http://foo.com'),
(new ShortUrl())->setOriginalUrl('https://bar.com'),
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
]);
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
$this->previewGenerator->generatePreview('http://foo.com')->shouldBeCalledTimes(1);
$this->previewGenerator->generatePreview('https://bar.com')->shouldBeCalledTimes(1);
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:process-previews'
]);
}
/**
* @test
*/
public function exceptionWillOutputError()
{
$items = [
(new ShortUrl())->setOriginalUrl('http://foo.com'),
(new ShortUrl())->setOriginalUrl('https://bar.com'),
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
];
$paginator = $this->createPaginator($items);
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
$this->previewGenerator->generatePreview(Argument::any())->willThrow(PreviewGenerationException::class)
->shouldBeCalledTimes(count($items));
$this->commandTester->execute([
'command' => 'shortcode:process-previews'
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals(count($items), substr_count($output, 'Error'));
}
protected function createPaginator(array $items)
{
$paginator = new Paginator(new ArrayAdapter($items));
$paginator->setItemCountPerPage(count($items));
return $paginator;
}
}

View File

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

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
<?php
namespace ShlinkioTest\Shlink\CLI\Command;
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit_Framework_TestCase as TestCase;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\ResolveUrlCommand;
use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;

View File

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

View File

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

View File

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

View File

@@ -2,36 +2,46 @@
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
use Doctrine\Common\Cache\Cache;
use Doctrine\ORM\EntityManager;
use Shlinkio\Shlink\Common\ErrorHandler;
use Shlinkio\Shlink\Common\Factory\CacheFactory;
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
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\IpLocationResolver;
use Shlinkio\Shlink\Common\Service;
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
use Symfony\Component\Filesystem\Filesystem;
use Zend\I18n\Translator\Translator;
use Zend\ServiceManager\Factory\InvokableFactory;
return [
'dependencies' => [
'invokables' => [
Filesystem::class => Filesystem::class,
],
'factories' => [
EntityManager::class => EntityManagerFactory::class,
EntityManager::class => Factory\EntityManagerFactory::class,
GuzzleHttp\Client::class => InvokableFactory::class,
Cache::class => CacheFactory::class,
IpLocationResolver::class => AnnotatedFactory::class,
Translator::class => TranslatorFactory::class,
Cache::class => Factory\CacheFactory::class,
'Logger_Shlink' => Factory\LoggerFactory::class,
Translator::class => Factory\TranslatorFactory::class,
TranslatorExtension::class => AnnotatedFactory::class,
LocaleMiddleware::class => AnnotatedFactory::class,
ErrorHandler\ContentBasedErrorHandler::class => AnnotatedFactory::class,
ErrorHandler\ErrorHandlerManager::class => ErrorHandler\ErrorHandlerManagerFactory::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

@@ -1,22 +0,0 @@
<?php
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
use Zend\Expressive\Container\TemplatedErrorHandlerFactory;
use Zend\Stratigility\FinalHandler;
return [
'error_handler' => [
'plugins' => [
'invokables' => [
'text/plain' => FinalHandler::class,
],
'factories' => [
ContentBasedErrorHandler::DEFAULT_CONTENT => TemplatedErrorHandlerFactory::class,
],
'aliases' => [
'application/xhtml+xml' => ContentBasedErrorHandler::DEFAULT_CONTENT,
],
],
],
];

View File

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

View File

@@ -1,76 +0,0 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
class ContentBasedErrorHandler implements ErrorHandlerInterface
{
const DEFAULT_CONTENT = 'text/html';
/**
* @var ErrorHandlerManagerInterface
*/
private $errorHandlerManager;
/**
* ContentBasedErrorHandler constructor.
* @param ErrorHandlerManagerInterface|ErrorHandlerManager $errorHandlerManager
*
* @Inject({ErrorHandlerManager::class})
*/
public function __construct(ErrorHandlerManagerInterface $errorHandlerManager)
{
$this->errorHandlerManager = $errorHandlerManager;
}
/**
* Final handler for an application.
*
* @param Request $request
* @param Response $response
* @param null|mixed $err
* @return Response
*/
public function __invoke(Request $request, Response $response, $err = null)
{
// Try to get an error handler for provided request accepted type
$errorHandler = $this->resolveErrorHandlerFromAcceptHeader($request);
return $errorHandler($request, $response, $err);
}
/**
* Tries to resolve
*
* @param Request $request
* @return callable
*/
protected function resolveErrorHandlerFromAcceptHeader(Request $request)
{
// Try to find an error handler for one of the accepted content types
$accepts = $request->hasHeader('Accept') ? $request->getHeaderLine('Accept') : self::DEFAULT_CONTENT;
$accepts = explode(',', $accepts);
foreach ($accepts as $accept) {
if (! $this->errorHandlerManager->has($accept)) {
continue;
}
return $this->errorHandlerManager->get($accept);
}
// If it wasn't possible to find an error handler for accepted content type, use default one if registered
if ($this->errorHandlerManager->has(self::DEFAULT_CONTENT)) {
return $this->errorHandlerManager->get(self::DEFAULT_CONTENT);
}
// It wasn't possible to find an error handler
throw new InvalidArgumentException(sprintf(
'It wasn\'t possible to find an error handler for ["%s"] content types. '
. 'Make sure you have registered at least the default "%s" content type',
implode('", "', $accepts),
self::DEFAULT_CONTENT
));
}
}

View File

@@ -1,18 +0,0 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
interface ErrorHandlerInterface
{
/**
* Final handler for an application.
*
* @param Request $request
* @param Response $response
* @param null|mixed $err
* @return Response
*/
public function __invoke(Request $request, Response $response, $err = null);
}

View File

@@ -1,21 +0,0 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
use Zend\ServiceManager\AbstractPluginManager;
use Zend\ServiceManager\Exception\InvalidServiceException;
class ErrorHandlerManager extends AbstractPluginManager implements ErrorHandlerManagerInterface
{
public function validate($instance)
{
if (is_callable($instance)) {
return;
}
throw new InvalidServiceException(sprintf(
'Only callables are valid plugins for "%s". "%s" provided',
__CLASS__,
is_object($instance) ? get_class($instance) : gettype($instance)
));
}
}

View File

@@ -1,9 +0,0 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
use Interop\Container\ContainerInterface;
interface ErrorHandlerManagerInterface extends ContainerInterface
{
}

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,10 +1,10 @@
<?php
namespace Shlinkio\Shlink\Common\Factory;
use Doctrine\Common\Cache\ApcuCache;
use Doctrine\Common\Cache\ArrayCache;
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;
@@ -12,8 +12,11 @@ use Zend\ServiceManager\Factory\FactoryInterface;
class CacheFactory implements FactoryInterface
{
const VALID_CACHE_ADAPTERS = [
ApcuCache::class,
ArrayCache::class,
Cache\ApcuCache::class,
Cache\ArrayCache::class,
Cache\FilesystemCache::class,
Cache\PhpFileCache::class,
Cache\MemcachedCache::class,
];
/**
@@ -29,6 +32,19 @@ class CacheFactory implements FactoryInterface
* @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');
@@ -36,10 +52,44 @@ class CacheFactory implements FactoryInterface
&& isset($config['cache']['adapter'])
&& in_array($config['cache']['adapter'], self::VALID_CACHE_ADAPTERS)
) {
return new $config['cache']['adapter']();
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 ApcuCache() : new ArrayCache();
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

@@ -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(
['module/Core/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,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,13 +1,14 @@
<?php
namespace Shlinkio\Shlink\Common\ErrorHandler;
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 ErrorHandlerManagerFactory implements FactoryInterface
class ImageBuilderFactory implements FactoryInterface
{
/**
* Create an object
@@ -23,8 +24,8 @@ class ErrorHandlerManagerFactory implements FactoryInterface
*/
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
$config = $container->get('config')['error_handler'];
$plugins = isset($config['plugins']) ? $config['plugins'] : [];
return new ErrorHandlerManager($container, $plugins);
return new 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
{
}

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