Compare commits

...

139 Commits

Author SHA1 Message Date
Alejandro Celaya
aca90ef907 Merge pull request #111 from shlinkio/develop
Develop
2017-10-25 16:25:39 +02:00
Alejandro Celaya
ddfccea901 Updated changelog 2017-10-25 16:21:34 +02:00
Alejandro Celaya
6c6cfb4fc3 Fixed typo 2017-10-25 16:19:08 +02:00
Alejandro Celaya
482b792adf Merge branch 'develop' 2017-10-24 17:45:54 +02:00
Alejandro Celaya
d3a86f4fae Added v1.6.1 to CHANGELOG 2017-10-24 17:45:19 +02:00
Alejandro Celaya
6de1d1057b Created gitattributes file 2017-10-24 17:43:19 +02:00
Alejandro Celaya
d7e68d29de Merge branch 'develop' 2017-10-23 14:01:22 +02:00
Alejandro Celaya
b8f24c3584 Merge pull request #107 from acelaya/feature/1.6
Feature/1.6
2017-10-23 14:00:24 +02:00
Alejandro Celaya
26c455616b Ensured validate_url does not fail when importing config, and instead, it gets a default BC value 2017-10-23 13:56:07 +02:00
Alejandro Celaya
909ecc2387 Ensured build does not fail when trying to delete missing optional files 2017-10-23 13:44:50 +02:00
Alejandro Celaya
a9dff56a92 Updated config aggregator version 2017-10-23 13:33:23 +02:00
Alejandro Celaya
ef0dd416f9 Updated CHANGELOG 2017-10-23 13:30:26 +02:00
Alejandro Celaya
781ca39938 Fixed merge conflicts 2017-10-23 13:28:09 +02:00
Alejandro Celaya
ccb9d5e83a Merge pull request #105 from gogus/feature/url-validation-option
Added option for enable/disable URL Validation by response status code.
2017-10-23 13:18:30 +02:00
Alejandro Celaya
91f08c9ead Updated CHANGELOG 2017-10-23 13:09:10 +02:00
Alejandro Celaya
433a5a923d Improved ShortUrlRepositoryTest 2017-10-23 13:06:29 +02:00
Alejandro Celaya
501a933d2e Created ShortUrlRepositoryTest 2017-10-23 13:03:23 +02:00
Alejandro Celaya
633f3b728f Created composer command to generate merge pretty code coverage 2017-10-23 12:54:54 +02:00
Alejandro Celaya
be34600494 Updated CI process to generate a merged coverage file 2017-10-23 12:51:06 +02:00
Alejandro Celaya
9577a4da4b Refactored ShortUrlRepository to make it more readable 2017-10-23 12:36:03 +02:00
Alejandro Celaya
a24688b92a Created VisitRepositoryTest 2017-10-23 12:33:46 +02:00
Alejandro Celaya
91442a3379 Ensured same testing database is used to generate with entities and to run functional tests 2017-10-23 12:19:28 +02:00
Alejandro Celaya
0bd9f1e19f Removed wrong extension for travis 2017-10-23 12:09:07 +02:00
Alejandro Celaya
c233e807c2 Added sqlite extensions to PDO 2017-10-23 11:42:13 +02:00
Alejandro Celaya
a002c60183 Updated travis config so that it loads apcu extension 2017-10-23 11:37:16 +02:00
Alejandro Celaya
c522879c64 Updated composer check to ru functional tests too 2017-10-23 11:29:37 +02:00
Mikolaj Gogula
d04abd1f75 Added validate_url config key for development. 2017-10-23 11:28:04 +02:00
Alejandro Celaya
c2feffa50c First version of functional tests working 2017-10-23 11:21:00 +02:00
Alejandro Celaya
e282521040 Updated migrations to prevent duplication when running after orm schema tool 2017-10-23 11:11:26 +02:00
Alejandro Celaya
d7b7db670f Created first common elements for functional tests 2017-10-22 18:03:35 +02:00
Alejandro Celaya
5a500a00d7 Added another feature to CHANGELOG 2017-10-22 09:18:28 +02:00
Alejandro Celaya
9fb07f4039 Fixed tests 2017-10-22 09:17:19 +02:00
Alejandro Celaya
cb23d38b38 Used maxVisits field when creating or fetching a ShortUrl 2017-10-22 09:15:37 +02:00
Alejandro Celaya
af7c11665c Added max_visits field to short_urls 2017-10-22 09:00:32 +02:00
Alejandro Celaya
7f4678261e Added first tasks to the CHANGELOG 2017-10-21 20:27:19 +02:00
Alejandro Celaya
a1c8c51f70 Updated translations 2017-10-21 20:21:08 +02:00
Alejandro Celaya
6bbe66e8f1 Improved CreateShortcodeActiontest 2017-10-21 20:16:39 +02:00
Alejandro Celaya
5f0d281255 Updated create shortcode action to accept the custom slug 2017-10-21 20:09:30 +02:00
Alejandro Celaya
fd468cd4e9 Added support for custom slug in shortcode command 2017-10-21 17:32:05 +02:00
Alejandro Celaya
1f7a94794d Added option to provide custom slug when creating a short url 2017-10-21 17:19:02 +02:00
Alejandro Celaya
0232f68b91 Updated action and command to create short urls so that it accepts validity dates 2017-10-21 12:24:53 +02:00
Alejandro Celaya
070055a8b9 Fixed type hints 2017-10-21 11:59:31 +02:00
Alejandro Celaya
a3bbd06fe3 Updated UrlShortener so that it does not match a short code which is out of the validity dat erange 2017-10-21 11:58:20 +02:00
Alejandro Celaya
68b4cfbae0 Added valid_since and valid_until columns to shoirt_urls table 2017-10-21 11:39:27 +02:00
Alejandro Celaya
97a54aef06 Imported function in config file 2017-10-21 11:28:44 +02:00
Mikolaj Gogula
297c88c334 Change variable name for validation enabled. 2017-10-17 11:44:30 +02:00
Mikolaj Gogula
fef5390a62 Codestyle fixes. 2017-10-17 11:35:32 +02:00
Mikolaj Gogula
08d18b1dc1 Codestyle fixes. 2017-10-17 11:33:11 +02:00
Mikolaj Gogula
16a2349d86 Composer fixes. 2017-10-17 11:28:05 +02:00
Mikolaj Gogula
18d9815e88 Added option for enable/disable URL Validation by response status code. 2017-10-17 11:03:12 +02:00
Alejandro Celaya
c8346bc5f8 Added target php platform in composer.json to prevent building versions that cannot be executed in older versions 2017-10-16 18:23:13 +02:00
Alejandro Celaya
2d85a207d1 Removed comented translations 2017-10-13 12:31:44 +02:00
Alejandro Celaya
0df8f17e7b Added new translations 2017-10-13 12:30:54 +02:00
Alejandro Celaya
29645e77cf Created DottedAccessConfigAbstractFactory 2017-10-13 12:27:20 +02:00
Alejandro Celaya
ea76092681 Ensured a generic template is used to render generic 404 errors, and a more specific one to render 'invalid short url' errors 2017-10-13 12:22:19 +02:00
Alejandro Celaya
c12e13dfd7 Created NotFoundDelegateTest 2017-10-13 12:02:00 +02:00
Alejandro Celaya
566940349f Created default delegate that returns a JSON response when accepted type is json 2017-10-13 11:55:14 +02:00
Alejandro Celaya
391ef5c323 Added return typehint 2017-10-12 11:59:22 +02:00
Alejandro Celaya
70264be8e7 Fixed coding styles 2017-10-12 11:29:11 +02:00
Alejandro Celaya
6208f6f0d5 Improved Exception management to be more specific 2017-10-12 11:28:45 +02:00
Alejandro Celaya
c422a14c5c Improved coding styles 2017-10-12 10:13:20 +02:00
Alejandro Celaya
fbeb959317 Fixed tests 2017-10-12 10:03:43 +02:00
Alejandro Celaya
e53ffc8d43 Migrated templates to plates 2017-10-12 10:03:20 +02:00
Alejandro Celaya
453ca1728e Updated system to use plates instead of twig 2017-10-12 09:40:42 +02:00
Alejandro Celaya
d10583c7c5 Udated minimum PHP version and docker stuff 2017-10-12 09:24:47 +02:00
Alejandro Celaya
a9d213990e Merge branch 'feature/1.6' of github.com:acelaya/shlink into feature/1.6 2017-09-30 12:14:05 +02:00
Alejandro Celaya
6321cc0f2d Added security-advisories dependency 2017-09-30 12:13:47 +02:00
Alejandro Celaya
3e5f0b2451 Ensured a doctrine/cache version which requires PHP 7.1 is not installed 2017-09-30 12:13:47 +02:00
Alejandro Celaya
4ad167eb30 Ensured all doctrine components are not updated to latest releases, which require PHP 7.1 2017-09-30 12:13:47 +02:00
Alejandro Celaya
01a4f9f867 Removed any remaining reference to AnnotatedFactory 2017-09-30 12:13:47 +02:00
Alejandro Celaya
b93d65ddc1 Replaced more ussages of AnnotatedFactory by ConfigAbstractFactory 2017-09-30 12:13:47 +02:00
Alejandro Celaya
9ef9da0870 Replaced more ussages of AnnotatedFactory by ConfigAbstractFactory 2017-09-30 12:13:47 +02:00
Alejandro Celaya
ba2053bd3a Dropped AnnotatedFactory in commands and replaced by ConfigAbstractFactory 2017-09-30 12:13:47 +02:00
Alejandro Celaya
1260da85a7 Added PHP 7.2 to the build matrix 2017-09-30 12:13:12 +02:00
Alejandro Celaya
960c7a0835 Added security-advisories dependency 2017-08-19 19:41:07 +02:00
Alejandro Celaya
574f407a90 Ensured a doctrine/cache version which requires PHP 7.1 is not installed 2017-07-23 11:51:47 +02:00
Alejandro Celaya
abf802093c Ensured all doctrine components are not updated to latest releases, which require PHP 7.1 2017-07-23 08:42:57 +02:00
Alejandro Celaya
7ca22f8629 Removed any remaining reference to AnnotatedFactory 2017-07-22 14:20:40 +02:00
Alejandro Celaya
6300982b07 Replaced more ussages of AnnotatedFactory by ConfigAbstractFactory 2017-07-22 13:48:30 +02:00
Alejandro Celaya
54cb40f6ed Replaced more ussages of AnnotatedFactory by ConfigAbstractFactory 2017-07-22 13:41:41 +02:00
Alejandro Celaya
8256f0c757 Dropped AnnotatedFactory in commands and replaced by ConfigAbstractFactory 2017-07-22 13:33:32 +02:00
Alejandro Celaya
1009f9e7c6 Merge pull request #98 from shlinkio/develop
Develop
2017-07-16 10:08:38 +02:00
Alejandro Celaya
9c8eef12ba Merge pull request #97 from acelaya/feature/1.5
Feature/1.5
2017-07-16 10:01:50 +02:00
Alejandro Celaya
9260b3ac6b Fixed coding styles 2017-07-16 09:58:03 +02:00
Alejandro Celaya
ff98e1fb3d Added v1.5.0 to CHANGELOG 2017-07-16 09:43:35 +02:00
Alejandro Celaya
f3389d3738 Updated language files 2017-07-16 09:40:34 +02:00
Alejandro Celaya
a138f4153d Created DeleteTagsCommand 2017-07-16 09:35:24 +02:00
Alejandro Celaya
602e11d5e7 Added namespace to functions 2017-07-16 09:28:40 +02:00
Alejandro Celaya
3cd14153ca Created command to rename tag 2017-07-16 09:24:21 +02:00
Alejandro Celaya
095d8e73b8 Created ListTagsCommand 2017-07-16 09:13:25 +02:00
Alejandro Celaya
b37f303e76 Created CreateTagCommand 2017-07-16 09:09:11 +02:00
Alejandro Celaya
c8368c9098 Updated language files 2017-07-15 12:16:15 +02:00
Alejandro Celaya
8d0bac9478 Documented delete and edit tags endpoints 2017-07-15 12:13:59 +02:00
Alejandro Celaya
286c24f8c0 Improved TagServiceTest 2017-07-15 12:09:25 +02:00
Alejandro Celaya
963d26f59b Created UpdateTagAction 2017-07-15 12:04:12 +02:00
Alejandro Celaya
e07c464de8 Removed strict declarations 2017-07-15 09:15:45 +02:00
Alejandro Celaya
575509c45b Created CreateTagsActiontest 2017-07-15 09:12:07 +02:00
Alejandro Celaya
563e654b99 Created DeleteTagsActionTest 2017-07-15 09:10:09 +02:00
Alejandro Celaya
3e268e2012 Improved TagServiceTest 2017-07-15 09:05:02 +02:00
Alejandro Celaya
b2d9f2fc01 Added Create and Delete tag actions 2017-07-15 09:00:53 +02:00
Alejandro Celaya
6717102dd2 Updated tag actions namespace 2017-07-15 08:31:21 +02:00
Alejandro Celaya
1ba7fc81ac Created ListTagsCommand 2017-07-08 13:17:46 +02:00
Alejandro Celaya
caf4fa7fdd Documented list tags endpoint 2017-07-08 12:55:38 +02:00
Alejandro Celaya
5c7962966d Created ListTagsActionTest 2017-07-07 13:28:58 +02:00
Alejandro Celaya
95ec7e0afa Registered action to list tags 2017-07-07 13:12:45 +02:00
Alejandro Celaya
c37660f763 Created TagService 2017-07-07 12:49:41 +02:00
Alejandro Celaya
486ea10c3c Renamed EditTagsAction to EditShortcodeTagsAction 2017-07-07 11:45:20 +02:00
Alejandro Celaya
e0f18f8d1f Created InstallApplicationFactoryTest 2017-07-06 18:06:11 +02:00
Alejandro Celaya
a66f116d66 Created DatabaseConfigCustomizerPluginTest 2017-07-06 18:00:38 +02:00
Alejandro Celaya
dd099dc39c Removed declare strict types added by mistake 2017-07-06 17:49:05 +02:00
Alejandro Celaya
c05aeabdee Improved if statements reducing indentation 2017-07-06 17:38:16 +02:00
Alejandro Celaya
23922f6c7b Created UrlShortenerConfigCustomizerPluginTest 2017-07-06 17:28:32 +02:00
Alejandro Celaya
69a99949e1 Created LanguageConfigCustomizerPluginTest 2017-07-06 17:22:03 +02:00
Alejandro Celaya
d56cde72a3 Created ApplicationConfigCustomizerPluginTest 2017-07-06 17:12:32 +02:00
Alejandro Celaya
99ffff11c7 Created DefaultConfigCustomizerPluginFactoryTest 2017-07-06 13:43:36 +02:00
Alejandro Celaya
bb050cc1b6 Improved InstallCommandTest coverage 2017-07-06 13:38:15 +02:00
Alejandro Celaya
3547889ad5 Fixed InstallCommandTest 2017-07-06 10:04:35 +02:00
Alejandro Celaya
479e694478 Moved all configuration customization steps to individual plugins 2017-07-05 20:04:44 +02:00
Alejandro Celaya
2368b634e3 Moved command and app creation logic to a factory for install scripts 2017-07-05 18:12:03 +02:00
Alejandro Celaya
dcc09975a9 Abstracted filesystem manipulation in InstallCommand 2017-07-04 20:14:22 +02:00
Alejandro Celaya
102f5c4e12 Updated instalation script to import sqlte database file when importing the rets of the config 2017-07-04 20:01:42 +02:00
Alejandro Celaya
cc688fa3ce Implemented method to deserialize customizable config 2017-07-04 19:48:53 +02:00
Alejandro Celaya
e7f7cbcaac Improved installation command, reducing duplication and moving serialization logic to specific model 2017-07-03 20:46:35 +02:00
Alejandro Celaya
f9c56d7cb1 Added process to import previous configuration when updating shlink 2017-07-03 13:43:53 +02:00
Alejandro Celaya
1fe2e6f6bd Improved check on update and install commands 2017-07-03 13:17:44 +02:00
Alejandro Celaya
c3cc88f03e Fixed inspections 2017-07-03 13:11:45 +02:00
Alejandro Celaya
584e1f5643 Created common abstract command for update and install 2017-07-03 13:10:16 +02:00
Alejandro Celaya
04c479148a Updated build script so that generated zip contains one folder 2017-07-03 12:54:50 +02:00
Alejandro Celaya
c45cb7bacb Updated build script 2017-07-03 12:49:32 +02:00
Alejandro Celaya
17be221920 Added response examples to swagger docs 2017-04-16 10:45:52 +02:00
Alejandro Celaya
10da57572f Fixed date format returned by the API 2017-04-16 10:27:27 +02:00
Alejandro Celaya
52478ca60a Returned all allowed methods until fast route router is fixed 2017-04-14 13:27:41 +02:00
Alejandro Celaya
62b49dcb19 Set cross domain allow-methods header with the same value as the allow header 2017-04-14 12:55:59 +02:00
Alejandro Celaya
a365faef9c Removed requirement of OPTIONS on every route 2017-04-14 12:52:24 +02:00
Alejandro Celaya
5d2698e8a1 Created EmptyResponseImplicitOptionsMiddlewareFactoryTest 2017-04-13 09:52:17 +02:00
Alejandro Celaya
ec4a413a5b Removed options bypass in actions in favor of implicit options middleware 2017-04-13 09:45:31 +02:00
Alejandro Celaya
596d1ee797 Registered implicit options middleware 2017-04-13 09:43:11 +02:00
Alejandro Celaya
d117f82bcb Installed expressive tooling 2017-04-13 09:39:35 +02:00
Alejandro Celaya
18abe9d0f9 Merge branch 'develop' of github.com:shlinkio/shlink into develop 2017-04-13 09:34:52 +02:00
271 changed files with 5633 additions and 1209 deletions

