mirror of
https://github.com/shlinkio/shlink.git
synced 2025-03-29 04:52:54 +03:00
commit
7aa42ada54
95 changed files with 2264 additions and 425 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -3,3 +3,4 @@ build
|
||||||
composer.lock
|
composer.lock
|
||||||
vendor/
|
vendor/
|
||||||
.env
|
.env
|
||||||
|
data/database.sqlite
|
||||||
|
|
25
CHANGELOG.md
25
CHANGELOG.md
|
@ -1,5 +1,30 @@
|
||||||
## CHANGELOG
|
## CHANGELOG
|
||||||
|
|
||||||
|
### 1.2.0
|
||||||
|
|
||||||
|
**Features**
|
||||||
|
|
||||||
|
* [45: Allow to define tags on short codes, to improve filtering and classification](https://github.com/acelaya/url-shortener/issues/45)
|
||||||
|
* [7: Add website previews while listing available URLs](https://github.com/acelaya/url-shortener/issues/7)
|
||||||
|
|
||||||
|
**Enhancements:**
|
||||||
|
|
||||||
|
* [57: Add database migrations system to improve updating between versions](https://github.com/acelaya/url-shortener/issues/57)
|
||||||
|
* [31: Add support for other database management systems by improving the EntityManager factory](https://github.com/acelaya/url-shortener/issues/31)
|
||||||
|
* [51: Generate build process to paquetize the app and ease distribution](https://github.com/acelaya/url-shortener/issues/51)
|
||||||
|
* [38: Define installation script. It will request dynamic data on the fly so that there is no need to define env vars](https://github.com/acelaya/url-shortener/issues/38)
|
||||||
|
|
||||||
|
**Tasks**
|
||||||
|
|
||||||
|
* [55: Create update script which does not try to create a new database](https://github.com/acelaya/url-shortener/issues/55)
|
||||||
|
* [54: Add cache namespace to prevent name collisions with other apps in the same environment](https://github.com/acelaya/url-shortener/issues/54)
|
||||||
|
* [29: Use the acelaya/ze-content-based-error-handler package instead of custom error handler implementation](https://github.com/acelaya/url-shortener/issues/29)
|
||||||
|
|
||||||
|
**Bugs**
|
||||||
|
|
||||||
|
* [53: Fix entities database interoperability](https://github.com/acelaya/url-shortener/issues/53)
|
||||||
|
* [52: Add missing htaccess file for apache environments](https://github.com/acelaya/url-shortener/issues/52)
|
||||||
|
|
||||||
### 1.1.0
|
### 1.1.0
|
||||||
|
|
||||||
**Features**
|
**Features**
|
||||||
|
|
8
bin/cli
8
bin/cli
|
@ -2,16 +2,10 @@
|
||||||
<?php
|
<?php
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
use Symfony\Component\Console\Application as CliApp;
|
use Symfony\Component\Console\Application as CliApp;
|
||||||
use Symfony\Component\Console\Application;
|
|
||||||
use Zend\I18n\Translator\Translator;
|
|
||||||
|
|
||||||
/** @var ContainerInterface $container */
|
/** @var ContainerInterface $container */
|
||||||
$container = include __DIR__ . '/../config/container.php';
|
$container = include __DIR__ . '/../config/container.php';
|
||||||
|
|
||||||
/** @var Translator $translator */
|
/** @var CliApp $app */
|
||||||
$translator = $container->get('translator');
|
|
||||||
$translator->setLocale(env('CLI_LOCALE', 'en'));
|
|
||||||
|
|
||||||
/** @var Application $app */
|
|
||||||
$app = $container->get(CliApp::class);
|
$app = $container->get(CliApp::class);
|
||||||
$app->run();
|
$app->run();
|
||||||
|
|
14
bin/install
Executable file
14
bin/install
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Zend\Config\Writer\PhpArray;
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
$app = new Application();
|
||||||
|
$app->add(new InstallCommand(new PhpArray()));
|
||||||
|
$app->setDefaultCommand('shlink:install');
|
||||||
|
$app->run();
|
14
bin/update
Executable file
14
bin/update
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Install\UpdateCommand;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Zend\Config\Writer\PhpArray;
|
||||||
|
|
||||||
|
chdir(dirname(__DIR__));
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
$app = new Application();
|
||||||
|
$app->add(new UpdateCommand(new PhpArray()));
|
||||||
|
$app->setDefaultCommand('shlink:install');
|
||||||
|
$app->run();
|
BIN
bin/wkhtmltoimage
Executable file
BIN
bin/wkhtmltoimage
Executable file
Binary file not shown.
44
build.sh
Executable file
44
build.sh
Executable file
|
@ -0,0 +1,44 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ "$#" -ne 1 ]; then
|
||||||
|
echo "Usage:" >&2
|
||||||
|
echo " $0 {version}" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
version=$1
|
||||||
|
builtcontent=$(readlink -f '../shlink_build_tmp')
|
||||||
|
projectdir=$(pwd)
|
||||||
|
|
||||||
|
# Copy project content to temp dir
|
||||||
|
echo 'Copying project files...'
|
||||||
|
rm -rf "${builtcontent}"
|
||||||
|
mkdir "${builtcontent}"
|
||||||
|
cp -R "${projectdir}"/* "${builtcontent}"
|
||||||
|
cd "${builtcontent}"
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
rm -r vendor
|
||||||
|
rm composer.lock
|
||||||
|
composer self-update
|
||||||
|
composer install --no-dev --optimize-autoloader
|
||||||
|
|
||||||
|
# Delete development files
|
||||||
|
echo 'Deleting dev files...'
|
||||||
|
rm build.sh
|
||||||
|
rm CHANGELOG.md
|
||||||
|
rm composer.*
|
||||||
|
rm LICENSE
|
||||||
|
rm php*
|
||||||
|
rm README.md
|
||||||
|
rm -r build
|
||||||
|
rm -f data/database.sqlite
|
||||||
|
rm -rf data/{cache,log,proxies}/{*,.gitignore}
|
||||||
|
rm -rf config/params/{*,.gitignore}
|
||||||
|
rm -rf config/autoload/{{,*.}local.php{,.dist},.gitignore}
|
||||||
|
|
||||||
|
# Compressing file
|
||||||
|
rm -f "${projectdir}"/build/shlink_${version}_dist.zip
|
||||||
|
zip -r "${projectdir}"/build/shlink_${version}_dist.zip .
|
||||||
|
rm -rf "${builtcontent}"
|
|
@ -23,13 +23,18 @@
|
||||||
"zendframework/zend-i18n": "^2.7",
|
"zendframework/zend-i18n": "^2.7",
|
||||||
"mtymek/expressive-config-manager": "^0.4",
|
"mtymek/expressive-config-manager": "^0.4",
|
||||||
"acelaya/zsm-annotated-services": "^0.2.0",
|
"acelaya/zsm-annotated-services": "^0.2.0",
|
||||||
|
"acelaya/ze-content-based-error-handler": "^1.0",
|
||||||
"doctrine/orm": "^2.5",
|
"doctrine/orm": "^2.5",
|
||||||
"guzzlehttp/guzzle": "^6.2",
|
"guzzlehttp/guzzle": "^6.2",
|
||||||
"symfony/console": "^3.0",
|
"symfony/console": "^3.0",
|
||||||
|
"symfony/process": "^3.0",
|
||||||
|
"symfony/filesystem": "^3.0",
|
||||||
"firebase/php-jwt": "^4.0",
|
"firebase/php-jwt": "^4.0",
|
||||||
"monolog/monolog": "^1.21",
|
"monolog/monolog": "^1.21",
|
||||||
"theorchard/monolog-cascade": "^0.4",
|
"theorchard/monolog-cascade": "^0.4",
|
||||||
"endroid/qrcode": "^1.7"
|
"endroid/qrcode": "^1.7",
|
||||||
|
"mikehaertl/phpwkhtmltopdf": "^2.2",
|
||||||
|
"doctrine/migrations": "^1.4"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpunit/phpunit": "^5.0",
|
"phpunit/phpunit": "^5.0",
|
||||||
|
|
|
@ -3,7 +3,7 @@ return [
|
||||||
|
|
||||||
'app_options' => [
|
'app_options' => [
|
||||||
'name' => 'Shlink',
|
'name' => 'Shlink',
|
||||||
'version' => '1.1.0',
|
'version' => '1.2.0',
|
||||||
'secret_key' => env('SECRET_KEY'),
|
'secret_key' => env('SECRET_KEY'),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<?php
|
<?php
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
|
|
||||||
use Zend\Expressive;
|
use Zend\Expressive;
|
||||||
use Zend\Expressive\Container;
|
use Zend\Expressive\Container;
|
||||||
use Zend\Expressive\Router;
|
use Zend\Expressive\Router;
|
||||||
|
@ -17,7 +16,6 @@ return [
|
||||||
],
|
],
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
Router\RouterInterface::class => Router\FastRouteRouter::class,
|
Router\RouterInterface::class => Router\FastRouteRouter::class,
|
||||||
'Zend\Expressive\FinalHandler' => ContentBasedErrorHandler::class,
|
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
|
use Acelaya\ExpressiveErrorHandler\ErrorHandler\ContentBasedErrorHandler;
|
||||||
use Zend\Expressive\Container\WhoopsErrorHandlerFactory;
|
use Zend\Expressive\Container\WhoopsErrorHandlerFactory;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
11
config/autoload/phpwkhtmltopdf.global.php
Normal file
11
config/autoload/phpwkhtmltopdf.global.php
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
return [
|
||||||
|
|
||||||
|
'phpwkhtmltopdf' => [
|
||||||
|
'images' => [
|
||||||
|
'binary' => 'bin/wkhtmltoimage',
|
||||||
|
'type' => 'jpg',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
8
config/autoload/preview-generation.global.php
Normal file
8
config/autoload/preview-generation.global.php
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
return [
|
||||||
|
|
||||||
|
'preview_generation' => [
|
||||||
|
'files_location' => 'data/cache',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
|
@ -1,10 +1,10 @@
|
||||||
<?php
|
<?php
|
||||||
|
use Acelaya\ExpressiveErrorHandler;
|
||||||
use Shlinkio\Shlink\CLI;
|
use Shlinkio\Shlink\CLI;
|
||||||
use Shlinkio\Shlink\Common;
|
use Shlinkio\Shlink\Common;
|
||||||
use Shlinkio\Shlink\Core;
|
use Shlinkio\Shlink\Core;
|
||||||
use Shlinkio\Shlink\Rest;
|
use Shlinkio\Shlink\Rest;
|
||||||
use Zend\Expressive\ConfigManager\ConfigManager;
|
use Zend\Expressive\ConfigManager;
|
||||||
use Zend\Expressive\ConfigManager\ZendConfigProvider;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
|
* Configuration files are loaded in a specific order. First ``global.php``, then ``*.global.php``.
|
||||||
|
@ -15,10 +15,11 @@ use Zend\Expressive\ConfigManager\ZendConfigProvider;
|
||||||
* Obviously, if you use closures in your config you can't cache it.
|
* Obviously, if you use closures in your config you can't cache it.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return (new ConfigManager([
|
return (new ConfigManager\ConfigManager([
|
||||||
|
ExpressiveErrorHandler\ConfigProvider::class,
|
||||||
Common\ConfigProvider::class,
|
Common\ConfigProvider::class,
|
||||||
Core\ConfigProvider::class,
|
Core\ConfigProvider::class,
|
||||||
CLI\ConfigProvider::class,
|
CLI\ConfigProvider::class,
|
||||||
Rest\ConfigProvider::class,
|
Rest\ConfigProvider::class,
|
||||||
new ZendConfigProvider('config/autoload/{{,*.}global,{,*.}local}.php'),
|
new ConfigManager\ZendConfigProvider('config/{autoload/{{,*.}global,{,*.}local},params/generated_config}.php'),
|
||||||
], 'data/cache/app_config.php'))->getMergedConfig();
|
], 'data/cache/app_config.php'))->getMergedConfig();
|
||||||
|
|
2
config/params/.gitignore
vendored
Normal file
2
config/params/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
39
data/migrations/Version20160819142757.php
Normal file
39
data/migrations/Version20160819142757.php
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace ShlinkMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
class Version20160819142757 extends AbstractMigration
|
||||||
|
{
|
||||||
|
const MYSQL = 'mysql';
|
||||||
|
const SQLITE = 'sqlite';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
*/
|
||||||
|
public function up(Schema $schema)
|
||||||
|
{
|
||||||
|
$db = $this->connection->getDatabasePlatform()->getName();
|
||||||
|
$table = $schema->getTable('short_urls');
|
||||||
|
$column = $table->getColumn('short_code');
|
||||||
|
|
||||||
|
if ($db === self::MYSQL) {
|
||||||
|
$column->setPlatformOption('collation', 'utf8_bin');
|
||||||
|
} elseif ($db === self::SQLITE) {
|
||||||
|
$column->setPlatformOption('collate', 'BINARY');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
*/
|
||||||
|
public function down(Schema $schema)
|
||||||
|
{
|
||||||
|
$db = $this->connection->getDatabasePlatform()->getName();
|
||||||
|
}
|
||||||
|
}
|
80
data/migrations/Version20160820191203.php
Normal file
80
data/migrations/Version20160820191203.php
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace ShlinkMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\DBAL\Types\Type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
class Version20160820191203 extends AbstractMigration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
*/
|
||||||
|
public function up(Schema $schema)
|
||||||
|
{
|
||||||
|
// Check if the tables already exist
|
||||||
|
$tables = $schema->getTables();
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
if ($table->getName() === 'tags') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->createTagsTable($schema);
|
||||||
|
$this->createShortUrlsInTagsTable($schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createTagsTable(Schema $schema)
|
||||||
|
{
|
||||||
|
$table = $schema->createTable('tags');
|
||||||
|
$table->addColumn('id', Type::BIGINT, [
|
||||||
|
'unsigned' => true,
|
||||||
|
'autoincrement' => true,
|
||||||
|
'notnull' => true,
|
||||||
|
]);
|
||||||
|
$table->addColumn('name', Type::STRING, [
|
||||||
|
'length' => 255,
|
||||||
|
'notnull' => true,
|
||||||
|
]);
|
||||||
|
$table->addUniqueIndex(['name']);
|
||||||
|
|
||||||
|
$table->setPrimaryKey(['id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createShortUrlsInTagsTable(Schema $schema)
|
||||||
|
{
|
||||||
|
$table = $schema->createTable('short_urls_in_tags');
|
||||||
|
$table->addColumn('short_url_id', Type::BIGINT, [
|
||||||
|
'unsigned' => true,
|
||||||
|
'notnull' => true,
|
||||||
|
]);
|
||||||
|
$table->addColumn('tag_id', Type::BIGINT, [
|
||||||
|
'unsigned' => true,
|
||||||
|
'notnull' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table->addForeignKeyConstraint('tags', ['tag_id'], ['id'], [
|
||||||
|
'onDelete' => 'CASCADE',
|
||||||
|
'onUpdate' => 'RESTRICT',
|
||||||
|
]);
|
||||||
|
$table->addForeignKeyConstraint('short_urls', ['short_url_id'], ['id'], [
|
||||||
|
'onDelete' => 'CASCADE',
|
||||||
|
'onUpdate' => 'RESTRICT',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$table->setPrimaryKey(['short_url_id', 'tag_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Schema $schema
|
||||||
|
*/
|
||||||
|
public function down(Schema $schema)
|
||||||
|
{
|
||||||
|
$schema->dropTable('short_urls_in_tags');
|
||||||
|
$schema->dropTable('tags');
|
||||||
|
}
|
||||||
|
}
|
4
migrations.yml
Normal file
4
migrations.yml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
name: ShlinkMigrations
|
||||||
|
migrations_namespace: ShlinkMigrations
|
||||||
|
table_name: migrations
|
||||||
|
migrations_directory: data/migrations
|
|
@ -4,11 +4,13 @@ use Shlinkio\Shlink\CLI\Command;
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'cli' => [
|
'cli' => [
|
||||||
|
'locale' => env('CLI_LOCALE', 'en'),
|
||||||
'commands' => [
|
'commands' => [
|
||||||
Command\Shortcode\GenerateShortcodeCommand::class,
|
Command\Shortcode\GenerateShortcodeCommand::class,
|
||||||
Command\Shortcode\ResolveUrlCommand::class,
|
Command\Shortcode\ResolveUrlCommand::class,
|
||||||
Command\Shortcode\ListShortcodesCommand::class,
|
Command\Shortcode\ListShortcodesCommand::class,
|
||||||
Command\Shortcode\GetVisitsCommand::class,
|
Command\Shortcode\GetVisitsCommand::class,
|
||||||
|
Command\Shortcode\GeneratePreviewCommand::class,
|
||||||
Command\Visit\ProcessVisitsCommand::class,
|
Command\Visit\ProcessVisitsCommand::class,
|
||||||
Command\Config\GenerateCharsetCommand::class,
|
Command\Config\GenerateCharsetCommand::class,
|
||||||
Command\Config\GenerateSecretCommand::class,
|
Command\Config\GenerateSecretCommand::class,
|
||||||
|
|
|
@ -14,6 +14,7 @@ return [
|
||||||
Command\Shortcode\ResolveUrlCommand::class => AnnotatedFactory::class,
|
Command\Shortcode\ResolveUrlCommand::class => AnnotatedFactory::class,
|
||||||
Command\Shortcode\ListShortcodesCommand::class => AnnotatedFactory::class,
|
Command\Shortcode\ListShortcodesCommand::class => AnnotatedFactory::class,
|
||||||
Command\Shortcode\GetVisitsCommand::class => AnnotatedFactory::class,
|
Command\Shortcode\GetVisitsCommand::class => AnnotatedFactory::class,
|
||||||
|
Command\Shortcode\GeneratePreviewCommand::class => AnnotatedFactory::class,
|
||||||
Command\Visit\ProcessVisitsCommand::class => AnnotatedFactory::class,
|
Command\Visit\ProcessVisitsCommand::class => AnnotatedFactory::class,
|
||||||
Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class,
|
Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class,
|
||||||
Command\Config\GenerateSecretCommand::class => AnnotatedFactory::class,
|
Command\Config\GenerateSecretCommand::class => AnnotatedFactory::class,
|
||||||
|
|
Binary file not shown.
|
@ -1,8 +1,8 @@
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Shlink 1.0\n"
|
"Project-Id-Version: Shlink 1.0\n"
|
||||||
"POT-Creation-Date: 2016-08-07 20:16+0200\n"
|
"POT-Creation-Date: 2016-08-21 18:16+0200\n"
|
||||||
"PO-Revision-Date: 2016-08-07 20:18+0200\n"
|
"PO-Revision-Date: 2016-08-21 18:16+0200\n"
|
||||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
|
@ -68,7 +68,35 @@ msgstr ""
|
||||||
msgid "Character set:"
|
msgid "Character set:"
|
||||||
msgstr "Grupo de caracteres:"
|
msgstr "Grupo de caracteres:"
|
||||||
|
|
||||||
#, fuzzy
|
msgid ""
|
||||||
|
"Generates a random secret string that can be used for JWT token encryption"
|
||||||
|
msgstr ""
|
||||||
|
"Genera una cadena de caracteres aleatoria que puede ser usada para cifrar "
|
||||||
|
"tokens JWT"
|
||||||
|
|
||||||
|
msgid "Secret key:"
|
||||||
|
msgstr "Clave secreta:"
|
||||||
|
|
||||||
|
msgid ""
|
||||||
|
"Processes and generates the previews for every URL, improving performance "
|
||||||
|
"for later web requests."
|
||||||
|
msgstr ""
|
||||||
|
"Procesa y genera las vistas previas para cada URL, mejorando el rendimiento "
|
||||||
|
"para peticiones web posteriores."
|
||||||
|
|
||||||
|
msgid "Finished processing all URLs"
|
||||||
|
msgstr "Finalizado el procesado de todas las URLs"
|
||||||
|
|
||||||
|
#, php-format
|
||||||
|
msgid "Processing URL %s..."
|
||||||
|
msgstr "Procesando URL %s..."
|
||||||
|
|
||||||
|
msgid " <info>Success!</info>"
|
||||||
|
msgstr "<info>¡Correcto!</info>"
|
||||||
|
|
||||||
|
msgid "Error"
|
||||||
|
msgstr "Error"
|
||||||
|
|
||||||
msgid "Generates a short code for provided URL and returns the short URL"
|
msgid "Generates a short code for provided URL and returns the short URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Genera un código corto para la URL proporcionada y devuelve la URL acortada"
|
"Genera un código corto para la URL proporcionada y devuelve la URL acortada"
|
||||||
|
@ -76,6 +104,9 @@ msgstr ""
|
||||||
msgid "The long URL to parse"
|
msgid "The long URL to parse"
|
||||||
msgstr "La URL larga a procesar"
|
msgstr "La URL larga a procesar"
|
||||||
|
|
||||||
|
msgid "Tags to apply to the new short URL"
|
||||||
|
msgstr "Etiquetas a aplicar a la nueva URL acortada"
|
||||||
|
|
||||||
msgid "A long URL was not provided. Which URL do you want to shorten?:"
|
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?"
|
msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?"
|
||||||
|
|
||||||
|
@ -131,6 +162,9 @@ msgstr "Listar todas las URLs cortas"
|
||||||
msgid "The first page to list (%s items per page)"
|
msgid "The first page to list (%s items per page)"
|
||||||
msgstr "La primera página a listar (%s elementos por página)"
|
msgstr "La primera página a listar (%s elementos por página)"
|
||||||
|
|
||||||
|
msgid "Whether to display the tags or not"
|
||||||
|
msgstr "Si se desea mostrar las etiquetas o no"
|
||||||
|
|
||||||
msgid "Short code"
|
msgid "Short code"
|
||||||
msgstr "Código corto"
|
msgstr "Código corto"
|
||||||
|
|
||||||
|
@ -143,28 +177,15 @@ msgstr "Fecha de creación"
|
||||||
msgid "Visits count"
|
msgid "Visits count"
|
||||||
msgstr "Número de visitas"
|
msgstr "Número de visitas"
|
||||||
|
|
||||||
|
msgid "Tags"
|
||||||
|
msgstr "Etiquetas"
|
||||||
|
|
||||||
msgid "You have reached last page"
|
msgid "You have reached last page"
|
||||||
msgstr "Has alcanzado la última página"
|
msgstr "Has alcanzado la última página"
|
||||||
|
|
||||||
msgid "Continue with page"
|
msgid "Continue with page"
|
||||||
msgstr "Continuar con la página"
|
msgstr "Continuar con la página"
|
||||||
|
|
||||||
msgid "Processes visits where location is not set yet"
|
|
||||||
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
|
|
||||||
|
|
||||||
msgid "Processing IP"
|
|
||||||
msgstr "Procesando IP"
|
|
||||||
|
|
||||||
msgid "Ignored localhost address"
|
|
||||||
msgstr "Ignorada IP de localhost"
|
|
||||||
|
|
||||||
#, php-format
|
|
||||||
msgid "Address located at \"%s\""
|
|
||||||
msgstr "Dirección localizada en \"%s\""
|
|
||||||
|
|
||||||
msgid "Finished processing all IPs"
|
|
||||||
msgstr "Finalizado el procesado de todas las IPs"
|
|
||||||
|
|
||||||
msgid "Returns the long URL behind a short code"
|
msgid "Returns the long URL behind a short code"
|
||||||
msgstr "Devuelve la URL larga detrás de un código corto"
|
msgstr "Devuelve la URL larga detrás de un código corto"
|
||||||
|
|
||||||
|
@ -185,3 +206,19 @@ msgstr "URL larga:"
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Provided short code \"%s\" has an invalid format."
|
msgid "Provided short code \"%s\" has an invalid format."
|
||||||
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
|
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
|
||||||
|
|
||||||
|
msgid "Processes visits where location is not set yet"
|
||||||
|
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
|
||||||
|
|
||||||
|
msgid "Processing IP"
|
||||||
|
msgstr "Procesando IP"
|
||||||
|
|
||||||
|
msgid "Ignored localhost address"
|
||||||
|
msgstr "Ignorada IP de localhost"
|
||||||
|
|
||||||
|
#, php-format
|
||||||
|
msgid "Address located at \"%s\""
|
||||||
|
msgstr "Dirección localizada en \"%s\""
|
||||||
|
|
||||||
|
msgid "Finished processing all IPs"
|
||||||
|
msgstr "Finalizado el procesado de todas las IPs"
|
||||||
|
|
307
module/CLI/src/Command/Install/InstallCommand.php
Normal file
307
module/CLI/src/Command/Install/InstallCommand.php
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Install;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||||
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||||
|
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||||
|
use Symfony\Component\Console\Question\Question;
|
||||||
|
use Zend\Config\Writer\WriterInterface;
|
||||||
|
|
||||||
|
class InstallCommand extends Command
|
||||||
|
{
|
||||||
|
use StringUtilsTrait;
|
||||||
|
|
||||||
|
const DATABASE_DRIVERS = [
|
||||||
|
'MySQL' => 'pdo_mysql',
|
||||||
|
'PostgreSQL' => 'pdo_pgsql',
|
||||||
|
'SQLite' => 'pdo_sqlite',
|
||||||
|
];
|
||||||
|
const SUPPORTED_LANGUAGES = ['en', 'es'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var InputInterface
|
||||||
|
*/
|
||||||
|
private $input;
|
||||||
|
/**
|
||||||
|
* @var OutputInterface
|
||||||
|
*/
|
||||||
|
private $output;
|
||||||
|
/**
|
||||||
|
* @var QuestionHelper
|
||||||
|
*/
|
||||||
|
private $questionHelper;
|
||||||
|
/**
|
||||||
|
* @var ProcessHelper
|
||||||
|
*/
|
||||||
|
private $processHelper;
|
||||||
|
/**
|
||||||
|
* @var WriterInterface
|
||||||
|
*/
|
||||||
|
private $configWriter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* InstallCommand constructor.
|
||||||
|
* @param WriterInterface $configWriter
|
||||||
|
* @param callable|null $databaseCreationLogic
|
||||||
|
*/
|
||||||
|
public function __construct(WriterInterface $configWriter)
|
||||||
|
{
|
||||||
|
parent::__construct(null);
|
||||||
|
$this->configWriter = $configWriter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure()
|
||||||
|
{
|
||||||
|
$this->setName('shlink:install')
|
||||||
|
->setDescription('Installs Shlink');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$this->input = $input;
|
||||||
|
$this->output = $output;
|
||||||
|
$this->questionHelper = $this->getHelper('question');
|
||||||
|
$this->processHelper = $this->getHelper('process');
|
||||||
|
$params = [];
|
||||||
|
|
||||||
|
$output->writeln([
|
||||||
|
'<info>Welcome to Shlink!!</info>',
|
||||||
|
'This process will guide you through the installation.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if a cached config file exists and drop it if so
|
||||||
|
if (file_exists('data/cache/app_config.php')) {
|
||||||
|
$output->write('Deleting old cached config...');
|
||||||
|
if (unlink('data/cache/app_config.php')) {
|
||||||
|
$output->writeln(' <info>Success</info>');
|
||||||
|
} else {
|
||||||
|
$output->writeln(
|
||||||
|
' <error>Failed!</error> You will have to manually delete the data/cache/app_config.php file to get'
|
||||||
|
. ' new config applied.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask for custom config params
|
||||||
|
$params['DATABASE'] = $this->askDatabase();
|
||||||
|
$params['URL_SHORTENER'] = $this->askUrlShortener();
|
||||||
|
$params['LANGUAGE'] = $this->askLanguage();
|
||||||
|
$params['APP'] = $this->askApplication();
|
||||||
|
|
||||||
|
// Generate config params files
|
||||||
|
$config = $this->buildAppConfig($params);
|
||||||
|
$this->configWriter->toFile('config/params/generated_config.php', $config, false);
|
||||||
|
$output->writeln(['<info>Custom configuration properly generated!</info>', '']);
|
||||||
|
|
||||||
|
// Generate database
|
||||||
|
if (! $this->createDatabase()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run database migrations
|
||||||
|
$output->writeln('Updating database...');
|
||||||
|
if (! $this->runCommand('php vendor/bin/doctrine-migrations migrations:migrate', 'Error updating database.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate proxies
|
||||||
|
$output->writeln('Generating proxies...');
|
||||||
|
if (! $this->runCommand('php vendor/bin/doctrine.php orm:generate-proxies', 'Error generating proxies.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function askDatabase()
|
||||||
|
{
|
||||||
|
$params = [];
|
||||||
|
$this->printTitle('DATABASE');
|
||||||
|
|
||||||
|
// Select database type
|
||||||
|
$databases = array_keys(self::DATABASE_DRIVERS);
|
||||||
|
$dbType = $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||||
|
'<question>Select database type (defaults to ' . $databases[0] . '):</question>',
|
||||||
|
$databases,
|
||||||
|
0
|
||||||
|
));
|
||||||
|
$params['DRIVER'] = self::DATABASE_DRIVERS[$dbType];
|
||||||
|
|
||||||
|
// Ask for connection params if database is not SQLite
|
||||||
|
if ($params['DRIVER'] !== self::DATABASE_DRIVERS['SQLite']) {
|
||||||
|
$params['NAME'] = $this->ask('Database name', 'shlink');
|
||||||
|
$params['USER'] = $this->ask('Database username');
|
||||||
|
$params['PASSWORD'] = $this->ask('Database password');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $params;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function askUrlShortener()
|
||||||
|
{
|
||||||
|
$this->printTitle('URL SHORTENER');
|
||||||
|
|
||||||
|
// Ask for URL shortener params
|
||||||
|
return [
|
||||||
|
'SCHEMA' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||||
|
'<question>Select schema for generated short URLs (defaults to http):</question>',
|
||||||
|
['http', 'https'],
|
||||||
|
0
|
||||||
|
)),
|
||||||
|
'HOSTNAME' => $this->ask('Hostname for generated URLs'),
|
||||||
|
'CHARS' => $this->ask(
|
||||||
|
'Character set for generated short codes (leave empty to autogenerate one)',
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
) ?: str_shuffle(UrlShortener::DEFAULT_CHARS)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function askLanguage()
|
||||||
|
{
|
||||||
|
$this->printTitle('LANGUAGE');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'DEFAULT' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||||
|
'<question>Select default language for the application in general (defaults to '
|
||||||
|
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
|
||||||
|
self::SUPPORTED_LANGUAGES,
|
||||||
|
0
|
||||||
|
)),
|
||||||
|
'CLI' => $this->questionHelper->ask($this->input, $this->output, new ChoiceQuestion(
|
||||||
|
'<question>Select default language for CLI executions (defaults to '
|
||||||
|
. self::SUPPORTED_LANGUAGES[0] . '):</question>',
|
||||||
|
self::SUPPORTED_LANGUAGES,
|
||||||
|
0
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function askApplication()
|
||||||
|
{
|
||||||
|
$this->printTitle('APPLICATION');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'SECRET' => $this->ask(
|
||||||
|
'Define a secret string that will be used to sign API tokens (leave empty to autogenerate one)',
|
||||||
|
null,
|
||||||
|
true
|
||||||
|
) ?: $this->generateRandomString(32),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $text
|
||||||
|
*/
|
||||||
|
protected function printTitle($text)
|
||||||
|
{
|
||||||
|
$text = trim($text);
|
||||||
|
$length = strlen($text) + 4;
|
||||||
|
$header = str_repeat('*', $length);
|
||||||
|
|
||||||
|
$this->output->writeln([
|
||||||
|
'',
|
||||||
|
'<info>' . $header . '</info>',
|
||||||
|
'<info>* ' . strtoupper($text) . ' *</info>',
|
||||||
|
'<info>' . $header . '</info>',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $text
|
||||||
|
* @param string|null $default
|
||||||
|
* @param bool $allowEmpty
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function ask($text, $default = null, $allowEmpty = false)
|
||||||
|
{
|
||||||
|
if (isset($default)) {
|
||||||
|
$text .= ' (defaults to ' . $default . ')';
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
$value = $this->questionHelper->ask($this->input, $this->output, new Question(
|
||||||
|
'<question>' . $text . ':</question> ',
|
||||||
|
$default
|
||||||
|
));
|
||||||
|
if (empty($value) && ! $allowEmpty) {
|
||||||
|
$this->output->writeln('<error>Value can\'t be empty</error>');
|
||||||
|
}
|
||||||
|
} while (empty($value) && empty($default) && ! $allowEmpty);
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $params
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function buildAppConfig(array $params)
|
||||||
|
{
|
||||||
|
// Build simple config
|
||||||
|
$config = [
|
||||||
|
'app_options' => [
|
||||||
|
'secret_key' => $params['APP']['SECRET'],
|
||||||
|
],
|
||||||
|
'entity_manager' => [
|
||||||
|
'connection' => [
|
||||||
|
'driver' => $params['DATABASE']['DRIVER'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'translator' => [
|
||||||
|
'locale' => $params['LANGUAGE']['DEFAULT'],
|
||||||
|
],
|
||||||
|
'cli' => [
|
||||||
|
'locale' => $params['LANGUAGE']['CLI'],
|
||||||
|
],
|
||||||
|
'url_shortener' => [
|
||||||
|
'domain' => [
|
||||||
|
'schema' => $params['URL_SHORTENER']['SCHEMA'],
|
||||||
|
'hostname' => $params['URL_SHORTENER']['HOSTNAME'],
|
||||||
|
],
|
||||||
|
'shortcode_chars' => $params['URL_SHORTENER']['CHARS'],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
// Build dynamic database config
|
||||||
|
if ($params['DATABASE']['DRIVER'] === 'pdo_sqlite') {
|
||||||
|
$config['entity_manager']['connection']['path'] = 'data/database.sqlite';
|
||||||
|
} else {
|
||||||
|
$config['entity_manager']['connection']['user'] = $params['DATABASE']['USER'];
|
||||||
|
$config['entity_manager']['connection']['password'] = $params['DATABASE']['PASSWORD'];
|
||||||
|
$config['entity_manager']['connection']['dbname'] = $params['DATABASE']['NAME'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createDatabase()
|
||||||
|
{
|
||||||
|
$this->output->writeln('Initializing database...');
|
||||||
|
return $this->runCommand('php vendor/bin/doctrine.php orm:schema-tool:create', 'Error generating database.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $command
|
||||||
|
* @param string $errorMessage
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function runCommand($command, $errorMessage)
|
||||||
|
{
|
||||||
|
$process = $this->processHelper->run($this->output, $command);
|
||||||
|
if ($process->isSuccessful()) {
|
||||||
|
$this->output->writeln(' <info>Success!</info>');
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
if ($this->output->isVerbose()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$this->output->writeln(
|
||||||
|
' <error>' . $errorMessage . '</error> Run this command with -vvv to see specific error info.'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
module/CLI/src/Command/Install/UpdateCommand.php
Normal file
12
module/CLI/src/Command/Install/UpdateCommand.php
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Install;
|
||||||
|
|
||||||
|
use Zend\Config\Writer\WriterInterface;
|
||||||
|
|
||||||
|
class UpdateCommand extends InstallCommand
|
||||||
|
{
|
||||||
|
public function createDatabase()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
89
module/CLI/src/Command/Shortcode/GeneratePreviewCommand.php
Normal file
89
module/CLI/src/Command/Shortcode/GeneratePreviewCommand.php
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\CLI\Command\Shortcode;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||||
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
|
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||||
|
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
|
||||||
|
class GeneratePreviewCommand extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PreviewGeneratorInterface
|
||||||
|
*/
|
||||||
|
private $previewGenerator;
|
||||||
|
/**
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
private $translator;
|
||||||
|
/**
|
||||||
|
* @var ShortUrlServiceInterface
|
||||||
|
*/
|
||||||
|
private $shortUrlService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GeneratePreviewCommand constructor.
|
||||||
|
* @param ShortUrlServiceInterface $shortUrlService
|
||||||
|
* @param PreviewGeneratorInterface $previewGenerator
|
||||||
|
* @param TranslatorInterface $translator
|
||||||
|
*
|
||||||
|
* @Inject({ShortUrlService::class, PreviewGenerator::class, "translator"})
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
ShortUrlServiceInterface $shortUrlService,
|
||||||
|
PreviewGeneratorInterface $previewGenerator,
|
||||||
|
TranslatorInterface $translator
|
||||||
|
) {
|
||||||
|
$this->shortUrlService = $shortUrlService;
|
||||||
|
$this->previewGenerator = $previewGenerator;
|
||||||
|
$this->translator = $translator;
|
||||||
|
parent::__construct(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configure()
|
||||||
|
{
|
||||||
|
$this->setName('shortcode:process-previews')
|
||||||
|
->setDescription(
|
||||||
|
$this->translator->translate(
|
||||||
|
'Processes and generates the previews for every URL, improving performance for later web requests.'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(InputInterface $input, OutputInterface $output)
|
||||||
|
{
|
||||||
|
$page = 1;
|
||||||
|
do {
|
||||||
|
$shortUrls = $this->shortUrlService->listShortUrls($page);
|
||||||
|
$page += 1;
|
||||||
|
|
||||||
|
foreach ($shortUrls as $shortUrl) {
|
||||||
|
$this->processUrl($shortUrl->getOriginalUrl(), $output);
|
||||||
|
}
|
||||||
|
} while ($page <= $shortUrls->count());
|
||||||
|
|
||||||
|
$output->writeln('<info>' . $this->translator->translate('Finished processing all URLs') . '</info>');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function processUrl($url, OutputInterface $output)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$output->write(sprintf($this->translator->translate('Processing URL %s...'), $url));
|
||||||
|
$this->previewGenerator->generatePreview($url);
|
||||||
|
$output->writeln($this->translator->translate(' <info>Success!</info>'));
|
||||||
|
} catch (PreviewGenerationException $e) {
|
||||||
|
$messages = [' <error>' . $this->translator->translate('Error') . '</error>'];
|
||||||
|
if ($output->isVerbose()) {
|
||||||
|
$messages[] = '<error>' . $e->__toString() . '</error>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$output->writeln($messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ use Symfony\Component\Console\Command\Command;
|
||||||
use Symfony\Component\Console\Helper\QuestionHelper;
|
use Symfony\Component\Console\Helper\QuestionHelper;
|
||||||
use Symfony\Component\Console\Input\InputArgument;
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Question\Question;
|
use Symfony\Component\Console\Question\Question;
|
||||||
use Zend\Diactoros\Uri;
|
use Zend\Diactoros\Uri;
|
||||||
|
@ -54,7 +55,13 @@ class GenerateShortcodeCommand extends Command
|
||||||
->setDescription(
|
->setDescription(
|
||||||
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
|
$this->translator->translate('Generates a short code for provided URL and returns the short URL')
|
||||||
)
|
)
|
||||||
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'));
|
->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse'))
|
||||||
|
->addOption(
|
||||||
|
'tags',
|
||||||
|
't',
|
||||||
|
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL,
|
||||||
|
$this->translator->translate('Tags to apply to the new short URL')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function interact(InputInterface $input, OutputInterface $output)
|
public function interact(InputInterface $input, OutputInterface $output)
|
||||||
|
@ -80,6 +87,13 @@ class GenerateShortcodeCommand extends Command
|
||||||
public function execute(InputInterface $input, OutputInterface $output)
|
public function execute(InputInterface $input, OutputInterface $output)
|
||||||
{
|
{
|
||||||
$longUrl = $input->getArgument('longUrl');
|
$longUrl = $input->getArgument('longUrl');
|
||||||
|
$tags = $input->getOption('tags');
|
||||||
|
$processedTags = [];
|
||||||
|
foreach ($tags as $key => $tag) {
|
||||||
|
$explodedTags = explode(',', $tag);
|
||||||
|
$processedTags = array_merge($processedTags, $explodedTags);
|
||||||
|
}
|
||||||
|
$tags = $processedTags;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (! isset($longUrl)) {
|
if (! isset($longUrl)) {
|
||||||
|
@ -87,10 +101,10 @@ class GenerateShortcodeCommand extends Command
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl));
|
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl), $tags);
|
||||||
$shortUrl = (new Uri())->withPath($shortCode)
|
$shortUrl = (new Uri())->withPath($shortCode)
|
||||||
->withScheme($this->domainConfig['schema'])
|
->withScheme($this->domainConfig['schema'])
|
||||||
->withHost($this->domainConfig['hostname']);
|
->withHost($this->domainConfig['hostname']);
|
||||||
|
|
||||||
$output->writeln([
|
$output->writeln([
|
||||||
sprintf('%s <info>%s</info>', $this->translator->translate('Processed URL:'), $longUrl),
|
sprintf('%s <info>%s</info>', $this->translator->translate('Processed URL:'), $longUrl),
|
||||||
|
|
|
@ -55,12 +55,20 @@ class ListShortcodesCommand extends Command
|
||||||
PaginableRepositoryAdapter::ITEMS_PER_PAGE
|
PaginableRepositoryAdapter::ITEMS_PER_PAGE
|
||||||
),
|
),
|
||||||
1
|
1
|
||||||
|
)
|
||||||
|
->addOption(
|
||||||
|
'tags',
|
||||||
|
't',
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
$this->translator->translate('Whether to display the tags or not')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function execute(InputInterface $input, OutputInterface $output)
|
public function execute(InputInterface $input, OutputInterface $output)
|
||||||
{
|
{
|
||||||
$page = intval($input->getOption('page'));
|
$page = intval($input->getOption('page'));
|
||||||
|
$showTags = $input->getOption('tags');
|
||||||
|
|
||||||
/** @var QuestionHelper $helper */
|
/** @var QuestionHelper $helper */
|
||||||
$helper = $this->getHelper('question');
|
$helper = $this->getHelper('question');
|
||||||
|
|
||||||
|
@ -68,15 +76,31 @@ class ListShortcodesCommand extends Command
|
||||||
$result = $this->shortUrlService->listShortUrls($page);
|
$result = $this->shortUrlService->listShortUrls($page);
|
||||||
$page++;
|
$page++;
|
||||||
$table = new Table($output);
|
$table = new Table($output);
|
||||||
$table->setHeaders([
|
|
||||||
|
$headers = [
|
||||||
$this->translator->translate('Short code'),
|
$this->translator->translate('Short code'),
|
||||||
$this->translator->translate('Original URL'),
|
$this->translator->translate('Original URL'),
|
||||||
$this->translator->translate('Date created'),
|
$this->translator->translate('Date created'),
|
||||||
$this->translator->translate('Visits count'),
|
$this->translator->translate('Visits count'),
|
||||||
]);
|
];
|
||||||
|
if ($showTags) {
|
||||||
|
$headers[] = $this->translator->translate('Tags');
|
||||||
|
}
|
||||||
|
$table->setHeaders($headers);
|
||||||
|
|
||||||
foreach ($result as $row) {
|
foreach ($result as $row) {
|
||||||
$table->addRow(array_values($row->jsonSerialize()));
|
$shortUrl = $row->jsonSerialize();
|
||||||
|
if ($showTags) {
|
||||||
|
$shortUrl['tags'] = [];
|
||||||
|
foreach ($row->getTags() as $tag) {
|
||||||
|
$shortUrl['tags'][] = $tag->getName();
|
||||||
|
}
|
||||||
|
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
|
||||||
|
} else {
|
||||||
|
unset($shortUrl['tags']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$table->addRow(array_values($shortUrl));
|
||||||
}
|
}
|
||||||
$table->render();
|
$table->render();
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@ namespace Shlinkio\Shlink\CLI\Factory;
|
||||||
|
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
use Interop\Container\Exception\ContainerException;
|
use Interop\Container\Exception\ContainerException;
|
||||||
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
use Symfony\Component\Console\Application as CliApp;
|
use Symfony\Component\Console\Application as CliApp;
|
||||||
|
use Zend\I18n\Translator\Translator;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||||
|
@ -25,9 +27,12 @@ class ApplicationFactory implements FactoryInterface
|
||||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||||
{
|
{
|
||||||
$config = $container->get('config')['cli'];
|
$config = $container->get('config')['cli'];
|
||||||
$app = new CliApp('Shlink', '1.0.0');
|
$appOptions = $container->get(AppOptions::class);
|
||||||
|
$translator = $container->get(Translator::class);
|
||||||
|
$translator->setLocale($config['locale']);
|
||||||
|
|
||||||
$commands = isset($config['commands']) ? $config['commands'] : [];
|
$commands = isset($config['commands']) ? $config['commands'] : [];
|
||||||
|
$app = new CliApp($appOptions->getName(), $appOptions->getVersion());
|
||||||
foreach ($commands as $command) {
|
foreach ($commands as $command) {
|
||||||
if (! $container->has($command)) {
|
if (! $container->has($command)) {
|
||||||
continue;
|
continue;
|
||||||
|
|
104
module/CLI/test/Command/Install/InstallCommandTest.php
Normal file
104
module/CLI/test/Command/Install/InstallCommandTest.php
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Install;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Install\InstallCommand;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Helper\ProcessHelper;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Zend\Config\Writer\WriterInterface;
|
||||||
|
|
||||||
|
class InstallCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var CommandTester
|
||||||
|
*/
|
||||||
|
protected $commandTester;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $configWriter;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$processMock = $this->prophesize(Process::class);
|
||||||
|
$processMock->isSuccessful()->willReturn(true);
|
||||||
|
$processHelper = $this->prophesize(ProcessHelper::class);
|
||||||
|
$processHelper->getName()->willReturn('process');
|
||||||
|
$processHelper->setHelperSet(Argument::any())->willReturn(null);
|
||||||
|
$processHelper->run(Argument::cetera())->willReturn($processMock->reveal());
|
||||||
|
|
||||||
|
$app = new Application();
|
||||||
|
$helperSet = $app->getHelperSet();
|
||||||
|
$helperSet->set($processHelper->reveal());
|
||||||
|
$app->setHelperSet($helperSet);
|
||||||
|
|
||||||
|
$this->configWriter = $this->prophesize(WriterInterface::class);
|
||||||
|
$command = new InstallCommand($this->configWriter->reveal());
|
||||||
|
$app->add($command);
|
||||||
|
|
||||||
|
$questionHelper = $command->getHelper('question');
|
||||||
|
$questionHelper->setInputStream($this->createInputStream());
|
||||||
|
$this->commandTester = new CommandTester($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createInputStream()
|
||||||
|
{
|
||||||
|
$stream = fopen('php://memory', 'r+', false);
|
||||||
|
fputs($stream, <<<CLI_INPUT
|
||||||
|
|
||||||
|
shlink_db
|
||||||
|
alejandro
|
||||||
|
1234
|
||||||
|
0
|
||||||
|
doma.in
|
||||||
|
abc123BCA
|
||||||
|
|
||||||
|
1
|
||||||
|
my_secret
|
||||||
|
CLI_INPUT
|
||||||
|
);
|
||||||
|
rewind($stream);
|
||||||
|
|
||||||
|
return $stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function testInputIsProperlyParsed()
|
||||||
|
{
|
||||||
|
$this->configWriter->toFile(Argument::any(), [
|
||||||
|
'app_options' => [
|
||||||
|
'secret_key' => 'my_secret',
|
||||||
|
],
|
||||||
|
'entity_manager' => [
|
||||||
|
'connection' => [
|
||||||
|
'driver' => 'pdo_mysql',
|
||||||
|
'dbname' => 'shlink_db',
|
||||||
|
'user' => 'alejandro',
|
||||||
|
'password' => '1234',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'translator' => [
|
||||||
|
'locale' => 'en',
|
||||||
|
],
|
||||||
|
'cli' => [
|
||||||
|
'locale' => 'es',
|
||||||
|
],
|
||||||
|
'url_shortener' => [
|
||||||
|
'domain' => [
|
||||||
|
'schema' => 'http',
|
||||||
|
'hostname' => 'doma.in',
|
||||||
|
],
|
||||||
|
'shortcode_chars' => 'abc123BCA',
|
||||||
|
],
|
||||||
|
], false)->shouldBeCalledTimes(1);
|
||||||
|
$this->commandTester->execute([
|
||||||
|
'command' => 'shlink:install',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\CLI\Command\Shortcode\GeneratePreviewCommand;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||||
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Tester\CommandTester;
|
||||||
|
use Zend\I18n\Translator\Translator;
|
||||||
|
use Zend\Paginator\Adapter\ArrayAdapter;
|
||||||
|
use Zend\Paginator\Paginator;
|
||||||
|
|
||||||
|
class GeneratePreviewCommandTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var CommandTester
|
||||||
|
*/
|
||||||
|
protected $commandTester;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
private $previewGenerator;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
private $shortUrlService;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
|
||||||
|
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
|
||||||
|
|
||||||
|
$command = new GeneratePreviewCommand(
|
||||||
|
$this->shortUrlService->reveal(),
|
||||||
|
$this->previewGenerator->reveal(),
|
||||||
|
Translator::factory([])
|
||||||
|
);
|
||||||
|
$app = new Application();
|
||||||
|
$app->add($command);
|
||||||
|
|
||||||
|
$this->commandTester = new CommandTester($command);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function previewsForEveryUrlAreGenerated()
|
||||||
|
{
|
||||||
|
$paginator = $this->createPaginator([
|
||||||
|
(new ShortUrl())->setOriginalUrl('http://foo.com'),
|
||||||
|
(new ShortUrl())->setOriginalUrl('https://bar.com'),
|
||||||
|
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
|
||||||
|
]);
|
||||||
|
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$this->previewGenerator->generatePreview('http://foo.com')->shouldBeCalledTimes(1);
|
||||||
|
$this->previewGenerator->generatePreview('https://bar.com')->shouldBeCalledTimes(1);
|
||||||
|
$this->previewGenerator->generatePreview('http://baz.com/something')->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$this->commandTester->execute([
|
||||||
|
'command' => 'shortcode:process-previews'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function exceptionWillOutputError()
|
||||||
|
{
|
||||||
|
$items = [
|
||||||
|
(new ShortUrl())->setOriginalUrl('http://foo.com'),
|
||||||
|
(new ShortUrl())->setOriginalUrl('https://bar.com'),
|
||||||
|
(new ShortUrl())->setOriginalUrl('http://baz.com/something'),
|
||||||
|
];
|
||||||
|
$paginator = $this->createPaginator($items);
|
||||||
|
$this->shortUrlService->listShortUrls(1)->willReturn($paginator)->shouldBeCalledTimes(1);
|
||||||
|
$this->previewGenerator->generatePreview(Argument::any())->willThrow(PreviewGenerationException::class)
|
||||||
|
->shouldBeCalledTimes(count($items));
|
||||||
|
|
||||||
|
$this->commandTester->execute([
|
||||||
|
'command' => 'shortcode:process-previews'
|
||||||
|
]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
$this->assertEquals(count($items), substr_count($output, 'Error'));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createPaginator(array $items)
|
||||||
|
{
|
||||||
|
$paginator = new Paginator(new ArrayAdapter($items));
|
||||||
|
$paginator->setItemCountPerPage(count($items));
|
||||||
|
|
||||||
|
return $paginator;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command;
|
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
@ -39,8 +39,8 @@ class GenerateShortcodeCommandTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function properShortCodeIsCreatedIfLongUrlIsCorrect()
|
public function properShortCodeIsCreatedIfLongUrlIsCorrect()
|
||||||
{
|
{
|
||||||
$this->urlShortener->urlToShortCode(Argument::any())->willReturn('abc123')
|
$this->urlShortener->urlToShortCode(Argument::cetera())->willReturn('abc123')
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:generate',
|
'command' => 'shortcode:generate',
|
||||||
|
@ -55,8 +55,8 @@ class GenerateShortcodeCommandTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function exceptionWhileParsingLongUrlOutputsError()
|
public function exceptionWhileParsingLongUrlOutputsError()
|
||||||
{
|
{
|
||||||
$this->urlShortener->urlToShortCode(Argument::any())->willThrow(new InvalidUrlException())
|
$this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException())
|
||||||
->shouldBeCalledTimes(1);
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$this->commandTester->execute([
|
$this->commandTester->execute([
|
||||||
'command' => 'shortcode:generate',
|
'command' => 'shortcode:generate',
|
|
@ -1,5 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command;
|
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
|
@ -1,5 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command;
|
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
|
@ -108,6 +108,23 @@ class ListShortcodesCommandTest extends TestCase
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function ifTagsFlagIsProvidedTagsColumnIsIncluded()
|
||||||
|
{
|
||||||
|
$this->questionHelper->setInputStream($this->getInputStream('\n'));
|
||||||
|
$this->shortUrlService->listShortUrls(1)->willReturn(new Paginator(new ArrayAdapter()))
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$this->commandTester->execute([
|
||||||
|
'command' => 'shortcode:list',
|
||||||
|
'--tags' => true,
|
||||||
|
]);
|
||||||
|
$output = $this->commandTester->getDisplay();
|
||||||
|
$this->assertTrue(strpos($output, 'Tags') > 0);
|
||||||
|
}
|
||||||
|
|
||||||
protected function getInputStream($inputData)
|
protected function getInputStream($inputData)
|
||||||
{
|
{
|
||||||
$stream = fopen('php://memory', 'r+', false);
|
$stream = fopen('php://memory', 'r+', false);
|
|
@ -1,5 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command;
|
namespace ShlinkioTest\Shlink\CLI\Command\Shortcode;
|
||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
|
@ -1,5 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
namespace ShlinkioTest\Shlink\CLI\Command;
|
namespace ShlinkioTest\Shlink\CLI\Command\Visit;
|
||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
|
@ -3,8 +3,10 @@ namespace ShlinkioTest\Shlink\CLI\Factory;
|
||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
use Shlinkio\Shlink\CLI\Factory\ApplicationFactory;
|
||||||
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
use Symfony\Component\Console\Application;
|
use Symfony\Component\Console\Application;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Zend\I18n\Translator\Translator;
|
||||||
use Zend\ServiceManager\ServiceManager;
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
class ApplicationFactoryTest extends TestCase
|
class ApplicationFactoryTest extends TestCase
|
||||||
|
@ -53,8 +55,10 @@ class ApplicationFactoryTest extends TestCase
|
||||||
{
|
{
|
||||||
return new ServiceManager(['services' => [
|
return new ServiceManager(['services' => [
|
||||||
'config' => [
|
'config' => [
|
||||||
'cli' => $config,
|
'cli' => array_merge($config, ['locale' => 'en']),
|
||||||
],
|
],
|
||||||
|
AppOptions::class => new AppOptions(),
|
||||||
|
Translator::class => Translator::factory([]),
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,43 +4,44 @@ use Doctrine\Common\Cache\Cache;
|
||||||
use Doctrine\ORM\EntityManager;
|
use Doctrine\ORM\EntityManager;
|
||||||
use Monolog\Logger;
|
use Monolog\Logger;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler;
|
use Shlinkio\Shlink\Common\Factory;
|
||||||
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
use Shlinkio\Shlink\Common\Image;
|
||||||
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
|
|
||||||
use Shlinkio\Shlink\Common\Factory\LoggerFactory;
|
|
||||||
use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
|
|
||||||
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
||||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
use Shlinkio\Shlink\Common\Service;
|
||||||
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
|
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
use Zend\I18n\Translator\Translator;
|
use Zend\I18n\Translator\Translator;
|
||||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
|
'invokables' => [
|
||||||
|
Filesystem::class => Filesystem::class,
|
||||||
|
],
|
||||||
'factories' => [
|
'factories' => [
|
||||||
EntityManager::class => EntityManagerFactory::class,
|
EntityManager::class => Factory\EntityManagerFactory::class,
|
||||||
GuzzleHttp\Client::class => InvokableFactory::class,
|
GuzzleHttp\Client::class => InvokableFactory::class,
|
||||||
Cache::class => CacheFactory::class,
|
Cache::class => Factory\CacheFactory::class,
|
||||||
LoggerInterface::class => LoggerFactory::class,
|
'Logger_Shlink' => Factory\LoggerFactory::class,
|
||||||
'Logger_Shlink' => LoggerFactory::class,
|
|
||||||
|
|
||||||
Translator::class => TranslatorFactory::class,
|
Translator::class => Factory\TranslatorFactory::class,
|
||||||
TranslatorExtension::class => AnnotatedFactory::class,
|
TranslatorExtension::class => AnnotatedFactory::class,
|
||||||
LocaleMiddleware::class => AnnotatedFactory::class,
|
LocaleMiddleware::class => AnnotatedFactory::class,
|
||||||
|
|
||||||
IpLocationResolver::class => AnnotatedFactory::class,
|
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
||||||
|
|
||||||
ErrorHandler\ContentBasedErrorHandler::class => AnnotatedFactory::class,
|
Service\IpLocationResolver::class => AnnotatedFactory::class,
|
||||||
ErrorHandler\ErrorHandlerManager::class => ErrorHandler\ErrorHandlerManagerFactory::class,
|
Service\PreviewGenerator::class => AnnotatedFactory::class,
|
||||||
],
|
],
|
||||||
'aliases' => [
|
'aliases' => [
|
||||||
'em' => EntityManager::class,
|
'em' => EntityManager::class,
|
||||||
'httpClient' => GuzzleHttp\Client::class,
|
'httpClient' => GuzzleHttp\Client::class,
|
||||||
'translator' => Translator::class,
|
'translator' => Translator::class,
|
||||||
'logger' => LoggerInterface::class,
|
'logger' => LoggerInterface::class,
|
||||||
Logger::class => LoggerInterface::class,
|
|
||||||
AnnotatedFactory::CACHE_SERVICE => Cache::class,
|
AnnotatedFactory::CACHE_SERVICE => Cache::class,
|
||||||
|
Logger::class => 'Logger_Shlink',
|
||||||
|
LoggerInterface::class => 'Logger_Shlink',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
<?php
|
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
|
|
||||||
use Zend\Expressive\Container\TemplatedErrorHandlerFactory;
|
|
||||||
use Zend\Stratigility\FinalHandler;
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
'error_handler' => [
|
|
||||||
'plugins' => [
|
|
||||||
'invokables' => [
|
|
||||||
'text/plain' => FinalHandler::class,
|
|
||||||
],
|
|
||||||
'factories' => [
|
|
||||||
ContentBasedErrorHandler::DEFAULT_CONTENT => TemplatedErrorHandlerFactory::class,
|
|
||||||
],
|
|
||||||
'aliases' => [
|
|
||||||
'application/xhtml+xml' => ContentBasedErrorHandler::DEFAULT_CONTENT,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
|
@ -1,76 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Shlinkio\Shlink\Common\ErrorHandler;
|
|
||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
||||||
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
|
|
||||||
|
|
||||||
class ContentBasedErrorHandler implements ErrorHandlerInterface
|
|
||||||
{
|
|
||||||
const DEFAULT_CONTENT = 'text/html';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var ErrorHandlerManagerInterface
|
|
||||||
*/
|
|
||||||
private $errorHandlerManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ContentBasedErrorHandler constructor.
|
|
||||||
* @param ErrorHandlerManagerInterface|ErrorHandlerManager $errorHandlerManager
|
|
||||||
*
|
|
||||||
* @Inject({ErrorHandlerManager::class})
|
|
||||||
*/
|
|
||||||
public function __construct(ErrorHandlerManagerInterface $errorHandlerManager)
|
|
||||||
{
|
|
||||||
$this->errorHandlerManager = $errorHandlerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Final handler for an application.
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @param Response $response
|
|
||||||
* @param null|mixed $err
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request, Response $response, $err = null)
|
|
||||||
{
|
|
||||||
// Try to get an error handler for provided request accepted type
|
|
||||||
$errorHandler = $this->resolveErrorHandlerFromAcceptHeader($request);
|
|
||||||
return $errorHandler($request, $response, $err);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tries to resolve
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @return callable
|
|
||||||
*/
|
|
||||||
protected function resolveErrorHandlerFromAcceptHeader(Request $request)
|
|
||||||
{
|
|
||||||
// Try to find an error handler for one of the accepted content types
|
|
||||||
$accepts = $request->hasHeader('Accept') ? $request->getHeaderLine('Accept') : self::DEFAULT_CONTENT;
|
|
||||||
$accepts = explode(',', $accepts);
|
|
||||||
foreach ($accepts as $accept) {
|
|
||||||
if (! $this->errorHandlerManager->has($accept)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->errorHandlerManager->get($accept);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it wasn't possible to find an error handler for accepted content type, use default one if registered
|
|
||||||
if ($this->errorHandlerManager->has(self::DEFAULT_CONTENT)) {
|
|
||||||
return $this->errorHandlerManager->get(self::DEFAULT_CONTENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
// It wasn't possible to find an error handler
|
|
||||||
throw new InvalidArgumentException(sprintf(
|
|
||||||
'It wasn\'t possible to find an error handler for ["%s"] content types. '
|
|
||||||
. 'Make sure you have registered at least the default "%s" content type',
|
|
||||||
implode('", "', $accepts),
|
|
||||||
self::DEFAULT_CONTENT
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Shlinkio\Shlink\Common\ErrorHandler;
|
|
||||||
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
|
||||||
|
|
||||||
interface ErrorHandlerInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Final handler for an application.
|
|
||||||
*
|
|
||||||
* @param Request $request
|
|
||||||
* @param Response $response
|
|
||||||
* @param null|mixed $err
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function __invoke(Request $request, Response $response, $err = null);
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Shlinkio\Shlink\Common\ErrorHandler;
|
|
||||||
|
|
||||||
use Zend\ServiceManager\AbstractPluginManager;
|
|
||||||
use Zend\ServiceManager\Exception\InvalidServiceException;
|
|
||||||
|
|
||||||
class ErrorHandlerManager extends AbstractPluginManager implements ErrorHandlerManagerInterface
|
|
||||||
{
|
|
||||||
public function validate($instance)
|
|
||||||
{
|
|
||||||
if (is_callable($instance)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidServiceException(sprintf(
|
|
||||||
'Only callables are valid plugins for "%s". "%s" provided',
|
|
||||||
__CLASS__,
|
|
||||||
is_object($instance) ? get_class($instance) : gettype($instance)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Shlinkio\Shlink\Common\ErrorHandler;
|
|
||||||
|
|
||||||
use Interop\Container\ContainerInterface;
|
|
||||||
|
|
||||||
interface ErrorHandlerManagerInterface extends ContainerInterface
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
10
module/Common/src/Exception/PreviewGenerationException.php
Normal file
10
module/Common/src/Exception/PreviewGenerationException.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Common\Exception;
|
||||||
|
|
||||||
|
class PreviewGenerationException extends RuntimeException
|
||||||
|
{
|
||||||
|
public static function fromImageError($error)
|
||||||
|
{
|
||||||
|
return new self(sprintf('Error generating a preview image with error: %s', $error));
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ namespace Shlinkio\Shlink\Common\Factory;
|
||||||
use Doctrine\Common\Cache;
|
use Doctrine\Common\Cache;
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
use Interop\Container\Exception\ContainerException;
|
use Interop\Container\Exception\ContainerException;
|
||||||
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||||
|
@ -31,6 +32,19 @@ class CacheFactory implements FactoryInterface
|
||||||
* @throws ContainerException if any other error occurs
|
* @throws ContainerException if any other error occurs
|
||||||
*/
|
*/
|
||||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||||
|
{
|
||||||
|
$appOptions = $container->get(AppOptions::class);
|
||||||
|
$adapter = $this->getAdapter($container);
|
||||||
|
$adapter->setNamespace($appOptions->__toString());
|
||||||
|
|
||||||
|
return $adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ContainerInterface $container
|
||||||
|
* @return Cache\CacheProvider
|
||||||
|
*/
|
||||||
|
protected function getAdapter(ContainerInterface $container)
|
||||||
{
|
{
|
||||||
// Try to get the adapter from config
|
// Try to get the adapter from config
|
||||||
$config = $container->get('config');
|
$config = $container->get('config');
|
||||||
|
@ -47,7 +61,7 @@ class CacheFactory implements FactoryInterface
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $cacheConfig
|
* @param array $cacheConfig
|
||||||
* @return Cache\Cache
|
* @return Cache\CacheProvider
|
||||||
*/
|
*/
|
||||||
protected function resolveCacheAdapter(array $cacheConfig)
|
protected function resolveCacheAdapter(array $cacheConfig)
|
||||||
{
|
{
|
||||||
|
|
10
module/Common/src/Image/ImageBuilder.php
Normal file
10
module/Common/src/Image/ImageBuilder.php
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Common\Image;
|
||||||
|
|
||||||
|
use mikehaertl\wkhtmlto\Image;
|
||||||
|
use Zend\ServiceManager\AbstractPluginManager;
|
||||||
|
|
||||||
|
class ImageBuilder extends AbstractPluginManager implements ImageBuilderInterface
|
||||||
|
{
|
||||||
|
protected $instanceOf = Image::class;
|
||||||
|
}
|
|
@ -1,13 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Shlinkio\Shlink\Common\ErrorHandler;
|
namespace Shlinkio\Shlink\Common\Image;
|
||||||
|
|
||||||
use Interop\Container\ContainerInterface;
|
use Interop\Container\ContainerInterface;
|
||||||
use Interop\Container\Exception\ContainerException;
|
use Interop\Container\Exception\ContainerException;
|
||||||
|
use mikehaertl\wkhtmlto\Image;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
use Zend\ServiceManager\Factory\FactoryInterface;
|
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||||
|
|
||||||
class ErrorHandlerManagerFactory implements FactoryInterface
|
class ImageBuilderFactory implements FactoryInterface
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Create an object
|
* Create an object
|
||||||
|
@ -23,8 +24,8 @@ class ErrorHandlerManagerFactory implements FactoryInterface
|
||||||
*/
|
*/
|
||||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||||
{
|
{
|
||||||
$config = $container->get('config')['error_handler'];
|
return new ImageBuilder($container, ['factories' => [
|
||||||
$plugins = isset($config['plugins']) ? $config['plugins'] : [];
|
Image::class => ImageFactory::class,
|
||||||
return new ErrorHandlerManager($container, $plugins);
|
]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
8
module/Common/src/Image/ImageBuilderInterface.php
Normal file
8
module/Common/src/Image/ImageBuilderInterface.php
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Common\Image;
|
||||||
|
|
||||||
|
use Zend\ServiceManager\ServiceLocatorInterface;
|
||||||
|
|
||||||
|
interface ImageBuilderInterface extends ServiceLocatorInterface
|
||||||
|
{
|
||||||
|
}
|
36
module/Common/src/Image/ImageFactory.php
Normal file
36
module/Common/src/Image/ImageFactory.php
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Common\Image;
|
||||||
|
|
||||||
|
use Interop\Container\ContainerInterface;
|
||||||
|
use Interop\Container\Exception\ContainerException;
|
||||||
|
use mikehaertl\wkhtmlto\Image;
|
||||||
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
|
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||||
|
|
||||||
|
class ImageFactory implements FactoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create an object
|
||||||
|
*
|
||||||
|
* @param ContainerInterface $container
|
||||||
|
* @param string $requestedName
|
||||||
|
* @param null|array $options
|
||||||
|
* @return object
|
||||||
|
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||||
|
* @throws ServiceNotCreatedException if an exception is raised when
|
||||||
|
* creating a service.
|
||||||
|
* @throws ContainerException if any other error occurs
|
||||||
|
*/
|
||||||
|
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||||
|
{
|
||||||
|
$config = $container->get('config')['phpwkhtmltopdf'];
|
||||||
|
$image = new Image(isset($config['images']) ? $config['images'] : null);
|
||||||
|
|
||||||
|
if (isset($options) && isset($options['url'])) {
|
||||||
|
$image->setPage($options['url']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $image;
|
||||||
|
}
|
||||||
|
}
|
70
module/Common/src/Service/PreviewGenerator.php
Normal file
70
module/Common/src/Service/PreviewGenerator.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Common\Service;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use mikehaertl\wkhtmlto\Image;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||||
|
use Shlinkio\Shlink\Common\Image\ImageBuilder;
|
||||||
|
use Shlinkio\Shlink\Common\Image\ImageBuilderInterface;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
|
||||||
|
class PreviewGenerator implements PreviewGeneratorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $location;
|
||||||
|
/**
|
||||||
|
* @var ImageBuilderInterface
|
||||||
|
*/
|
||||||
|
private $imageBuilder;
|
||||||
|
/**
|
||||||
|
* @var Filesystem
|
||||||
|
*/
|
||||||
|
private $filesystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PreviewGenerator constructor.
|
||||||
|
* @param ImageBuilderInterface $imageBuilder
|
||||||
|
* @param Filesystem $filesystem
|
||||||
|
* @param string $location
|
||||||
|
*
|
||||||
|
* @Inject({ImageBuilder::class, Filesystem::class, "config.preview_generation.files_location"})
|
||||||
|
*/
|
||||||
|
public function __construct(ImageBuilderInterface $imageBuilder, Filesystem $filesystem, $location)
|
||||||
|
{
|
||||||
|
$this->location = $location;
|
||||||
|
$this->imageBuilder = $imageBuilder;
|
||||||
|
$this->filesystem = $filesystem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates and stores preview for provided website and returns the path to the image file
|
||||||
|
*
|
||||||
|
* @param string $url
|
||||||
|
* @return string
|
||||||
|
* @throws PreviewGenerationException
|
||||||
|
*/
|
||||||
|
public function generatePreview($url)
|
||||||
|
{
|
||||||
|
/** @var Image $image */
|
||||||
|
$image = $this->imageBuilder->build(Image::class, ['url' => $url]);
|
||||||
|
|
||||||
|
// If the file already exists, return its path
|
||||||
|
$cacheId = sprintf('preview_%s.%s', urlencode($url), $image->type);
|
||||||
|
$path = $this->location . '/' . $cacheId;
|
||||||
|
if ($this->filesystem->exists($path)) {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save and check if an error occurred
|
||||||
|
$image->saveAs($path);
|
||||||
|
$error = $image->getError();
|
||||||
|
if (! empty($error)) {
|
||||||
|
throw PreviewGenerationException::fromImageError($error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache the path and return it
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
}
|
16
module/Common/src/Service/PreviewGeneratorInterface.php
Normal file
16
module/Common/src/Service/PreviewGeneratorInterface.php
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Common\Service;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||||
|
|
||||||
|
interface PreviewGeneratorInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Generates and stores preview for provided website and returns the path to the image file
|
||||||
|
*
|
||||||
|
* @param string $url
|
||||||
|
* @return string
|
||||||
|
* @throws PreviewGenerationException
|
||||||
|
*/
|
||||||
|
public function generatePreview($url);
|
||||||
|
}
|
35
module/Common/src/Util/ResponseUtilsTrait.php
Normal file
35
module/Common/src/Util/ResponseUtilsTrait.php
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Common\Util;
|
||||||
|
|
||||||
|
use Zend\Diactoros\Response;
|
||||||
|
use Zend\Diactoros\Stream;
|
||||||
|
use Zend\Stdlib\ArrayUtils;
|
||||||
|
|
||||||
|
trait ResponseUtilsTrait
|
||||||
|
{
|
||||||
|
protected function generateDownloadFileResponse($filePath)
|
||||||
|
{
|
||||||
|
return $this->generateBinaryResponse($filePath, [
|
||||||
|
'Content-Disposition' => 'attachment; filename=' . basename($filePath),
|
||||||
|
'Content-Transfer-Encoding' => 'Binary',
|
||||||
|
'Content-Description' => 'File Transfer',
|
||||||
|
'Pragma' => 'public',
|
||||||
|
'Expires' => '0',
|
||||||
|
'Cache-Control' => 'must-revalidate',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateImageResponse($imagePath)
|
||||||
|
{
|
||||||
|
return $this->generateBinaryResponse($imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function generateBinaryResponse($path, $extraHeaders = [])
|
||||||
|
{
|
||||||
|
$body = new Stream($path);
|
||||||
|
return new Response($body, 200, ArrayUtils::merge([
|
||||||
|
'Content-Type' => (new \finfo(FILEINFO_MIME))->file($path),
|
||||||
|
'Content-Length' => (string) $body->getSize(),
|
||||||
|
], $extraHeaders));
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,6 @@ class ConfigProviderTest extends TestCase
|
||||||
{
|
{
|
||||||
$config = $this->configProvider->__invoke();
|
$config = $this->configProvider->__invoke();
|
||||||
|
|
||||||
$this->assertArrayHasKey('error_handler', $config);
|
|
||||||
$this->assertArrayHasKey('middleware_pipeline', $config);
|
$this->assertArrayHasKey('middleware_pipeline', $config);
|
||||||
$this->assertArrayHasKey('dependencies', $config);
|
$this->assertArrayHasKey('dependencies', $config);
|
||||||
$this->assertArrayHasKey('twig', $config);
|
$this->assertArrayHasKey('twig', $config);
|
||||||
|
|
|
@ -1,75 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace ShlinkioTest\Shlink\Common\ErrorHandler;
|
|
||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler\ContentBasedErrorHandler;
|
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler\ErrorHandlerManager;
|
|
||||||
use Zend\Diactoros\Response;
|
|
||||||
use Zend\Diactoros\ServerRequestFactory;
|
|
||||||
use Zend\ServiceManager\ServiceManager;
|
|
||||||
|
|
||||||
class ContentBasedErrorHandlerTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var ContentBasedErrorHandler
|
|
||||||
*/
|
|
||||||
protected $errorHandler;
|
|
||||||
|
|
||||||
public function setUp()
|
|
||||||
{
|
|
||||||
$this->errorHandler = new ContentBasedErrorHandler(new ErrorHandlerManager(new ServiceManager(), [
|
|
||||||
'factories' => [
|
|
||||||
'text/html' => [$this, 'factory'],
|
|
||||||
'application/json' => [$this, 'factory'],
|
|
||||||
],
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function factory($container, $name)
|
|
||||||
{
|
|
||||||
return function () use ($name) {
|
|
||||||
return $name;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function correctAcceptHeaderValueInvokesErrorHandler()
|
|
||||||
{
|
|
||||||
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,application/json');
|
|
||||||
$result = $this->errorHandler->__invoke($request, new Response());
|
|
||||||
$this->assertEquals('application/json', $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function defaultContentTypeIsUsedWhenNoAcceptHeaderisPresent()
|
|
||||||
{
|
|
||||||
$request = ServerRequestFactory::fromGlobals();
|
|
||||||
$result = $this->errorHandler->__invoke($request, new Response());
|
|
||||||
$this->assertEquals('text/html', $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function defaultContentTypeIsUsedWhenAcceptedContentIsNotSupported()
|
|
||||||
{
|
|
||||||
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,text/xml');
|
|
||||||
$result = $this->errorHandler->__invoke($request, new Response());
|
|
||||||
$this->assertEquals('text/html', $result);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
* @expectedException \Shlinkio\Shlink\Common\Exception\InvalidArgumentException
|
|
||||||
*/
|
|
||||||
public function ifNoErrorHandlerIsFoundAnExceptionIsThrown()
|
|
||||||
{
|
|
||||||
$this->errorHandler = new ContentBasedErrorHandler(new ErrorHandlerManager(new ServiceManager(), []));
|
|
||||||
$request = ServerRequestFactory::fromGlobals()->withHeader('Accept', 'foo/bar,text/xml');
|
|
||||||
$result = $this->errorHandler->__invoke($request, new Response());
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace ShlinkioTest\Shlink\Common\ErrorHandler;
|
|
||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler\ErrorHandlerManager;
|
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler\ErrorHandlerManagerFactory;
|
|
||||||
use Zend\ServiceManager\ServiceManager;
|
|
||||||
|
|
||||||
class ErrorHandlerManagerFactoryTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var ErrorHandlerManagerFactory
|
|
||||||
*/
|
|
||||||
protected $factory;
|
|
||||||
|
|
||||||
public function setUp()
|
|
||||||
{
|
|
||||||
$this->factory = new ErrorHandlerManagerFactory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function serviceIsCreated()
|
|
||||||
{
|
|
||||||
$instance = $this->factory->__invoke(new ServiceManager(['services' => [
|
|
||||||
'config' => [
|
|
||||||
'error_handler' => [
|
|
||||||
'plugins' => [],
|
|
||||||
],
|
|
||||||
],
|
|
||||||
]]), '');
|
|
||||||
$this->assertInstanceOf(ErrorHandlerManager::class, $instance);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace ShlinkioTest\Shlink\Common\ErrorHandler;
|
|
||||||
|
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler\ErrorHandlerManager;
|
|
||||||
use Zend\ServiceManager\ServiceManager;
|
|
||||||
|
|
||||||
class ErrorHandlerManagerTest extends TestCase
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @var ErrorHandlerManager
|
|
||||||
*/
|
|
||||||
protected $pluginManager;
|
|
||||||
|
|
||||||
public function setUp()
|
|
||||||
{
|
|
||||||
$this->pluginManager = new ErrorHandlerManager(new ServiceManager(), [
|
|
||||||
'services' => [
|
|
||||||
'foo' => function () {
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'invokables' => [
|
|
||||||
'invalid' => \stdClass::class,
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
*/
|
|
||||||
public function callablesAreReturned()
|
|
||||||
{
|
|
||||||
$instance = $this->pluginManager->get('foo');
|
|
||||||
$this->assertInstanceOf(\Closure::class, $instance);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @test
|
|
||||||
* @expectedException \Zend\ServiceManager\Exception\InvalidServiceException
|
|
||||||
*/
|
|
||||||
public function nonCallablesThrowException()
|
|
||||||
{
|
|
||||||
$this->pluginManager->get('invalid');
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -8,6 +8,7 @@ use Doctrine\Common\Cache\MemcachedCache;
|
||||||
use Doctrine\Common\Cache\RedisCache;
|
use Doctrine\Common\Cache\RedisCache;
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
||||||
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
use Zend\ServiceManager\ServiceManager;
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
class CacheFactoryTest extends TestCase
|
class CacheFactoryTest extends TestCase
|
||||||
|
@ -109,6 +110,7 @@ class CacheFactoryTest extends TestCase
|
||||||
'options' => $options,
|
'options' => $options,
|
||||||
],
|
],
|
||||||
] : [],
|
] : [],
|
||||||
|
AppOptions::class => new AppOptions(),
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
29
module/Common/test/Image/ImageBuilderFactoryTest.php
Normal file
29
module/Common/test/Image/ImageBuilderFactoryTest.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Common\Image;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Shlinkio\Shlink\Common\Image\ImageBuilder;
|
||||||
|
use Shlinkio\Shlink\Common\Image\ImageBuilderFactory;
|
||||||
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
|
class ImageBuilderFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ImageBuilderFactory
|
||||||
|
*/
|
||||||
|
protected $factory;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->factory = new ImageBuilderFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function serviceIsCreated()
|
||||||
|
{
|
||||||
|
$instance = $this->factory->__invoke(new ServiceManager(), '');
|
||||||
|
$this->assertInstanceOf(ImageBuilder::class, $instance);
|
||||||
|
}
|
||||||
|
}
|
56
module/Common/test/Image/ImageFactoryTest.php
Normal file
56
module/Common/test/Image/ImageFactoryTest.php
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Common\Image;
|
||||||
|
|
||||||
|
use mikehaertl\wkhtmlto\Image;
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Shlinkio\Shlink\Common\Image\ImageFactory;
|
||||||
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
|
class ImageFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ImageFactory
|
||||||
|
*/
|
||||||
|
protected $factory;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->factory = new ImageFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function noPageIsSetWhenOptionsAreNotProvided()
|
||||||
|
{
|
||||||
|
/** @var Image $image */
|
||||||
|
$image = $this->factory->__invoke(new ServiceManager(['services' => [
|
||||||
|
'config' => ['phpwkhtmltopdf' => []],
|
||||||
|
]]), '');
|
||||||
|
$this->assertInstanceOf(Image::class, $image);
|
||||||
|
|
||||||
|
$ref = new \ReflectionObject($image);
|
||||||
|
$page = $ref->getProperty('_page');
|
||||||
|
$page->setAccessible(true);
|
||||||
|
$this->assertNull($page->getValue($image));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function aPageIsSetWhenOptionsIncludeTheUrl()
|
||||||
|
{
|
||||||
|
$expectedPage = 'foo/bar.html';
|
||||||
|
|
||||||
|
/** @var Image $image */
|
||||||
|
$image = $this->factory->__invoke(new ServiceManager(['services' => [
|
||||||
|
'config' => ['phpwkhtmltopdf' => []],
|
||||||
|
]]), '', ['url' => $expectedPage]);
|
||||||
|
$this->assertInstanceOf(Image::class, $image);
|
||||||
|
|
||||||
|
$ref = new \ReflectionObject($image);
|
||||||
|
$page = $ref->getProperty('_page');
|
||||||
|
$page->setAccessible(true);
|
||||||
|
$this->assertEquals($expectedPage, $page->getValue($image));
|
||||||
|
}
|
||||||
|
}
|
89
module/Common/test/Service/PreviewGeneratorTest.php
Normal file
89
module/Common/test/Service/PreviewGeneratorTest.php
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Common\Service;
|
||||||
|
|
||||||
|
use mikehaertl\wkhtmlto\Image;
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\Common\Image\ImageBuilder;
|
||||||
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
|
use Symfony\Component\Filesystem\Filesystem;
|
||||||
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
|
class PreviewGeneratorTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PreviewGenerator
|
||||||
|
*/
|
||||||
|
protected $generator;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $image;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
protected $filesystem;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->image = $this->prophesize(Image::class);
|
||||||
|
$this->filesystem = $this->prophesize(Filesystem::class);
|
||||||
|
|
||||||
|
$this->generator = new PreviewGenerator(new ImageBuilder(new ServiceManager(), [
|
||||||
|
'factories' => [
|
||||||
|
Image::class => function () {
|
||||||
|
return $this->image->reveal();
|
||||||
|
},
|
||||||
|
]
|
||||||
|
]), $this->filesystem->reveal(), 'dir');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function alreadyProcessedElementsAreNotProcessed()
|
||||||
|
{
|
||||||
|
$url = 'http://foo.com';
|
||||||
|
$this->filesystem->exists(sprintf('dir/preview_%s.png', urlencode($url)))->willReturn(true)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->image->saveAs(Argument::cetera())->shouldBeCalledTimes(0);
|
||||||
|
$this->assertEquals(sprintf('dir/preview_%s.png', urlencode($url)), $this->generator->generatePreview($url));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function nonProcessedElementsAreProcessed()
|
||||||
|
{
|
||||||
|
$url = 'http://foo.com';
|
||||||
|
$cacheId = sprintf('preview_%s.png', urlencode($url));
|
||||||
|
$expectedPath = 'dir/' . $cacheId;
|
||||||
|
|
||||||
|
$this->filesystem->exists(sprintf('dir/preview_%s.png', urlencode($url)))->willReturn(false)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$this->image->saveAs($expectedPath)->shouldBeCalledTimes(1);
|
||||||
|
$this->image->getError()->willReturn('')->shouldBeCalledTimes(1);
|
||||||
|
$this->assertEquals($expectedPath, $this->generator->generatePreview($url));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @expectedException \Shlinkio\Shlink\Common\Exception\PreviewGenerationException
|
||||||
|
*/
|
||||||
|
public function errorWhileGeneratingPreviewThrowsException()
|
||||||
|
{
|
||||||
|
$url = 'http://foo.com';
|
||||||
|
$cacheId = sprintf('preview_%s.png', urlencode($url));
|
||||||
|
$expectedPath = 'dir/' . $cacheId;
|
||||||
|
|
||||||
|
$this->filesystem->exists(sprintf('dir/preview_%s.png', urlencode($url)))->willReturn(false)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$this->image->saveAs($expectedPath)->shouldBeCalledTimes(1);
|
||||||
|
$this->image->getError()->willReturn('Error!!')->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$this->generator->generatePreview($url);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,14 +2,14 @@
|
||||||
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
use Acelaya\ZsmAnnotatedServices\Factory\V3\AnnotatedFactory;
|
||||||
use Shlinkio\Shlink\Core\Action;
|
use Shlinkio\Shlink\Core\Action;
|
||||||
use Shlinkio\Shlink\Core\Middleware;
|
use Shlinkio\Shlink\Core\Middleware;
|
||||||
use Shlinkio\Shlink\Core\Options\AppOptions;
|
use Shlinkio\Shlink\Core\Options;
|
||||||
use Shlinkio\Shlink\Core\Service;
|
use Shlinkio\Shlink\Core\Service;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
||||||
'dependencies' => [
|
'dependencies' => [
|
||||||
'factories' => [
|
'factories' => [
|
||||||
AppOptions::class => AnnotatedFactory::class,
|
Options\AppOptions::class => Options\AppOptionsFactory::class,
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
Service\UrlShortener::class => AnnotatedFactory::class,
|
Service\UrlShortener::class => AnnotatedFactory::class,
|
||||||
|
@ -20,6 +20,7 @@ return [
|
||||||
// Middleware
|
// Middleware
|
||||||
Action\RedirectAction::class => AnnotatedFactory::class,
|
Action\RedirectAction::class => AnnotatedFactory::class,
|
||||||
Action\QrCodeAction::class => AnnotatedFactory::class,
|
Action\QrCodeAction::class => AnnotatedFactory::class,
|
||||||
|
Action\PreviewAction::class => AnnotatedFactory::class,
|
||||||
Middleware\QrCodeCacheMiddleware::class => AnnotatedFactory::class,
|
Middleware\QrCodeCacheMiddleware::class => AnnotatedFactory::class,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|
|
@ -13,6 +13,23 @@ return [
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'name' => 'short-url-qr-code',
|
'name' => 'short-url-qr-code',
|
||||||
|
'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]',
|
||||||
|
'middleware' => [
|
||||||
|
Middleware\QrCodeCacheMiddleware::class,
|
||||||
|
Action\QrCodeAction::class,
|
||||||
|
],
|
||||||
|
'allowed_methods' => ['GET'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'name' => 'short-url-preview',
|
||||||
|
'path' => '/{shortCode}/preview',
|
||||||
|
'middleware' => Action\PreviewAction::class,
|
||||||
|
'allowed_methods' => ['GET'],
|
||||||
|
],
|
||||||
|
|
||||||
|
// Old QR code route. Deprecated
|
||||||
|
[
|
||||||
|
'name' => 'short-url-qr-code-old',
|
||||||
'path' => '/qr/{shortCode}[/{size:[0-9]+}]',
|
'path' => '/qr/{shortCode}[/{size:[0-9]+}]',
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
Middleware\QrCodeCacheMiddleware::class,
|
Middleware\QrCodeCacheMiddleware::class,
|
||||||
|
|
Binary file not shown.
|
@ -1,9 +1,9 @@
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Shlink 1.0\n"
|
"Project-Id-Version: Shlink 1.0\n"
|
||||||
"POT-Creation-Date: 2016-07-21 16:50+0200\n"
|
"POT-Creation-Date: 2016-08-21 18:17+0200\n"
|
||||||
"PO-Revision-Date: 2016-07-21 16:51+0200\n"
|
"PO-Revision-Date: 2016-08-21 18:17+0200\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
|
|
85
module/Core/src/Action/PreviewAction.php
Normal file
85
module/Core/src/Action/PreviewAction.php
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Core\Action;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||||
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
|
use Shlinkio\Shlink\Common\Service\PreviewGeneratorInterface;
|
||||||
|
use Shlinkio\Shlink\Common\Util\ResponseUtilsTrait;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
|
use Shlinkio\Shlink\Core\Service\UrlShortenerInterface;
|
||||||
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
|
||||||
|
class PreviewAction implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
use ResponseUtilsTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var PreviewGeneratorInterface
|
||||||
|
*/
|
||||||
|
private $previewGenerator;
|
||||||
|
/**
|
||||||
|
* @var UrlShortenerInterface
|
||||||
|
*/
|
||||||
|
private $urlShortener;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PreviewAction constructor.
|
||||||
|
* @param PreviewGeneratorInterface $previewGenerator
|
||||||
|
* @param UrlShortenerInterface $urlShortener
|
||||||
|
*
|
||||||
|
* @Inject({PreviewGenerator::class, UrlShortener::class})
|
||||||
|
*/
|
||||||
|
public function __construct(PreviewGeneratorInterface $previewGenerator, UrlShortenerInterface $urlShortener)
|
||||||
|
{
|
||||||
|
$this->previewGenerator = $previewGenerator;
|
||||||
|
$this->urlShortener = $urlShortener;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process an incoming request and/or response.
|
||||||
|
*
|
||||||
|
* Accepts a server-side request and a response instance, and does
|
||||||
|
* something with them.
|
||||||
|
*
|
||||||
|
* If the response is not complete and/or further processing would not
|
||||||
|
* interfere with the work done in the middleware, or if the middleware
|
||||||
|
* wants to delegate to another process, it can use the `$out` callable
|
||||||
|
* if present.
|
||||||
|
*
|
||||||
|
* If the middleware does not return a value, execution of the current
|
||||||
|
* request is considered complete, and the response instance provided will
|
||||||
|
* be considered the response to return.
|
||||||
|
*
|
||||||
|
* Alternately, the middleware may return a response instance.
|
||||||
|
*
|
||||||
|
* Often, middleware will `return $out();`, with the assumption that a
|
||||||
|
* later middleware will return a response.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param null|callable $out
|
||||||
|
* @return null|Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
|
{
|
||||||
|
$shortCode = $request->getAttribute('shortCode');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$url = $this->urlShortener->shortCodeToUrl($shortCode);
|
||||||
|
if (! isset($url)) {
|
||||||
|
return $out($request, $response->withStatus(404), 'Not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$imagePath = $this->previewGenerator->generatePreview($url);
|
||||||
|
return $this->generateImageResponse($imagePath);
|
||||||
|
} catch (InvalidShortCodeException $e) {
|
||||||
|
return $out($request, $response->withStatus(404), 'Not found');
|
||||||
|
} catch (PreviewGenerationException $e) {
|
||||||
|
return $out($request, $response->withStatus(500), 'Preview generation error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -28,8 +28,7 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
|
||||||
* type="string",
|
* type="string",
|
||||||
* nullable=false,
|
* nullable=false,
|
||||||
* length=10,
|
* length=10,
|
||||||
* unique=true,
|
* unique=true
|
||||||
* options={"collation": "utf8_bin"}
|
|
||||||
* )
|
* )
|
||||||
*/
|
*/
|
||||||
protected $shortCode;
|
protected $shortCode;
|
||||||
|
@ -43,6 +42,16 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
|
||||||
* @ORM\OneToMany(targetEntity=Visit::class, mappedBy="shortUrl", fetch="EXTRA_LAZY")
|
* @ORM\OneToMany(targetEntity=Visit::class, mappedBy="shortUrl", fetch="EXTRA_LAZY")
|
||||||
*/
|
*/
|
||||||
protected $visits;
|
protected $visits;
|
||||||
|
/**
|
||||||
|
* @var Collection|Tag[]
|
||||||
|
* @ORM\ManyToMany(targetEntity=Tag::class, cascade={"persist"})
|
||||||
|
* @ORM\JoinTable(name="short_urls_in_tags", joinColumns={
|
||||||
|
* @ORM\JoinColumn(name="short_url_id", referencedColumnName="id")
|
||||||
|
* }, inverseJoinColumns={
|
||||||
|
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
protected $tags;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ShortUrl constructor.
|
* ShortUrl constructor.
|
||||||
|
@ -52,6 +61,7 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
|
||||||
$this->setDateCreated(new \DateTime());
|
$this->setDateCreated(new \DateTime());
|
||||||
$this->setVisits(new ArrayCollection());
|
$this->setVisits(new ArrayCollection());
|
||||||
$this->setShortCode('');
|
$this->setShortCode('');
|
||||||
|
$this->tags = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -126,6 +136,34 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection|Tag[]
|
||||||
|
*/
|
||||||
|
public function getTags()
|
||||||
|
{
|
||||||
|
return $this->tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Collection|Tag[] $tags
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setTags($tags)
|
||||||
|
{
|
||||||
|
$this->tags = $tags;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Tag $tag
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function addTag(Tag $tag)
|
||||||
|
{
|
||||||
|
$this->tags->add($tag);
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Specify data which should be serialized to JSON
|
* Specify data which should be serialized to JSON
|
||||||
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
|
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
|
||||||
|
@ -140,6 +178,7 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable
|
||||||
'originalUrl' => $this->originalUrl,
|
'originalUrl' => $this->originalUrl,
|
||||||
'dateCreated' => isset($this->dateCreated) ? $this->dateCreated->format(\DateTime::ISO8601) : null,
|
'dateCreated' => isset($this->dateCreated) ? $this->dateCreated->format(\DateTime::ISO8601) : null,
|
||||||
'visitsCount' => count($this->visits),
|
'visitsCount' => count($this->visits),
|
||||||
|
'tags' => $this->tags->toArray(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
52
module/Core/src/Entity/Tag.php
Normal file
52
module/Core/src/Entity/Tag.php
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Core\Entity;
|
||||||
|
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Tag
|
||||||
|
* @author
|
||||||
|
* @link
|
||||||
|
*
|
||||||
|
* @ORM\Entity()
|
||||||
|
* @ORM\Table(name="tags")
|
||||||
|
*/
|
||||||
|
class Tag extends AbstractEntity implements \JsonSerializable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
* @ORM\Column(unique=true)
|
||||||
|
*/
|
||||||
|
protected $name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getName()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $name
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function setName($name)
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify data which should be serialized to JSON
|
||||||
|
* @link http://php.net/manual/en/jsonserializable.jsonserialize.php
|
||||||
|
* @return mixed data which can be serialized by <b>json_encode</b>,
|
||||||
|
* which is a value of any type other than a resource.
|
||||||
|
* @since 5.4.0
|
||||||
|
*/
|
||||||
|
public function jsonSerialize()
|
||||||
|
{
|
||||||
|
return $this->name;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||||
|
|
||||||
class InvalidShortCodeException extends RuntimeException
|
class InvalidShortCodeException extends RuntimeException
|
||||||
{
|
{
|
||||||
public static function fromShortCode($shortCode, $charSet, \Exception $previous = null)
|
public static function fromCharset($shortCode, $charSet, \Exception $previous = null)
|
||||||
{
|
{
|
||||||
$code = isset($previous) ? $previous->getCode() : -1;
|
$code = isset($previous) ? $previous->getCode() : -1;
|
||||||
return new static(
|
return new static(
|
||||||
|
@ -14,4 +14,9 @@ class InvalidShortCodeException extends RuntimeException
|
||||||
$previous
|
$previous
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function fromNotFoundShortCode($shortCode)
|
||||||
|
{
|
||||||
|
return new static(sprintf('Provided short code "%s" does not belong to a short URL', $shortCode));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Shlinkio\Shlink\Core\Options;
|
namespace Shlinkio\Shlink\Core\Options;
|
||||||
|
|
||||||
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
|
||||||
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
use Shlinkio\Shlink\Common\Util\StringUtilsTrait;
|
||||||
use Zend\Stdlib\AbstractOptions;
|
use Zend\Stdlib\AbstractOptions;
|
||||||
|
|
||||||
|
@ -25,8 +24,6 @@ class AppOptions extends AbstractOptions
|
||||||
/**
|
/**
|
||||||
* AppOptions constructor.
|
* AppOptions constructor.
|
||||||
* @param array|null|\Traversable $options
|
* @param array|null|\Traversable $options
|
||||||
*
|
|
||||||
* @Inject({"config.app_options"})
|
|
||||||
*/
|
*/
|
||||||
public function __construct($options = null)
|
public function __construct($options = null)
|
||||||
{
|
{
|
||||||
|
|
29
module/Core/src/Options/AppOptionsFactory.php
Normal file
29
module/Core/src/Options/AppOptionsFactory.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Core\Options;
|
||||||
|
|
||||||
|
use Interop\Container\ContainerInterface;
|
||||||
|
use Interop\Container\Exception\ContainerException;
|
||||||
|
use Zend\ServiceManager\Exception\ServiceNotCreatedException;
|
||||||
|
use Zend\ServiceManager\Exception\ServiceNotFoundException;
|
||||||
|
use Zend\ServiceManager\Factory\FactoryInterface;
|
||||||
|
|
||||||
|
class AppOptionsFactory implements FactoryInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create an object
|
||||||
|
*
|
||||||
|
* @param ContainerInterface $container
|
||||||
|
* @param string $requestedName
|
||||||
|
* @param null|array $options
|
||||||
|
* @return object
|
||||||
|
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||||
|
* @throws ServiceNotCreatedException if an exception is raised when
|
||||||
|
* creating a service.
|
||||||
|
* @throws ContainerException if any other error occurs
|
||||||
|
*/
|
||||||
|
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||||
|
{
|
||||||
|
$config = $container->has('config') ? $container->get('config') : [];
|
||||||
|
return new AppOptions(isset($config['app_options']) ? $config['app_options'] : []);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,11 +5,15 @@ use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||||
|
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||||
use Zend\Paginator\Paginator;
|
use Zend\Paginator\Paginator;
|
||||||
|
|
||||||
class ShortUrlService implements ShortUrlServiceInterface
|
class ShortUrlService implements ShortUrlServiceInterface
|
||||||
{
|
{
|
||||||
|
use TagManagerTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var EntityManagerInterface
|
* @var EntityManagerInterface
|
||||||
*/
|
*/
|
||||||
|
@ -40,4 +44,26 @@ class ShortUrlService implements ShortUrlServiceInterface
|
||||||
|
|
||||||
return $paginator;
|
return $paginator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $shortCode
|
||||||
|
* @param string[] $tags
|
||||||
|
* @return ShortUrl
|
||||||
|
* @throws InvalidShortCodeException
|
||||||
|
*/
|
||||||
|
public function setTagsByShortCode($shortCode, array $tags = [])
|
||||||
|
{
|
||||||
|
/** @var ShortUrl $shortUrl */
|
||||||
|
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||||
|
'shortCode' => $shortCode,
|
||||||
|
]);
|
||||||
|
if (! isset($shortUrl)) {
|
||||||
|
throw InvalidShortCodeException::fromNotFoundShortCode($shortCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
$shortUrl->setTags($this->tagNamesToEntities($this->em, $tags));
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $shortUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
namespace Shlinkio\Shlink\Core\Service;
|
namespace Shlinkio\Shlink\Core\Service;
|
||||||
|
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
use Zend\Paginator\Paginator;
|
use Zend\Paginator\Paginator;
|
||||||
|
|
||||||
interface ShortUrlServiceInterface
|
interface ShortUrlServiceInterface
|
||||||
|
@ -11,4 +12,12 @@ interface ShortUrlServiceInterface
|
||||||
* @return ShortUrl[]|Paginator
|
* @return ShortUrl[]|Paginator
|
||||||
*/
|
*/
|
||||||
public function listShortUrls($page = 1);
|
public function listShortUrls($page = 1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $shortCode
|
||||||
|
* @param string[] $tags
|
||||||
|
* @return ShortUrl
|
||||||
|
* @throws InvalidShortCodeException
|
||||||
|
*/
|
||||||
|
public function setTagsByShortCode($shortCode, array $tags = []);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,9 +12,12 @@ use Shlinkio\Shlink\Common\Exception\RuntimeException;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
use Shlinkio\Shlink\Core\Exception\InvalidUrlException;
|
||||||
|
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
|
||||||
|
|
||||||
class UrlShortener implements UrlShortenerInterface
|
class UrlShortener implements UrlShortenerInterface
|
||||||
{
|
{
|
||||||
|
use TagManagerTrait;
|
||||||
|
|
||||||
const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ';
|
const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,15 +62,16 @@ class UrlShortener implements UrlShortenerInterface
|
||||||
* Creates and persists a unique shortcode generated for provided url
|
* Creates and persists a unique shortcode generated for provided url
|
||||||
*
|
*
|
||||||
* @param UriInterface $url
|
* @param UriInterface $url
|
||||||
|
* @param string[] $tags
|
||||||
* @return string
|
* @return string
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public function urlToShortCode(UriInterface $url)
|
public function urlToShortCode(UriInterface $url, array $tags = [])
|
||||||
{
|
{
|
||||||
// If the url already exists in the database, just return its short code
|
// If the url already exists in the database, just return its short code
|
||||||
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
$shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([
|
||||||
'originalUrl' => $url
|
'originalUrl' => $url,
|
||||||
]);
|
]);
|
||||||
if (isset($shortUrl)) {
|
if (isset($shortUrl)) {
|
||||||
return $shortUrl->getShortCode();
|
return $shortUrl->getShortCode();
|
||||||
|
@ -88,7 +92,8 @@ class UrlShortener implements UrlShortenerInterface
|
||||||
|
|
||||||
// Generate the short code and persist it
|
// Generate the short code and persist it
|
||||||
$shortCode = $this->convertAutoincrementIdToShortCode($shortUrl->getId());
|
$shortCode = $this->convertAutoincrementIdToShortCode($shortUrl->getId());
|
||||||
$shortUrl->setShortCode($shortCode);
|
$shortUrl->setShortCode($shortCode)
|
||||||
|
->setTags($this->tagNamesToEntities($this->em, $tags));
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
$this->em->commit();
|
$this->em->commit();
|
||||||
|
@ -156,7 +161,7 @@ class UrlShortener implements UrlShortenerInterface
|
||||||
|
|
||||||
// Validate short code format
|
// Validate short code format
|
||||||
if (! preg_match('|[' . $this->chars . "]+|", $shortCode)) {
|
if (! preg_match('|[' . $this->chars . "]+|", $shortCode)) {
|
||||||
throw InvalidShortCodeException::fromShortCode($shortCode, $this->chars);
|
throw InvalidShortCodeException::fromCharset($shortCode, $this->chars);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @var ShortUrl $shortUrl */
|
/** @var ShortUrl $shortUrl */
|
||||||
|
|
|
@ -12,11 +12,12 @@ interface UrlShortenerInterface
|
||||||
* Creates and persists a unique shortcode generated for provided url
|
* Creates and persists a unique shortcode generated for provided url
|
||||||
*
|
*
|
||||||
* @param UriInterface $url
|
* @param UriInterface $url
|
||||||
|
* @param string[] $tags
|
||||||
* @return string
|
* @return string
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
* @throws RuntimeException
|
* @throws RuntimeException
|
||||||
*/
|
*/
|
||||||
public function urlToShortCode(UriInterface $url);
|
public function urlToShortCode(UriInterface $url, array $tags = []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to find the mapped URL for provided short code. Returns null if not found
|
* Tries to find the mapped URL for provided short code. Returns null if not found
|
||||||
|
|
38
module/Core/src/Util/TagManagerTrait.php
Normal file
38
module/Core/src/Util/TagManagerTrait.php
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Core\Util;
|
||||||
|
|
||||||
|
use Doctrine\Common\Collections;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||||
|
|
||||||
|
trait TagManagerTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param EntityManagerInterface $em
|
||||||
|
* @param string[] $tags
|
||||||
|
* @return Collections\Collection|Tag[]
|
||||||
|
*/
|
||||||
|
protected function tagNamesToEntities(EntityManagerInterface $em, array $tags)
|
||||||
|
{
|
||||||
|
$entities = [];
|
||||||
|
foreach ($tags as $tagName) {
|
||||||
|
$tagName = $this->normalizeTagName($tagName);
|
||||||
|
$tag = $em->getRepository(Tag::class)->findOneBy(['name' => $tagName]) ?: (new Tag())->setName($tagName);
|
||||||
|
$em->persist($tag);
|
||||||
|
$entities[] = $tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Collections\ArrayCollection($entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag names are trimmed, lowercased and spaces are replaced by dashes
|
||||||
|
*
|
||||||
|
* @param string $tagName
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function normalizeTagName($tagName)
|
||||||
|
{
|
||||||
|
return str_replace(' ', '-', strtolower(trim($tagName)));
|
||||||
|
}
|
||||||
|
}
|
114
module/Core/test/Action/PreviewActionTest.php
Normal file
114
module/Core/test/Action/PreviewActionTest.php
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Core\Action;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\Common\Exception\PreviewGenerationException;
|
||||||
|
use Shlinkio\Shlink\Common\Service\PreviewGenerator;
|
||||||
|
use Shlinkio\Shlink\Core\Action\PreviewAction;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
|
use Shlinkio\Shlink\Core\Service\UrlShortener;
|
||||||
|
use Zend\Diactoros\Response;
|
||||||
|
use Zend\Diactoros\ServerRequestFactory;
|
||||||
|
|
||||||
|
class PreviewActionTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var PreviewAction
|
||||||
|
*/
|
||||||
|
protected $action;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
private $previewGenerator;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
private $urlShortener;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->previewGenerator = $this->prophesize(PreviewGenerator::class);
|
||||||
|
$this->urlShortener = $this->prophesize(UrlShortener::class);
|
||||||
|
$this->action = new PreviewAction($this->previewGenerator->reveal(), $this->urlShortener->reveal());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function invalidShortCodeFallbacksToNextMiddlewareWithStatusNotFound()
|
||||||
|
{
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn(null)->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$resp = $this->action->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
|
||||||
|
new Response(),
|
||||||
|
function ($req, $resp) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(404, $resp->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function correctShortCodeReturnsImageResponse()
|
||||||
|
{
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$url = 'foobar.com';
|
||||||
|
$path = __FILE__;
|
||||||
|
$this->urlShortener->shortCodeToUrl($shortCode)->willReturn($url)->shouldBeCalledTimes(1);
|
||||||
|
$this->previewGenerator->generatePreview($url)->willReturn($path)->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$resp = $this->action->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
|
||||||
|
new Response()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(filesize($path), $resp->getHeaderLine('Content-length'));
|
||||||
|
$this->assertEquals((new \finfo(FILEINFO_MIME))->file($path), $resp->getHeaderLine('Content-type'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function invalidShortcodeExceptionReturnsNotFound()
|
||||||
|
{
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(InvalidShortCodeException::class)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$resp = $this->action->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
|
||||||
|
new Response(),
|
||||||
|
function ($req, $resp) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(404, $resp->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function previewExceptionReturnsNotFound()
|
||||||
|
{
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$this->urlShortener->shortCodeToUrl($shortCode)->willThrow(PreviewGenerationException::class)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$resp = $this->action->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', $shortCode),
|
||||||
|
new Response(),
|
||||||
|
function ($req, $resp) {
|
||||||
|
return $resp;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals(500, $resp->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
18
module/Core/test/Entity/TagTest.php
Normal file
18
module/Core/test/Entity/TagTest.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Core\Entity;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||||
|
|
||||||
|
class TagTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function jsonSerializationOfTagsReturnsItsName()
|
||||||
|
{
|
||||||
|
$tag = new Tag();
|
||||||
|
$tag->setName('This is my name');
|
||||||
|
$this->assertEquals($tag->getName(), $tag->jsonSerialize());
|
||||||
|
}
|
||||||
|
}
|
29
module/Core/test/Options/AppOptionsFactoryTest.php
Normal file
29
module/Core/test/Options/AppOptionsFactoryTest.php
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Core\Options;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Shlinkio\Shlink\Core\Options\AppOptions;
|
||||||
|
use Shlinkio\Shlink\Core\Options\AppOptionsFactory;
|
||||||
|
use Zend\ServiceManager\ServiceManager;
|
||||||
|
|
||||||
|
class AppOptionsFactoryTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var AppOptionsFactory
|
||||||
|
*/
|
||||||
|
protected $factory;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->factory = new AppOptionsFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function serviceIsCreated()
|
||||||
|
{
|
||||||
|
$instance = $this->factory->__invoke(new ServiceManager([]), '');
|
||||||
|
$this->assertInstanceOf(AppOptions::class, $instance);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,10 +2,12 @@
|
||||||
namespace ShlinkioTest\Shlink\Core\Service;
|
namespace ShlinkioTest\Shlink\Core\Service;
|
||||||
|
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\ORM\EntityRepository;
|
||||||
use PHPUnit_Framework_TestCase as TestCase;
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\Tag;
|
||||||
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
|
||||||
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||||
|
|
||||||
|
@ -23,6 +25,8 @@ class ShortUrlServiceTest extends TestCase
|
||||||
public function setUp()
|
public function setUp()
|
||||||
{
|
{
|
||||||
$this->em = $this->prophesize(EntityManagerInterface::class);
|
$this->em = $this->prophesize(EntityManagerInterface::class);
|
||||||
|
$this->em->persist(Argument::any())->willReturn(null);
|
||||||
|
$this->em->flush()->willReturn(null);
|
||||||
$this->service = new ShortUrlService($this->em->reveal());
|
$this->service = new ShortUrlService($this->em->reveal());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,4 +50,40 @@ class ShortUrlServiceTest extends TestCase
|
||||||
$list = $this->service->listShortUrls();
|
$list = $this->service->listShortUrls();
|
||||||
$this->assertEquals(4, $list->getCurrentItemCount());
|
$this->assertEquals(4, $list->getCurrentItemCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
* @expectedException \Shlinkio\Shlink\Core\Exception\InvalidShortCodeException
|
||||||
|
*/
|
||||||
|
public function exceptionIsThrownWhenSettingTagsOnInvalidShortcode()
|
||||||
|
{
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$repo = $this->prophesize(ShortUrlRepository::class);
|
||||||
|
$repo->findOneBy(['shortCode' => $shortCode])->willReturn(null)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$this->service->setTagsByShortCode($shortCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function providedTagsAreGetFromRepoAndSetToTheShortUrl()
|
||||||
|
{
|
||||||
|
$shortUrl = $this->prophesize(ShortUrl::class);
|
||||||
|
$shortUrl->setTags(Argument::any())->shouldBeCalledTimes(1);
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$repo = $this->prophesize(ShortUrlRepository::class);
|
||||||
|
$repo->findOneBy(['shortCode' => $shortCode])->willReturn($shortUrl->reveal())
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
$this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
|
||||||
|
|
||||||
|
$tagRepo = $this->prophesize(EntityRepository::class);
|
||||||
|
$tagRepo->findOneBy(['name' => 'foo'])->willReturn(new Tag())->shouldbeCalledTimes(1);
|
||||||
|
$tagRepo->findOneBy(['name' => 'bar'])->willReturn(null)->shouldbeCalledTimes(1);
|
||||||
|
$this->em->getRepository(Tag::class)->willReturn($tagRepo->reveal());
|
||||||
|
|
||||||
|
$this->service->setTagsByShortCode($shortCode, ['foo', 'bar']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,9 @@ return [
|
||||||
Action\ResolveUrlAction::class => AnnotatedFactory::class,
|
Action\ResolveUrlAction::class => AnnotatedFactory::class,
|
||||||
Action\GetVisitsAction::class => AnnotatedFactory::class,
|
Action\GetVisitsAction::class => AnnotatedFactory::class,
|
||||||
Action\ListShortcodesAction::class => AnnotatedFactory::class,
|
Action\ListShortcodesAction::class => AnnotatedFactory::class,
|
||||||
|
Action\EditTagsAction::class => AnnotatedFactory::class,
|
||||||
|
|
||||||
|
Middleware\BodyParserMiddleware::class => AnnotatedFactory::class,
|
||||||
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
|
Middleware\CrossDomainMiddleware::class => InvokableFactory::class,
|
||||||
Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class,
|
Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class,
|
||||||
],
|
],
|
||||||
|
|
|
@ -7,6 +7,7 @@ return [
|
||||||
'rest' => [
|
'rest' => [
|
||||||
'path' => '/rest',
|
'path' => '/rest',
|
||||||
'middleware' => [
|
'middleware' => [
|
||||||
|
Middleware\BodyParserMiddleware::class,
|
||||||
Middleware\CheckAuthenticationMiddleware::class,
|
Middleware\CheckAuthenticationMiddleware::class,
|
||||||
Middleware\CrossDomainMiddleware::class,
|
Middleware\CrossDomainMiddleware::class,
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
<?php
|
|
||||||
return [
|
|
||||||
|
|
||||||
'rest' => [
|
|
||||||
'username' => env('REST_USER'),
|
|
||||||
'password' => env('REST_PASSWORD'),
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
|
@ -34,6 +34,12 @@ return [
|
||||||
'middleware' => Action\GetVisitsAction::class,
|
'middleware' => Action\GetVisitsAction::class,
|
||||||
'allowed_methods' => ['GET', 'OPTIONS'],
|
'allowed_methods' => ['GET', 'OPTIONS'],
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'name' => 'rest-edit-tags',
|
||||||
|
'path' => '/rest/short-codes/{shortCode}/tags',
|
||||||
|
'middleware' => Action\EditTagsAction::class,
|
||||||
|
'allowed_methods' => ['PUT', 'OPTIONS'],
|
||||||
|
],
|
||||||
],
|
],
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
Binary file not shown.
|
@ -1,8 +1,8 @@
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: Shlink 1.0\n"
|
"Project-Id-Version: Shlink 1.0\n"
|
||||||
"POT-Creation-Date: 2016-08-07 20:19+0200\n"
|
"POT-Creation-Date: 2016-08-21 18:17+0200\n"
|
||||||
"PO-Revision-Date: 2016-08-07 20:21+0200\n"
|
"PO-Revision-Date: 2016-08-21 18:17+0200\n"
|
||||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||||
"Language-Team: \n"
|
"Language-Team: \n"
|
||||||
"Language: es_ES\n"
|
"Language: es_ES\n"
|
||||||
|
@ -29,21 +29,22 @@ msgid "A URL was not provided"
|
||||||
msgstr "No se ha proporcionado una URL"
|
msgstr "No se ha proporcionado una URL"
|
||||||
|
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Provided URL \"%s\" is invalid. Try with a different one."
|
msgid "Provided URL %s is invalid. Try with a different one."
|
||||||
msgstr ""
|
msgstr "La URL proporcionada \"%s\" es inválida. Prueba con una diferente."
|
||||||
"La URL \"%s\" proporcionada es inválida. Inténtalo de nuevo con una "
|
|
||||||
"diferente."
|
|
||||||
|
|
||||||
msgid "Unexpected error occurred"
|
msgid "Unexpected error occurred"
|
||||||
msgstr "Ocurrió un error inesperado"
|
msgstr "Ocurrió un error inesperado"
|
||||||
|
|
||||||
#, php-format
|
msgid "A list of tags was not provided"
|
||||||
msgid "Provided short code \"%s\" is invalid"
|
msgstr "No se ha proporcionado una lista de etiquetas"
|
||||||
msgstr "El código corto \"%s\" proporcionado es inválido"
|
|
||||||
|
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "No URL found for shortcode \"%s\""
|
msgid "No URL found for short code \"%s\""
|
||||||
msgstr "No se ha encontrado una URL para el código corto \"%s\""
|
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
|
||||||
|
|
||||||
|
#, php-format
|
||||||
|
msgid "Provided short code %s does not exist"
|
||||||
|
msgstr "El código corto \"%s\" proporcionado no existe"
|
||||||
|
|
||||||
#, php-format
|
#, php-format
|
||||||
msgid "Provided short code \"%s\" has an invalid format"
|
msgid "Provided short code \"%s\" has an invalid format"
|
||||||
|
|
|
@ -16,7 +16,7 @@ use Zend\I18n\Translator\TranslatorInterface;
|
||||||
class CreateShortcodeAction extends AbstractRestAction
|
class CreateShortcodeAction extends AbstractRestAction
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var UrlShortener|UrlShortenerInterface
|
* @var UrlShortenerInterface
|
||||||
*/
|
*/
|
||||||
private $urlShortener;
|
private $urlShortener;
|
||||||
/**
|
/**
|
||||||
|
@ -31,7 +31,7 @@ class CreateShortcodeAction extends AbstractRestAction
|
||||||
/**
|
/**
|
||||||
* GenerateShortcodeMiddleware constructor.
|
* GenerateShortcodeMiddleware constructor.
|
||||||
*
|
*
|
||||||
* @param UrlShortenerInterface|UrlShortener $urlShortener
|
* @param UrlShortenerInterface $urlShortener
|
||||||
* @param TranslatorInterface $translator
|
* @param TranslatorInterface $translator
|
||||||
* @param array $domainConfig
|
* @param array $domainConfig
|
||||||
* @param LoggerInterface|null $logger
|
* @param LoggerInterface|null $logger
|
||||||
|
@ -66,9 +66,10 @@ class CreateShortcodeAction extends AbstractRestAction
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
$longUrl = $postData['longUrl'];
|
$longUrl = $postData['longUrl'];
|
||||||
|
$tags = isset($postData['tags']) && is_array($postData['tags']) ? $postData['tags'] : [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl));
|
$shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl), $tags);
|
||||||
$shortUrl = (new Uri())->withPath($shortCode)
|
$shortUrl = (new Uri())->withPath($shortCode)
|
||||||
->withScheme($this->domainConfig['schema'])
|
->withScheme($this->domainConfig['schema'])
|
||||||
->withHost($this->domainConfig['hostname']);
|
->withHost($this->domainConfig['hostname']);
|
||||||
|
|
73
module/Rest/src/Action/EditTagsAction.php
Normal file
73
module/Rest/src/Action/EditTagsAction.php
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Rest\Action;
|
||||||
|
|
||||||
|
use Acelaya\ZsmAnnotatedServices\Annotation\Inject;
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
|
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||||
|
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
|
||||||
|
use Shlinkio\Shlink\Rest\Util\RestUtils;
|
||||||
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
|
use Zend\I18n\Translator\TranslatorInterface;
|
||||||
|
|
||||||
|
class EditTagsAction extends AbstractRestAction
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var ShortUrlServiceInterface
|
||||||
|
*/
|
||||||
|
private $shortUrlService;
|
||||||
|
/**
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
private $translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EditTagsAction constructor.
|
||||||
|
* @param ShortUrlServiceInterface $shortUrlService
|
||||||
|
* @param TranslatorInterface $translator
|
||||||
|
* @param LoggerInterface|null $logger
|
||||||
|
*
|
||||||
|
* @Inject({ShortUrlService::class, "translator", "Logger_Shlink"})
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
ShortUrlServiceInterface $shortUrlService,
|
||||||
|
TranslatorInterface $translator,
|
||||||
|
LoggerInterface $logger = null
|
||||||
|
) {
|
||||||
|
parent::__construct($logger);
|
||||||
|
$this->shortUrlService = $shortUrlService;
|
||||||
|
$this->translator = $translator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param callable|null $out
|
||||||
|
* @return null|Response
|
||||||
|
*/
|
||||||
|
protected function dispatch(Request $request, Response $response, callable $out = null)
|
||||||
|
{
|
||||||
|
$shortCode = $request->getAttribute('shortCode');
|
||||||
|
$bodyParams = $request->getParsedBody();
|
||||||
|
|
||||||
|
if (! isset($bodyParams['tags'])) {
|
||||||
|
return new JsonResponse([
|
||||||
|
'error' => RestUtils::INVALID_ARGUMENT_ERROR,
|
||||||
|
'message' => $this->translator->translate('A list of tags was not provided'),
|
||||||
|
], 400);
|
||||||
|
}
|
||||||
|
$tags = $bodyParams['tags'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$shortUrl = $this->shortUrlService->setTagsByShortCode($shortCode, $tags);
|
||||||
|
return new JsonResponse(['tags' => $shortUrl->getTags()->toArray()]);
|
||||||
|
} catch (InvalidShortCodeException $e) {
|
||||||
|
return new JsonResponse([
|
||||||
|
'error' => RestUtils::getRestErrorCodeFromException($e),
|
||||||
|
'message' => sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode),
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Shlinkio\Shlink\Rest\ErrorHandler;
|
namespace Shlinkio\Shlink\Rest\ErrorHandler;
|
||||||
|
|
||||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
use Acelaya\ExpressiveErrorHandler\ErrorHandler\ErrorHandlerInterface;
|
||||||
use Psr\Http\Message\ResponseInterface as Response;
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
use Shlinkio\Shlink\Common\ErrorHandler\ErrorHandlerInterface;
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
use Zend\Diactoros\Response\JsonResponse;
|
use Zend\Diactoros\Response\JsonResponse;
|
||||||
use Zend\Expressive\Router\RouteResult;
|
use Zend\Expressive\Router\RouteResult;
|
||||||
|
|
||||||
|
|
52
module/Rest/src/Middleware/BodyParserMiddleware.php
Normal file
52
module/Rest/src/Middleware/BodyParserMiddleware.php
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<?php
|
||||||
|
namespace Shlinkio\Shlink\Rest\Middleware;
|
||||||
|
|
||||||
|
use Psr\Http\Message\ResponseInterface as Response;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Zend\Stratigility\MiddlewareInterface;
|
||||||
|
|
||||||
|
class BodyParserMiddleware implements MiddlewareInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Process an incoming request and/or response.
|
||||||
|
*
|
||||||
|
* Accepts a server-side request and a response instance, and does
|
||||||
|
* something with them.
|
||||||
|
*
|
||||||
|
* If the response is not complete and/or further processing would not
|
||||||
|
* interfere with the work done in the middleware, or if the middleware
|
||||||
|
* wants to delegate to another process, it can use the `$out` callable
|
||||||
|
* if present.
|
||||||
|
*
|
||||||
|
* If the middleware does not return a value, execution of the current
|
||||||
|
* request is considered complete, and the response instance provided will
|
||||||
|
* be considered the response to return.
|
||||||
|
*
|
||||||
|
* Alternately, the middleware may return a response instance.
|
||||||
|
*
|
||||||
|
* Often, middleware will `return $out();`, with the assumption that a
|
||||||
|
* later middleware will return a response.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param Response $response
|
||||||
|
* @param null|callable $out
|
||||||
|
* @return null|Response
|
||||||
|
*/
|
||||||
|
public function __invoke(Request $request, Response $response, callable $out = null)
|
||||||
|
{
|
||||||
|
$method = $request->getMethod();
|
||||||
|
if (! in_array($method, ['PUT', 'PATCH'])) {
|
||||||
|
return $out($request, $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
$contentType = $request->getHeaderLine('Content-type');
|
||||||
|
$rawBody = (string) $request->getBody();
|
||||||
|
if (in_array($contentType, ['application/json', 'text/json', 'application/x-json'])) {
|
||||||
|
return $out($request->withParsedBody(json_decode($rawBody, true)), $response);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parsedBody = [];
|
||||||
|
parse_str($rawBody, $parsedBody);
|
||||||
|
return $out($request->withParsedBody($parsedBody), $response);
|
||||||
|
}
|
||||||
|
}
|
|
@ -48,7 +48,7 @@ class CrossDomainMiddleware implements MiddlewareInterface
|
||||||
|
|
||||||
// Add OPTIONS-specific headers
|
// Add OPTIONS-specific headers
|
||||||
foreach ([
|
foreach ([
|
||||||
'Access-Control-Allow-Methods' => 'GET, POST, PUT, DELETE, OPTIONS', // TODO Should be based on path
|
'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS', // TODO Should be based on path
|
||||||
'Access-Control-Max-Age' => '1000',
|
'Access-Control-Max-Age' => '1000',
|
||||||
'Access-Control-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'),
|
'Access-Control-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'),
|
||||||
] as $key => $value) {
|
] as $key => $value) {
|
||||||
|
|
|
@ -47,8 +47,9 @@ class CreateShortcodeActionTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function properShortcodeConversionReturnsData()
|
public function properShortcodeConversionReturnsData()
|
||||||
{
|
{
|
||||||
$this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willReturn('abc123')
|
$this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array'))
|
||||||
->shouldBeCalledTimes(1);
|
->willReturn('abc123')
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||||
'longUrl' => 'http://www.domain.com/foo/bar',
|
'longUrl' => 'http://www.domain.com/foo/bar',
|
||||||
|
@ -63,8 +64,9 @@ class CreateShortcodeActionTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function anInvalidUrlReturnsError()
|
public function anInvalidUrlReturnsError()
|
||||||
{
|
{
|
||||||
$this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willThrow(InvalidUrlException::class)
|
$this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array'))
|
||||||
->shouldBeCalledTimes(1);
|
->willThrow(InvalidUrlException::class)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||||
'longUrl' => 'http://www.domain.com/foo/bar',
|
'longUrl' => 'http://www.domain.com/foo/bar',
|
||||||
|
@ -79,8 +81,9 @@ class CreateShortcodeActionTest extends TestCase
|
||||||
*/
|
*/
|
||||||
public function aGenericExceptionWillReturnError()
|
public function aGenericExceptionWillReturnError()
|
||||||
{
|
{
|
||||||
$this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willThrow(\Exception::class)
|
$this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array'))
|
||||||
->shouldBeCalledTimes(1);
|
->willThrow(\Exception::class)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
$request = ServerRequestFactory::fromGlobals()->withParsedBody([
|
||||||
'longUrl' => 'http://www.domain.com/foo/bar',
|
'longUrl' => 'http://www.domain.com/foo/bar',
|
||||||
|
|
76
module/Rest/test/Action/EditTagsActionTest.php
Normal file
76
module/Rest/test/Action/EditTagsActionTest.php
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Rest\Action;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
|
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||||
|
use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException;
|
||||||
|
use Shlinkio\Shlink\Core\Service\ShortUrlService;
|
||||||
|
use Shlinkio\Shlink\Rest\Action\EditTagsAction;
|
||||||
|
use Zend\Diactoros\Response;
|
||||||
|
use Zend\Diactoros\ServerRequestFactory;
|
||||||
|
use Zend\I18n\Translator\Translator;
|
||||||
|
|
||||||
|
class EditTagsActionTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var EditTagsAction
|
||||||
|
*/
|
||||||
|
protected $action;
|
||||||
|
/**
|
||||||
|
* @var ObjectProphecy
|
||||||
|
*/
|
||||||
|
private $shortUrlService;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->shortUrlService = $this->prophesize(ShortUrlService::class);
|
||||||
|
$this->action = new EditTagsAction($this->shortUrlService->reveal(), Translator::factory([]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function notProvidingTagsReturnsError()
|
||||||
|
{
|
||||||
|
$response = $this->action->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123'),
|
||||||
|
new Response()
|
||||||
|
);
|
||||||
|
$this->assertEquals(400, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function anInvalidShortCodeReturnsNotFound()
|
||||||
|
{
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willThrow(InvalidShortCodeException::class)
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$response = $this->action->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
|
||||||
|
->withParsedBody(['tags' => []]),
|
||||||
|
new Response()
|
||||||
|
);
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function tagsListIsReturnedIfCorrectShortCodeIsProvided()
|
||||||
|
{
|
||||||
|
$shortCode = 'abc123';
|
||||||
|
$this->shortUrlService->setTagsByShortCode($shortCode, [])->willReturn(new ShortUrl())
|
||||||
|
->shouldBeCalledTimes(1);
|
||||||
|
|
||||||
|
$response = $this->action->__invoke(
|
||||||
|
ServerRequestFactory::fromGlobals()->withAttribute('shortCode', 'abc123')
|
||||||
|
->withParsedBody(['tags' => []]),
|
||||||
|
new Response()
|
||||||
|
);
|
||||||
|
$this->assertEquals(200, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,6 @@ class ConfigProviderTest extends TestCase
|
||||||
|
|
||||||
$this->assertArrayHasKey('error_handler', $config);
|
$this->assertArrayHasKey('error_handler', $config);
|
||||||
$this->assertArrayHasKey('middleware_pipeline', $config);
|
$this->assertArrayHasKey('middleware_pipeline', $config);
|
||||||
$this->assertArrayHasKey('rest', $config);
|
|
||||||
$this->assertArrayHasKey('routes', $config);
|
$this->assertArrayHasKey('routes', $config);
|
||||||
$this->assertArrayHasKey('dependencies', $config);
|
$this->assertArrayHasKey('dependencies', $config);
|
||||||
$this->assertArrayHasKey('translator', $config);
|
$this->assertArrayHasKey('translator', $config);
|
||||||
|
|
79
module/Rest/test/Middleware/BodyParserMiddlewareTest.php
Normal file
79
module/Rest/test/Middleware/BodyParserMiddlewareTest.php
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<?php
|
||||||
|
namespace ShlinkioTest\Shlink\Rest\Middleware;
|
||||||
|
|
||||||
|
use PHPUnit_Framework_TestCase as TestCase;
|
||||||
|
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||||
|
use Shlinkio\Shlink\Rest\Middleware\BodyParserMiddleware;
|
||||||
|
use Zend\Diactoros\Response;
|
||||||
|
use Zend\Diactoros\ServerRequestFactory;
|
||||||
|
use Zend\Diactoros\Stream;
|
||||||
|
|
||||||
|
class BodyParserMiddlewareTest extends TestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var BodyParserMiddleware
|
||||||
|
*/
|
||||||
|
private $middleware;
|
||||||
|
|
||||||
|
public function setUp()
|
||||||
|
{
|
||||||
|
$this->middleware = new BodyParserMiddleware();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function requestsFromOtherMethodsJustFallbackToNextMiddleware()
|
||||||
|
{
|
||||||
|
$request = ServerRequestFactory::fromGlobals()->withMethod('GET');
|
||||||
|
$test = $this;
|
||||||
|
$this->middleware->__invoke($request, new Response(), function ($req, $resp) use ($test, $request) {
|
||||||
|
$test->assertSame($request, $req);
|
||||||
|
});
|
||||||
|
|
||||||
|
$request = $request->withMethod('POST');
|
||||||
|
$test = $this;
|
||||||
|
$this->middleware->__invoke($request, new Response(), function ($req, $resp) use ($test, $request) {
|
||||||
|
$test->assertSame($request, $req);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function jsonRequestsAreJsonDecoded()
|
||||||
|
{
|
||||||
|
$body = new Stream('php://temp', 'wr');
|
||||||
|
$body->write('{"foo": "bar", "bar": ["one", 5]}');
|
||||||
|
$request = ServerRequestFactory::fromGlobals()->withMethod('PUT')
|
||||||
|
->withBody($body)
|
||||||
|
->withHeader('content-type', 'application/json');
|
||||||
|
$test = $this;
|
||||||
|
$this->middleware->__invoke($request, new Response(), function (Request $req, $resp) use ($test, $request) {
|
||||||
|
$test->assertNotSame($request, $req);
|
||||||
|
$test->assertEquals([
|
||||||
|
'foo' => 'bar',
|
||||||
|
'bar' => ['one', 5],
|
||||||
|
], $req->getParsedBody());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @test
|
||||||
|
*/
|
||||||
|
public function regularRequestsAreUrlDecoded()
|
||||||
|
{
|
||||||
|
$body = new Stream('php://temp', 'wr');
|
||||||
|
$body->write('foo=bar&bar[]=one&bar[]=5');
|
||||||
|
$request = ServerRequestFactory::fromGlobals()->withMethod('PUT')
|
||||||
|
->withBody($body);
|
||||||
|
$test = $this;
|
||||||
|
$this->middleware->__invoke($request, new Response(), function (Request $req, $resp) use ($test, $request) {
|
||||||
|
$test->assertNotSame($request, $req);
|
||||||
|
$test->assertEquals([
|
||||||
|
'foo' => 'bar',
|
||||||
|
'bar' => ['one', 5],
|
||||||
|
], $req->getParsedBody());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,7 +16,10 @@
|
||||||
</rule>
|
</rule>
|
||||||
|
|
||||||
<!-- Paths to check -->
|
<!-- Paths to check -->
|
||||||
|
<file>bin</file>
|
||||||
<file>module</file>
|
<file>module</file>
|
||||||
|
<file>data/migrations</file>
|
||||||
<file>config</file>
|
<file>config</file>
|
||||||
<file>public/index.php</file>
|
<file>public/index.php</file>
|
||||||
|
<exclude-pattern>config/params/*</exclude-pattern>
|
||||||
</ruleset>
|
</ruleset>
|
||||||
|
|
17
public/.htaccess
Normal file
17
public/.htaccess
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
RewriteEngine On
|
||||||
|
# The following rule tells Apache that if the requested filename
|
||||||
|
# exists, simply serve it.
|
||||||
|
RewriteCond %{REQUEST_FILENAME} -s [OR]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} -l [OR]
|
||||||
|
RewriteCond %{REQUEST_FILENAME} -d
|
||||||
|
RewriteRule ^.*$ - [NC,L]
|
||||||
|
|
||||||
|
# The following rewrites all other queries to index.php. The
|
||||||
|
# condition ensures that if you are using Apache aliases to do
|
||||||
|
# mass virtual hosting, the base path will be prepended to
|
||||||
|
# allow proper resolution of the index.php file; it will work
|
||||||
|
# in non-aliased environments as well, providing a safe, one-size
|
||||||
|
# fits all solution.
|
||||||
|
RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$
|
||||||
|
RewriteRule ^(.*) - [E=BASE:%1]
|
||||||
|
RewriteRule ^(.*)$ %{ENV:BASE}index.php [NC,L]
|
Loading…
Add table
Reference in a new issue