24
.gitattributes vendored Normal file
View File

@@ -0,0 +1,24 @@
/data/infra export-ignore
/docs export-ignore
/module/CLI/test export-ignore
/module/CLI/test-resources export-ignore
/module/Common/test export-ignore
/module/Common/test-func export-ignore
/module/Core/test export-ignore
/module/Core/test-func export-ignore
/module/Rest/test export-ignore
.env.dist export-ignore
.gitattributes export-ignore
.gitignore export-ignore
.phpstorm.meta.php export-ignore
.scrutinizer.yml export-ignore
.travis.yml export-ignore
build.sh export-ignore
CHANGELOG.md export-ignore
docker-compose.override.yml.dist export-ignore
docker-compose.yml export-ignore
func_tests_bootstrap.php export-ignore
indocker export-ignore
phpcs.xml export-ignore
phpunit.xml.dist export-ignore
phpunit-func.xml export-ignore

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@ vendor/
.env
data/database.sqlite
docs/swagger-ui
docker-compose.override.yml

View File

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

View File

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

View File

@@ -6,11 +6,13 @@ branches:
- develop
php:
- 5.6
- 7
- 7.1
- 7.2
before_install: phpenv config-add .travis-php.ini
before_install:
- phpenv config-add data/infra/travis-php/memcached.ini
- phpenv config-add data/infra/travis-php/apcu.ini
before_script:
- composer self-update
@@ -21,6 +23,7 @@ script:
- composer check
after_script:
- vendor/bin/phpcov merge build --clover build/clover.xml
- wget https://scrutinizer-ci.com/ocular.phar
- php ocular.phar code-coverage:upload --format=php-clover build/clover.xml

View File

@@ -1,5 +1,61 @@
## CHANGELOG
### 1.6.2
**Bugs**
* [109: Fix installation error due to typo in latest migration](https://github.com/shlinkio/shlink/issues/109)
### 1.6.1
**Tasks**
* [110: Create gitattributes file to define files to be excluded from distributable package](https://github.com/shlinkio/shlink/issues/110)
### 1.6.0
**Features**
* [44: Consider allowing to set custom slugs instead of generating a short code](https://github.com/shlinkio/shlink/issues/44)
* [47: Allow to limit short codes availability by date range](https://github.com/shlinkio/shlink/issues/47)
* [48: Allow to limit the number of visits to a short code](https://github.com/shlinkio/shlink/issues/48)
* [105: Added option to enable/disable URL validation by response status code.](https://github.com/shlinkio/shlink/pull/105)
**Enhancements:**
* [27: Add repository functional tests with dbunit](https://github.com/shlinkio/shlink/issues/27)
* [86: Drop support for PHP 5](https://github.com/shlinkio/shlink/issues/86)
* [101: Make actions just capture very specific exceptions, and let the ErrorHandler catch any other exception](https://github.com/shlinkio/shlink/issues/101)
* [104: Use different templates for requested-short-code-does-not-exist and route-could-not-be-match](https://github.com/shlinkio/shlink/issues/104)
**Tasks**
* [99: Replace AnnotatedFactory by ConfigAbstractFactory](https://github.com/shlinkio/shlink/issues/99)
* [100: Replace twig by plates](https://github.com/shlinkio/shlink/issues/100)
* [102: Improve coding standards strictness](https://github.com/shlinkio/shlink/issues/102)
**Bugs**
* [103: Make NotFoundDelegate return proper content types based on accepted content](https://github.com/shlinkio/shlink/issues/103)
### 1.5.0
**Enhancements:**
* [95: Add tags CRUD to CLI](https://github.com/shlinkio/shlink/issues/95)
* [59: Add tags CRUD to REST](https://github.com/shlinkio/shlink/issues/59)
* [66: Allow to import certain information from older app directory when updating](https://github.com/shlinkio/shlink/issues/66)
**Tasks**
* [96: Add namespace to functions](https://github.com/shlinkio/shlink/issues/96)
* [76: Add response examples to swagger docs](https://github.com/shlinkio/shlink/issues/76)
* [93: Improve cross domain management by using the ImplicitOptionsMiddleware](https://github.com/shlinkio/shlink/issues/93)
**Bugs**
* [92: Fix formatted dates, using an ISO compliant format](https://github.com/shlinkio/shlink/issues/92)
### 1.4.0
**Enhancements:**

View File

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

View File

@@ -1,14 +1,30 @@
#!/usr/bin/env php
<?php
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizerPlugin;
use Symfony\Component\Console\Application;
use Zend\Config\Writer\PhpArray;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Filesystem\Filesystem;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ServiceManager\ServiceManager;
chdir(dirname(__DIR__));
require __DIR__ . '/../vendor/autoload.php';
$app = new Application();
$app->add(new InstallCommand(new PhpArray()));
$app->setDefaultCommand('shlink:install');
$app->run();
$container = new ServiceManager([
'factories' => [
Application::class => InstallApplicationFactory::class,
Filesystem::class => InvokableFactory::class,
QuestionHelper::class => InvokableFactory::class,
],
'services' => [
'config' => [
ConfigAbstractFactory::class => [
DatabaseConfigCustomizerPlugin::class => [QuestionHelper::class, Filesystem::class]
],
],
],
]);
$container->build(Application::class)->run();

View File

@@ -1,14 +1,30 @@
#!/usr/bin/env php
<?php
use Shlinkio\Shlink\CLI\Command\Install\UpdateCommand;
use Shlinkio\Shlink\CLI\Factory\InstallApplicationFactory;
use Shlinkio\Shlink\CLI\Install\Plugin\DatabaseConfigCustomizerPlugin;
use Symfony\Component\Console\Application;
use Zend\Config\Writer\PhpArray;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Filesystem\Filesystem;
use Zend\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Zend\ServiceManager\Factory\InvokableFactory;
use Zend\ServiceManager\ServiceManager;
chdir(dirname(__DIR__));
require __DIR__ . '/../vendor/autoload.php';
$app = new Application();
$app->add(new UpdateCommand(new PhpArray()));
$app->setDefaultCommand('shlink:install');
$app->run();
$container = new ServiceManager([
'factories' => [
Application::class => InstallApplicationFactory::class,
Filesystem::class => InvokableFactory::class,
QuestionHelper::class => InvokableFactory::class,
],
'services' => [
'config' => [
ConfigAbstractFactory::class => [
DatabaseConfigCustomizerPlugin::class => [QuestionHelper::class, Filesystem::class]
],
],
],
]);
$container->build(Application::class, ['isUpdate' => true])->run();

View File

@@ -8,7 +8,7 @@ if [ "$#" -ne 1 ]; then
fi
version=$1
builtcontent=$(readlink -f '../shlink_build_tmp')
builtcontent=$(readlink -f "../shlink_${version}_dist")
projectdir=$(pwd)
# Copy project content to temp dir
@@ -20,8 +20,8 @@ cp -R "${projectdir}"/* "${builtcontent}"
cd "${builtcontent}"
# Install dependencies
rm -r vendor
rm composer.lock
rm -rf vendor
rm -f composer.lock
composer self-update
composer install --no-dev --optimize-autoloader --no-progress --no-interaction
@@ -31,10 +31,12 @@ rm build.sh
rm CHANGELOG.md
rm composer.*
rm LICENSE
rm indocker
rm docker-compose.yml
rm php*
rm README.md
rm -r build
rm -f data/database.sqlite
rm -rf build
rm -ff data/database.sqlite
rm -rf data/infra
rm -rf data/{cache,log,proxies}/{*,.gitignore}
rm -rf config/params/{*,.gitignore}
@@ -42,5 +44,5 @@ 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 .
zip -ry "${projectdir}"/build/shlink_${version}_dist.zip "../shlink_${version}_dist"
rm -rf "${builtcontent}"

View File

@@ -12,37 +12,48 @@
}
],
"require": {
"php": "^5.6 || ^7.0",
"php": "^7.0",
"acelaya/ze-content-based-error-handler": "^2.0",
"cocur/slugify": "^3.0",
"doctrine/annotations": "^1.4 <1.5",
"doctrine/cache": "^1.6 <1.7",
"doctrine/collections": "^1.4 <1.5",
"doctrine/common": "^2.7 <2.8",
"doctrine/dbal": "^2.5 <2.6",
"doctrine/migrations": "^1.4",
"doctrine/orm": "^2.5 <2.6",
"endroid/qrcode": "^1.7",
"firebase/php-jwt": "^4.0",
"guzzlehttp/guzzle": "^6.2",
"http-interop/http-middleware": "^0.4.1",
"mikehaertl/phpwkhtmltopdf": "^2.2",
"monolog/monolog": "^1.21",
"roave/security-advisories": "dev-master",
"symfony/console": "^3.0",
"symfony/filesystem": "^3.0",
"symfony/process": "^3.0",
"theorchard/monolog-cascade": "^0.4",
"zendframework/zend-config": "^3.0",
"zendframework/zend-config-aggregator": "^1.0",
"zendframework/zend-expressive": "^2.0",
"zendframework/zend-expressive-fastroute": "^2.0",
"zendframework/zend-expressive-twigrenderer": "^1.4",
"zendframework/zend-stdlib": "^3.0",
"zendframework/zend-servicemanager": "^3.0",
"zendframework/zend-paginator": "^2.6",
"zendframework/zend-config": "^3.0",
"zendframework/zend-expressive-helpers": "^4.2",
"zendframework/zend-expressive-platesrenderer": "^1.3",
"zendframework/zend-i18n": "^2.7",
"zendframework/zend-config-aggregator": "^0.1",
"acelaya/zsm-annotated-services": "^1.0",
"acelaya/ze-content-based-error-handler": "^2.0",
"doctrine/orm": "^2.5",
"guzzlehttp/guzzle": "^6.2",
"symfony/console": "^3.0",
"symfony/process": "^3.0",
"symfony/filesystem": "^3.0",
"firebase/php-jwt": "^4.0",
"monolog/monolog": "^1.21",
"theorchard/monolog-cascade": "^0.4",
"endroid/qrcode": "^1.7",
"mikehaertl/phpwkhtmltopdf": "^2.2",
"doctrine/migrations": "^1.4"
"zendframework/zend-paginator": "^2.6",
"zendframework/zend-servicemanager": "^3.0",
"zendframework/zend-stdlib": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^5.7 || ^6.0",
"squizlabs/php_codesniffer": "^2.3",
"roave/security-advisories": "dev-master",
"filp/whoops": "^2.0",
"phpunit/dbunit": "^3.0",
"phpunit/phpcov": "^4.0",
"phpunit/phpunit": "^6.0",
"slevomat/coding-standard": "^4.0",
"squizlabs/php_codesniffer": "^3.1",
"symfony/var-dumper": "^3.0",
"vlucas/phpdotenv": "^2.2"
"vlucas/phpdotenv": "^2.2",
"zendframework/zend-expressive-tooling": "^0.4"
},
"autoload": {
"psr-4": {
@@ -59,22 +70,39 @@
"psr-4": {
"ShlinkioTest\\Shlink\\CLI\\": "module/CLI/test",
"ShlinkioTest\\Shlink\\Rest\\": "module/Rest/test",
"ShlinkioTest\\Shlink\\Core\\": "module/Core/test",
"ShlinkioTest\\Shlink\\Common\\": "module/Common/test"
"ShlinkioTest\\Shlink\\Core\\": [
"module/Core/test",
"module/Core/test-func"
],
"ShlinkioTest\\Shlink\\Common\\": [
"module/Common/test",
"module/Common/test-func"
]
}
},
"scripts": {
"check": [
"@cs",
"@test"
"@test",
"@func-test"
],
"cs": "phpcs",
"cs-fix": "phpcbf",
"serve": "php -S 0.0.0.0:8000 -t public/",
"test": "phpunit --coverage-clover build/clover.xml",
"pretty-test": "phpunit --coverage-html build/coverage"
"test": "phpunit --coverage-php build/coverage-unit.cov",
"pretty-test": "phpunit --coverage-html build/coverage",
"func-test": "phpunit -c phpunit-func.xml --coverage-php build/coverage-func.cov",
"complete-pretty-test": [
"@test",
"@func-test",
"phpcov merge build --html build/html"
]
},
"config": {
"process-timeout": 0
"process-timeout": 0,
"sort-packages": true,
"platform": {
"php": "7.0"
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
use Monolog\Handler\RotatingFileHandler;
use Monolog\Logger;

View File

@@ -1,10 +1,12 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CheckAuthenticationMiddleware;
use Shlinkio\Shlink\Rest\Middleware\CrossDomainMiddleware;
use Shlinkio\Shlink\Rest\Middleware\PathVersionMiddleware;
use Zend\Expressive\Container\ApplicationFactory;
use Zend\Expressive;
use Zend\Stratigility\Middleware\ErrorHandler;
return [
@@ -27,7 +29,7 @@ return [
'routing' => [
'middleware' => [
ApplicationFactory::ROUTING_MIDDLEWARE,
Expressive\Application::ROUTING_MIDDLEWARE,
],
'priority' => 10,
],
@@ -36,6 +38,7 @@ return [
'path' => '/rest',
'middleware' => [
CrossDomainMiddleware::class,
Expressive\Middleware\ImplicitOptionsMiddleware::class,
BodyParserMiddleware::class,
CheckAuthenticationMiddleware::class,
],
@@ -44,7 +47,7 @@ return [
'post-routing' => [
'middleware' => [
ApplicationFactory::DISPATCH_MIDDLEWARE,
Expressive\Application::DISPATCH_MIDDLEWARE,
],
'priority' => 1,
],

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
return [
'phpwkhtmltopdf' => [

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
return [
'preview_generation' => [

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
use Zend\Expressive\Router\FastRouteRouter;
return [

View File

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

View File

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

View File

@@ -1,5 +1,8 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\Core\Service\UrlShortener;
use function Shlinkio\Shlink\Common\env;
return [
@@ -9,6 +12,7 @@ return [
'hostname' => env('SHORTENED_URL_HOSTNAME'),
],
'shortcode_chars' => env('SHORTCODE_CHARS', UrlShortener::DEFAULT_CHARS),
'validate_url' => true,
],
];

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
return [

View File

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

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
use Acelaya\ExpressiveErrorHandler;
use Shlinkio\Shlink\CLI;
use Shlinkio\Shlink\Common;

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
use Dotenv\Dotenv;
use Zend\ServiceManager\ServiceManager;
@@ -9,7 +11,7 @@ require 'vendor/autoload.php';
// If the Dotenv class exists, load env vars and enable errors
if (class_exists(Dotenv::class)) {
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('display_errors', '1');
$dotenv = new Dotenv(__DIR__ . '/..');
$dotenv->load();
}

View File

@@ -26,7 +26,7 @@ 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
ADD https://github.com/phpredis/phpredis/archive/3.1.4.tar.gz /tmp/phpredis.tar.gz
RUN mkdir -p /usr/src/php/ext/redis\
&& tar xf /tmp/phpredis.tar.gz -C /usr/src/php/ext/redis --strip-components=1
# configure and install
@@ -85,3 +85,6 @@ RUN rm /tmp/xdebug.tar.gz
RUN php -r "readfile('https://getcomposer.org/installer');" | php
RUN chmod +x composer.phar
RUN mv composer.phar /usr/local/bin/composer
# Make home directory writable by anyone
RUN chmod 777 /home

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);
namespace ShlinkMigrations;

View File

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

View File

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

View File

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

View File

@@ -25,6 +25,11 @@
"description": "The authentication token that needs to be sent in the Authorization header"
}
}
},
"examples": {
"application/json": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ"
}
}
},
"400": {

View File

@@ -68,6 +68,44 @@
}
}
}
},
"examples": {
"application/json": {
"shortUrls": {
"data": [
{
"shortCode": "12C18",
"originalUrl": "https://store.steampowered.com",
"dateCreated": "2016-08-21T20:34:16+02:00",
"visitsCount": 328,
"tags": [
"games",
"tech"
]
},
{
"shortCode": "12Kb3",
"originalUrl": "https://shlink.io",
"dateCreated": "2016-05-01T20:34:16+02:00",
"visitsCount": 1029,
"tags": [
"shlink"
]
},
{
"shortCode": "123bA",
"originalUrl": "https://www.google.com",
"dateCreated": "2015-10-01T20:34:16+02:00",
"visitsCount": 25,
"tags": []
}
],
"pagination": {
"currentPage": 5,
"pagesCount": 12
}
}
}
}
},
"500": {

View File

@@ -28,6 +28,11 @@
"description": "The original long URL behind the short code."
}
}
},
"examples": {
"application/json": {
"longUrl": "https://shlink.io"
}
}
},
"400": {

View File

@@ -41,6 +41,14 @@
}
}
}
},
"examples": {
"application/json": {
"tags": [
"games",
"tech"
]
}
}
},
"400": {

View File

@@ -36,6 +36,32 @@
}
}
}
},
"examples": {
"application/json": {
"visits": {
"data": [
{
"referer": "https://twitter.com",
"date": "2015-08-20T05:05:03+04:00",
"remoteAddr": "10.20.30.40",
"userAgent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0 Mozilla/5.0 (Macintosh; Intel Mac OS X x.y; rv:42.0) Gecko/20100101 Firefox/42.0"
},
{
"referer": "https://t.co",
"date": "2015-08-20T05:05:03+04:00",
"remoteAddr": "11.22.33.44",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36"
},
{
"referer": null,
"date": "2015-08-20T05:05:03+04:00",
"remoteAddr": "110.220.5.6",
"userAgent": "some_web_crawler/1.4"
}
]
}
}
}
},
"404": {

View File

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

View File

@@ -3,7 +3,7 @@
"info": {
"title": "Shlink",
"description": "Shlink, the self-hosted URL shortener",
"version": "1.2.0"
"version": "1.0"
},
"schemes": [
"http",
@@ -22,17 +22,23 @@
"/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"
},
"/v1/tags": {
"$ref": "paths/v1_tags.json"
},
"/v1/short-codes/{shortCode}/visits": {
"$ref": "paths/v1_short-codes_{shortCode}_visits.json"
}
}
}

33
func_tests_bootstrap.php Normal file
View File

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

View File

@@ -1,10 +1,13 @@
<?php
declare(strict_types=1);
use Shlinkio\Shlink\CLI\Command;
use Shlinkio\Shlink\Common;
return [
'cli' => [
'locale' => env('CLI_LOCALE', 'en'),
'locale' => Common\env('CLI_LOCALE', 'en'),
'commands' => [
Command\Shortcode\GenerateShortcodeCommand::class,
Command\Shortcode\ResolveUrlCommand::class,
@@ -17,7 +20,11 @@ return [
Command\Api\GenerateKeyCommand::class,
Command\Api\DisableKeyCommand::class,
Command\Api\ListKeysCommand::class,
]
Command\Tag\ListTagsCommand::class,
Command\Tag\CreateTagCommand::class,
Command\Tag\RenameTagCommand::class,
Command\Tag\DeleteTagsCommand::class,
],
],
];

View File

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

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
return [
'translator' => [

Binary file not shown.

View File

@@ -1,15 +1,15 @@
msgid ""
msgstr ""
"Project-Id-Version: Shlink 1.0\n"
"POT-Creation-Date: 2016-10-22 23:12+0200\n"
"PO-Revision-Date: 2016-10-22 23:13+0200\n"
"POT-Creation-Date: 2017-10-21 20:17+0200\n"
"PO-Revision-Date: 2017-10-21 20:19+0200\n"
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
"Language-Team: \n"
"Language: es_ES\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 1.8.7.1\n"
"X-Generator: Poedit 2.0.1\n"
"X-Poedit-Basepath: ..\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Poedit-SourceCharset: UTF-8\n"
@@ -92,7 +92,7 @@ msgid "Processing URL %s..."
msgstr "Procesando URL %s..."
msgid " <info>Success!</info>"
msgstr "<info>¡Correcto!</info>"
msgstr " <info>¡Correcto!</info>"
msgid "Error"
msgstr "Error"
@@ -107,6 +107,24 @@ msgstr "La URL larga a procesar"
msgid "Tags to apply to the new short URL"
msgstr "Etiquetas a aplicar a la nueva URL acortada"
msgid ""
"The date from which this short URL will be valid. If someone tries to access "
"it before this date, it will not be found."
msgstr ""
"La fecha desde la cual será válida esta URL acortada. Si alguien intenta "
"acceder a ella antes de esta fecha, no será encontrada."
msgid ""
"The date until which this short URL will be valid. If someone tries to "
"access it after this date, it will not be found."
msgstr ""
"La fecha hasta la cual será válida está URL acortada. Si alguien intenta "
"acceder a ella después de esta fecha, no será encontrada."
msgid "If provided, this slug will be used instead of generating a short code"
msgstr ""
"Si se proporciona, este slug será usado en vez de generar un código corto"
msgid "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?"
@@ -123,6 +141,14 @@ msgstr "URL generada:"
msgid "Provided URL \"%s\" is invalid. Try with a different one."
msgstr "La URL proporcionada \"%s\" e inválida. Prueba con una diferente."
#, php-format
msgid ""
"Provided slug \"%s\" is already in use by another URL. Try with a different "
"one."
msgstr ""
"El slug proporcionado \"%s\" ya está siendo usado para otra URL. Prueba con "
"uno diferente."
msgid "Returns the detailed visits information for provided short code"
msgstr ""
"Devuelve la información detallada de visitas para el código corto "
@@ -141,7 +167,7 @@ msgstr ""
"Permite filtrar las visitas, devolviendo sólo aquellas más nuevas que endDate"
msgid "A short code was not provided. Which short code do you want to use?:"
msgstr "No se prporcionó un código corto. ¿Qué código corto deseas usar?"
msgstr "No se prporcionó un código corto. ¿Qué código corto deseas usar?:"
msgid "Referer"
msgstr "Origen"
@@ -210,7 +236,7 @@ msgstr "El código corto a convertir"
msgid "A short code was not provided. Which short code do you want to parse?:"
msgstr ""
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?"
"No se proporcionó un código corto. ¿Qué código corto quieres convertir?:"
#, php-format
msgid "No URL found for short code \"%s\""
@@ -223,6 +249,57 @@ msgstr "URL larga:"
msgid "Provided short code \"%s\" has an invalid format."
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
#, php-format
msgid "Provided short code \"%s\" could not be found."
msgstr "El código corto proporcionado \"%s\" no ha podido ser encontrado."
msgid "Creates one or more tags."
msgstr "Crea una o más etiquetas."
msgid "The name of the tags to create"
msgstr "El nombre de las etiquetas a crear"
msgid "You have to provide at least one tag name"
msgstr "Debes proporcionar al menos un nombre de etiqueta"
msgid "Created tags"
msgstr "Etiquetas creadas"
msgid "Deletes one or more tags."
msgstr "Elimina una o más etiquetas."
msgid "The name of the tags to delete"
msgstr "El nombre de las etiquetas a eliminar"
msgid "Deleted tags"
msgstr "Etiquetas eliminadas"
msgid "Lists existing tags."
msgstr "Lista las etiquetas existentes."
#, fuzzy
msgid "Name"
msgstr "Nombre"
msgid "No tags yet"
msgstr "Aún no hay etiquetas"
msgid "Renames one existing tag."
msgstr "Renombra una etiqueta existente."
msgid "Current name of the tag."
msgstr "Nombre actual de la etiqueta."
msgid "New name of the tag."
msgstr "Nuevo nombre de la etiqueta."
msgid "Tag properly renamed."
msgstr "Etiqueta correctamente renombrada."
#, php-format
msgid "A tag with name \"%s\" was not found"
msgstr "Una etiqueta con nombre \"%s\" no ha sido encontrada"
msgid "Processes visits where location is not set yet"
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"

View File

@@ -1,8 +1,8 @@
<?php
declare(strict_types=1);
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;
@@ -21,18 +21,11 @@ class DisableKeyCommand extends Command
*/
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);
parent::__construct();
}
public function configure()

View File

@@ -1,8 +1,8 @@
<?php
declare(strict_types=1);
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;
@@ -21,18 +21,11 @@ class GenerateKeyCommand extends Command
*/
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);
parent::__construct();
}
public function configure()

View File

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
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;
@@ -23,18 +23,11 @@ class ListKeysCommand extends Command
*/
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);
parent::__construct();
}
public function configure()
@@ -84,7 +77,7 @@ class ListKeysCommand extends Command
$rowData[] = $this->{$formatMethod}($this->getEnabledSymbol($row));
}
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ISO8601) : '-';
$rowData[] = isset($expiration) ? $expiration->format(\DateTime::ATOM) : '-';
$table->addRow($rowData);
}

View File

@@ -1,7 +1,8 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Config;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -15,16 +16,10 @@ class GenerateCharsetCommand extends Command
*/
private $translator;
/**
* GenerateCharsetCommand constructor.
* @param TranslatorInterface $translator
*
* @Inject({"translator"})
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
parent::__construct(null);
parent::__construct();
}
public function configure()

View File

@@ -1,7 +1,8 @@
<?php
declare(strict_types=1);
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;
@@ -17,16 +18,10 @@ class GenerateSecretCommand extends Command
*/
private $translator;
/**
* GenerateCharsetCommand constructor.
* @param TranslatorInterface $translator
*
* @Inject({"translator"})
*/
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
parent::__construct(null);
parent::__construct();
}
public function configure()

View File

@@ -1,27 +1,27 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Install;
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\CLI\Install\ConfigCustomizerPluginManagerInterface;
use Shlinkio\Shlink\CLI\Install\Plugin;
use Shlinkio\Shlink\CLI\Model\CustomizableAppConfig;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\LogicException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Zend\Config\Writer\WriterInterface;
class InstallCommand extends Command
{
use StringUtilsTrait;
const DATABASE_DRIVERS = [
'MySQL' => 'pdo_mysql',
'PostgreSQL' => 'pdo_pgsql',
'SQLite' => 'pdo_sqlite',
];
const SUPPORTED_LANGUAGES = ['en', 'es'];
const GENERATED_CONFIG_PATH = 'config/params/generated_config.php';
/**
* @var InputInterface
@@ -43,22 +43,44 @@ class InstallCommand extends Command
* @var WriterInterface
*/
private $configWriter;
/**
* @var Filesystem
*/
private $filesystem;
/**
* @var ConfigCustomizerPluginManagerInterface
*/
private $configCustomizers;
/**
* @var bool
*/
private $isUpdate;
/**
* InstallCommand constructor.
* @param WriterInterface $configWriter
* @param callable|null $databaseCreationLogic
* @param Filesystem $filesystem
* @param bool $isUpdate
* @throws LogicException
*/
public function __construct(WriterInterface $configWriter)
{
parent::__construct(null);
public function __construct(
WriterInterface $configWriter,
Filesystem $filesystem,
ConfigCustomizerPluginManagerInterface $configCustomizers,
$isUpdate = false
) {
parent::__construct();
$this->configWriter = $configWriter;
$this->isUpdate = $isUpdate;
$this->filesystem = $filesystem;
$this->configCustomizers = $configCustomizers;
}
public function configure()
{
$this->setName('shlink:install')
->setDescription('Installs Shlink');
$this
->setName('shlink:install')
->setDescription('Installs or updates Shlink');
}
public function execute(InputInterface $input, OutputInterface $output)
@@ -67,40 +89,58 @@ class InstallCommand extends Command
$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.',
'This will guide you through the installation process.',
]);
// Check if a cached config file exists and drop it if so
if (file_exists('data/cache/app_config.php')) {
if ($this->filesystem->exists('data/cache/app_config.php')) {
$output->write('Deleting old cached config...');
if (unlink('data/cache/app_config.php')) {
try {
$this->filesystem->remove('data/cache/app_config.php');
$output->writeln(' <info>Success</info>');
} else {
} catch (IOException $e) {
$output->writeln(
' <error>Failed!</error> You will have to manually delete the data/cache/app_config.php file to get'
. ' new config applied.'
);
if ($output->isVerbose()) {
$this->getApplication()->renderException($e, $output);
}
return;
}
}
// If running update command, ask the user to import previous config
$config = $this->isUpdate ? $this->importConfig() : new CustomizableAppConfig();
// Ask for custom config params
$params['DATABASE'] = $this->askDatabase();
$params['URL_SHORTENER'] = $this->askUrlShortener();
$params['LANGUAGE'] = $this->askLanguage();
$params['APP'] = $this->askApplication();
foreach ([
Plugin\DatabaseConfigCustomizerPlugin::class,
Plugin\UrlShortenerConfigCustomizerPlugin::class,
Plugin\LanguageConfigCustomizerPlugin::class,
Plugin\ApplicationConfigCustomizerPlugin::class,
] as $pluginName) {
/** @var Plugin\ConfigCustomizerPluginInterface $configCustomizer */
$configCustomizer = $this->configCustomizers->get($pluginName);
$configCustomizer->process($input, $output, $config);
}
// Generate config params files
$config = $this->buildAppConfig($params);
$this->configWriter->toFile('config/params/generated_config.php', $config, false);
$this->configWriter->toFile(self::GENERATED_CONFIG_PATH, $config->getArrayCopy(), false);
$output->writeln(['<info>Custom configuration properly generated!</info>', '']);
// Generate database
if (! $this->createDatabase()) {
return;
// If current command is not update, generate database
if (! $this->isUpdate) {
$this->output->writeln('Initializing database...');
if (! $this->runCommand(
'php vendor/bin/doctrine.php orm:schema-tool:create',
'Error generating database.'
)) {
return;
}
}
// Run database migrations
@@ -116,105 +156,47 @@ class InstallCommand extends Command
}
}
protected function askDatabase()
/**
* @return CustomizableAppConfig
* @throws RuntimeException
*/
private function importConfig()
{
$params = [];
$this->printTitle('DATABASE');
$config = new CustomizableAppConfig();
// 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
// Ask the user if he/she wants to import an older configuration
$importConfig = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
'<question>Do you want to import previous configuration? (Y/n):</question> '
));
$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']));
if (! $importConfig) {
return $config;
}
return $params;
}
// Ask the user for the older shlink path
$keepAsking = true;
do {
$config->setImportedInstallationPath($this->ask(
'Previous shlink installation path from which to import config'
));
$configFile = $config->getImportedInstallationPath() . '/' . self::GENERATED_CONFIG_PATH;
$configExists = $this->filesystem->exists($configFile);
protected function getDefaultDbPort($driver)
{
return $driver === 'pdo_mysql' ? '3306' : '5432';
}
if (! $configExists) {
$keepAsking = $this->questionHelper->ask($this->input, $this->output, new ConfirmationQuestion(
'Provided path does not seem to be a valid shlink root path. '
. '<question>Do you want to try another path? (Y/n):</question> '
));
}
} while (! $configExists && $keepAsking);
protected function askUrlShortener()
{
$this->printTitle('URL SHORTENER');
// If after some retries the user has chosen not to test another path, return
if (! $configExists) {
return $config;
}
// 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>',
]);
// Read the config file
$config->exchangeArray(include $configFile);
return $config;
}
/**
@@ -222,10 +204,11 @@ class InstallCommand extends Command
* @param string|null $default
* @param bool $allowEmpty
* @return string
* @throws RuntimeException
*/
protected function ask($text, $default = null, $allowEmpty = false)
private function ask($text, $default = null, $allowEmpty = false)
{
if (isset($default)) {
if ($default !== null) {
$text .= ' (defaults to ' . $default . ')';
}
do {
@@ -236,87 +219,31 @@ class InstallCommand extends Command
if (empty($value) && ! $allowEmpty) {
$this->output->writeln('<error>Value can\'t be empty</error>');
}
} while (empty($value) && empty($default) && ! $allowEmpty);
} while (empty($value) && $default === null && ! $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)
private 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.'
);
}
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

@@ -1,12 +0,0 @@
<?php
namespace Shlinkio\Shlink\CLI\Command\Install;
use Zend\Config\Writer\WriterInterface;
class UpdateCommand extends InstallCommand
{
public function createDatabase()
{
return true;
}
}

View File

@@ -1,11 +1,10 @@
<?php
declare(strict_types=1);
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;
@@ -27,14 +26,6 @@ class GeneratePreviewCommand extends Command
*/
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,

View File

@@ -1,9 +1,10 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
@@ -30,14 +31,6 @@ class GenerateShortcodeCommand extends Command
*/
private $translator;
/**
* GenerateShortcodeCommand constructor.
* @param UrlShortenerInterface $urlShortener
* @param TranslatorInterface $translator
* @param array $domainConfig
*
* @Inject({UrlShortener::class, "translator", "config.url_shortener.domain"})
*/
public function __construct(
UrlShortenerInterface $urlShortener,
TranslatorInterface $translator,
@@ -59,9 +52,23 @@ class GenerateShortcodeCommand extends Command
->addOption(
'tags',
't',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
$this->translator->translate('Tags to apply to the new short URL')
);
)
->addOption('validSince', 's', InputOption::VALUE_REQUIRED, $this->translator->translate(
'The date from which this short URL will be valid. '
. 'If someone tries to access it before this date, it will not be found.'
))
->addOption('validUntil', 'u', InputOption::VALUE_REQUIRED, $this->translator->translate(
'The date until which this short URL will be valid. '
. 'If someone tries to access it after this date, it will not be found.'
))
->addOption('customSlug', 'c', InputOption::VALUE_REQUIRED, $this->translator->translate(
'If provided, this slug will be used instead of generating a short code'
))
->addOption('maxVisits', 'm', InputOption::VALUE_REQUIRED, $this->translator->translate(
'This will limit the number of visits for this short URL.'
));
}
public function interact(InputInterface $input, OutputInterface $output)
@@ -94,6 +101,8 @@ class GenerateShortcodeCommand extends Command
$processedTags = array_merge($processedTags, $explodedTags);
}
$tags = $processedTags;
$customSlug = $input->getOption('customSlug');
$maxVisits = $input->getOption('maxVisits');
try {
if (! isset($longUrl)) {
@@ -101,7 +110,14 @@ class GenerateShortcodeCommand extends Command
return;
}
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl), $tags);
$shortCode = $this->urlShortener->urlToShortCode(
new Uri($longUrl),
$tags,
$this->getOptionalDate($input, 'validSince'),
$this->getOptionalDate($input, 'validUntil'),
$customSlug,
$maxVisits !== null ? (int) $maxVisits : null
);
$shortUrl = (new Uri())->withPath($shortCode)
->withScheme($this->domainConfig['schema'])
->withHost($this->domainConfig['hostname']);
@@ -117,6 +133,19 @@ class GenerateShortcodeCommand extends Command
) . '</error>',
$longUrl
));
} catch (NonUniqueSlugException $e) {
$output->writeln(sprintf(
'<error>' . $this->translator->translate(
'Provided slug "%s" is already in use by another URL. Try with a different one.'
) . '</error>',
$customSlug
));
}
}
private function getOptionalDate(InputInterface $input, string $fieldName)
{
$since = $input->getOption($fieldName);
return $since !== null ? new \DateTime($since) : null;
}
}

View File

@@ -1,9 +1,9 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Service\VisitsTracker;
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
@@ -26,13 +26,6 @@ class GetVisitsCommand extends Command
*/
private $translator;
/**
* GetVisitsCommand constructor.
* @param VisitsTrackerInterface $visitsTracker
* @param TranslatorInterface $translator
*
* @Inject({VisitsTracker::class, "translator"})
*/
public function __construct(VisitsTrackerInterface $visitsTracker, TranslatorInterface $translator)
{
$this->visitsTracker = $visitsTracker;

View File

@@ -1,10 +1,10 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Core\Service\ShortUrlService;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
@@ -28,13 +28,6 @@ class ListShortcodesCommand extends Command
*/
private $translator;
/**
* ListShortcodesCommand constructor.
* @param ShortUrlServiceInterface $shortUrlService
* @param TranslatorInterface $translator
*
* @Inject({ShortUrlService::class, "translator"})
*/
public function __construct(ShortUrlServiceInterface $shortUrlService, TranslatorInterface $translator)
{
$this->shortUrlService = $shortUrlService;
@@ -88,12 +81,11 @@ class ListShortcodesCommand extends Command
public function execute(InputInterface $input, OutputInterface $output)
{
$page = intval($input->getOption('page'));
$page = (int) $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');

View File

@@ -1,9 +1,10 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
@@ -24,13 +25,6 @@ class ResolveUrlCommand extends Command
*/
private $translator;
/**
* ResolveUrlCommand constructor.
* @param UrlShortenerInterface $urlShortener
* @param TranslatorInterface $translator
*
* @Inject({UrlShortener::class, "translator"})
*/
public function __construct(UrlShortenerInterface $urlShortener, TranslatorInterface $translator)
{
$this->urlShortener = $urlShortener;
@@ -88,6 +82,10 @@ class ResolveUrlCommand extends Command
$output->writeln(sprintf('<error>' . $this->translator->translate(
'Provided short code "%s" has an invalid format.'
) . '</error>', $shortCode));
} catch (EntityDoesNotExistException $e) {
$output->writeln(sprintf('<error>' . $this->translator->translate(
'Provided short code "%s" could not be found.'
) . '</error>', $shortCode));
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Zend\I18n\Translator\TranslatorInterface;
class CreateTagCommand extends Command
{
/**
* @var TagServiceInterface
*/
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
}
protected function configure()
{
$this
->setName('tag:create')
->setDescription($this->translator->translate('Creates one or more tags.'))
->addOption(
'name',
't',
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
$this->translator->translate('The name of the tags to create')
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$tagNames = $input->getOption('name');
if (empty($tagNames)) {
$output->writeln(sprintf(
'<comment>%s</comment>',
$this->translator->translate('You have to provide at least one tag name')
));
return;
}
$this->tagService->createTags($tagNames);
$output->writeln($this->translator->translate('Created tags') . sprintf(': ["<info>%s</info>"]', implode(
'</info>", "<info>',
$tagNames
)));
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Zend\I18n\Translator\TranslatorInterface;
class DeleteTagsCommand extends Command
{
/**
* @var TagServiceInterface
*/
private $tagService;
/**
* @var TranslatorInterface
*/
private $translator;
public function __construct(TagServiceInterface $tagService, TranslatorInterface $translator)
{
$this->tagService = $tagService;
$this->translator = $translator;
parent::__construct();
}
protected function configure()
{
$this
->setName('tag:delete')
->setDescription($this->translator->translate('Deletes one or more tags.'))
->addOption(
'name',
't',
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
$this->translator->translate('The name of the tags to delete')
);
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$tagNames = $input->getOption('name');
if (empty($tagNames)) {
$output->writeln(sprintf(
'<comment>%s</comment>',
$this->translator->translate('You have to provide at least one tag name')
));
return;
}
$this->tagService->deleteTags($tagNames);
$output->writeln($this->translator->translate('Deleted tags') . sprintf(': ["<info>%s</info>"]', implode(
'</info>", "<info>',
$tagNames
)));
}
}

View File

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

View File

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

View File

@@ -1,12 +1,11 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
use Shlinkio\Shlink\Common\Service\IpLocationResolverInterface;
use Shlinkio\Shlink\Core\Entity\VisitLocation;
use Shlinkio\Shlink\Core\Service\VisitService;
use Shlinkio\Shlink\Core\Service\VisitServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
@@ -30,14 +29,6 @@ class ProcessVisitsCommand extends Command
*/
private $translator;
/**
* ProcessVisitsCommand constructor.
* @param VisitServiceInterface $visitService
* @param IpLocationResolverInterface $ipLocationResolver
* @param TranslatorInterface $translator
*
* @Inject({VisitService::class, IpLocationResolver::class, "translator"})
*/
public function __construct(
VisitServiceInterface $visitService,
IpLocationResolverInterface $ipLocationResolver,

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI;
use Zend\Config\Factory;

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Factory;
use Interop\Container\ContainerInterface;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Install\Plugin\Factory;
use Interop\Container\ContainerInterface;
use Interop\Container\Exception\ContainerException;
use Symfony\Component\Console\Helper\QuestionHelper;
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
use Zend\ServiceManager\Exception\ServiceNotFoundException;
use Zend\ServiceManager\Factory\FactoryInterface;
class DefaultConfigCustomizerPluginFactory 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)
{
return new $requestedName($container->get(QuestionHelper::class));
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit\Framework\TestCase;

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit\Framework\TestCase;

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Api;
use PHPUnit\Framework\TestCase;

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Config;
use PHPUnit\Framework\TestCase;

View File

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

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit\Framework\TestCase;
@@ -63,7 +65,7 @@ class GeneratePreviewCommandTest extends TestCase
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledTimes(1);
$this->commandTester->execute([
'command' => 'shortcode:process-previews'
'command' => 'shortcode:process-previews',
]);
}
@@ -83,7 +85,7 @@ class GeneratePreviewCommandTest extends TestCase
->shouldBeCalledTimes(count($items));
$this->commandTester->execute([
'command' => 'shortcode:process-previews'
'command' => 'shortcode:process-previews',
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals(count($items), substr_count($output, 'Error'));

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit\Framework\TestCase;
@@ -27,7 +29,7 @@ class GenerateShortcodeCommandTest extends TestCase
$this->urlShortener = $this->prophesize(UrlShortener::class);
$command = new GenerateShortcodeCommand($this->urlShortener->reveal(), Translator::factory([]), [
'schema' => 'http',
'hostname' => 'foo.com'
'hostname' => 'foo.com',
]);
$app = new Application();
$app->add($command);
@@ -44,7 +46,7 @@ class GenerateShortcodeCommandTest extends TestCase
$this->commandTester->execute([
'command' => 'shortcode:generate',
'longUrl' => 'http://domain.com/foo/bar'
'longUrl' => 'http://domain.com/foo/bar',
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(strpos($output, 'http://foo.com/abc123') > 0);
@@ -60,7 +62,7 @@ class GenerateShortcodeCommandTest extends TestCase
$this->commandTester->execute([
'command' => 'shortcode:generate',
'longUrl' => 'http://domain.com/invalid'
'longUrl' => 'http://domain.com/invalid',
]);
$output = $this->commandTester->getDisplay();
$this->assertTrue(

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit\Framework\TestCase;

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit\Framework\TestCase;

View File

@@ -1,9 +1,12 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
use PHPUnit\Framework\TestCase;
use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\CLI\Command\Shortcode\ResolveUrlCommand;
use Shlinkio\Shlink\Core\Exception\EntityDoesNotExistException;
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
use Shlinkio\Shlink\Core\Service\UrlShortener;
use Symfony\Component\Console\Application;
@@ -55,7 +58,7 @@ class ResolveUrlCommandTest extends TestCase
public function incorrectShortCodeOutputsErrorMessage()
{
$shortCode = 'abc123';
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(EntityDoesNotExistException::class)
->shouldBeCalledTimes(1);
$this->commandTester->execute([
@@ -63,7 +66,7 @@ class ResolveUrlCommandTest extends TestCase
'shortCode' => $shortCode,
]);
$output = $this->commandTester->getDisplay();
$this->assertEquals('No URL found for short code "' . $shortCode . '"' . PHP_EOL, $output);
$this->assertEquals('Provided short code "' . $shortCode . '" could not be found.' . PHP_EOL, $output);
}
/**

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
use PHPUnit\Framework\TestCase;

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI;
use PHPUnit\Framework\TestCase;

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI\Factory;
use PHPUnit\Framework\TestCase;

View File

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

View File

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

View File

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

View File

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

View File

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

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