mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-27 16:26:37 +03:00
commit
f77273ef93
29 changed files with 890 additions and 40 deletions
BIN
bin/wkhtmltoimage
Executable file
BIN
bin/wkhtmltoimage
Executable file
Binary file not shown.
|
@ -28,10 +28,12 @@
|
|||
"guzzlehttp/guzzle": "^6.2",
|
||||
"symfony/console": "^3.0",
|
||||
"symfony/process": "^3.0",
|
||||
"symfony/filesystem": "^3.0",
|
||||
"firebase/php-jwt": "^4.0",
|
||||
"monolog/monolog": "^1.21",
|
||||
"theorchard/monolog-cascade": "^0.4",
|
||||
"endroid/qrcode": "^1.7"
|
||||
"endroid/qrcode": "^1.7",
|
||||
"mikehaertl/phpwkhtmltopdf": "^2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5.0",
|
||||
|
|
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',
|
||||
],
|
||||
|
||||
];
|
|
@ -10,6 +10,7 @@ return [
|
|||
Command\Shortcode\ResolveUrlCommand::class,
|
||||
Command\Shortcode\ListShortcodesCommand::class,
|
||||
Command\Shortcode\GetVisitsCommand::class,
|
||||
Command\Shortcode\GeneratePreviewCommand::class,
|
||||
Command\Visit\ProcessVisitsCommand::class,
|
||||
Command\Config\GenerateCharsetCommand::class,
|
||||
Command\Config\GenerateSecretCommand::class,
|
||||
|
|
|
@ -14,6 +14,7 @@ return [
|
|||
Command\Shortcode\ResolveUrlCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\ListShortcodesCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\GetVisitsCommand::class => AnnotatedFactory::class,
|
||||
Command\Shortcode\GeneratePreviewCommand::class => AnnotatedFactory::class,
|
||||
Command\Visit\ProcessVisitsCommand::class => AnnotatedFactory::class,
|
||||
Command\Config\GenerateCharsetCommand::class => AnnotatedFactory::class,
|
||||
Command\Config\GenerateSecretCommand::class => AnnotatedFactory::class,
|
||||
|
|
Binary file not shown.
|
@ -1,8 +1,8 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2016-08-07 20:16+0200\n"
|
||||
"PO-Revision-Date: 2016-08-07 20:18+0200\n"
|
||||
"POT-Creation-Date: 2016-08-18 17:24+0200\n"
|
||||
"PO-Revision-Date: 2016-08-18 17:26+0200\n"
|
||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es_ES\n"
|
||||
|
@ -68,7 +68,35 @@ msgstr ""
|
|||
msgid "Character set:"
|
||||
msgstr "Grupo de caracteres:"
|
||||
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
"Generates a random secret string that can be used for JWT token encryption"
|
||||
msgstr ""
|
||||
"Genera una cadena de caracteres aleatoria que puede ser usada para cifrar "
|
||||
"tokens JWT"
|
||||
|
||||
msgid "Secret key:"
|
||||
msgstr "Clave secreta:"
|
||||
|
||||
msgid ""
|
||||
"Processes and generates the previews for every URL, improving performance "
|
||||
"for later web requests."
|
||||
msgstr ""
|
||||
"Procesa y genera las vistas previas para cada URL, mejorando el rendimiento "
|
||||
"para peticiones web posteriores."
|
||||
|
||||
msgid "Finished processing all URLs"
|
||||
msgstr "Finalizado el procesado de todas las URLs"
|
||||
|
||||
#, php-format
|
||||
msgid "Processing URL %s..."
|
||||
msgstr "Procesando URL %s..."
|
||||
|
||||
msgid " <info>Success!</info>"
|
||||
msgstr "<info>¡Correcto!</info>"
|
||||
|
||||
msgid "Error"
|
||||
msgstr "Error"
|
||||
|
||||
msgid "Generates a short code for provided URL and returns the short URL"
|
||||
msgstr ""
|
||||
"Genera un código corto para la URL proporcionada y devuelve la URL acortada"
|
||||
|
@ -149,22 +177,6 @@ msgstr "Has alcanzado la última página"
|
|||
msgid "Continue with page"
|
||||
msgstr "Continuar con la página"
|
||||
|
||||
msgid "Processes visits where location is not set yet"
|
||||
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
|
||||
|
||||
msgid "Processing IP"
|
||||
msgstr "Procesando IP"
|
||||
|
||||
msgid "Ignored localhost address"
|
||||
msgstr "Ignorada IP de localhost"
|
||||
|
||||
#, php-format
|
||||
msgid "Address located at \"%s\""
|
||||
msgstr "Dirección localizada en \"%s\""
|
||||
|
||||
msgid "Finished processing all IPs"
|
||||
msgstr "Finalizado el procesado de todas las IPs"
|
||||
|
||||
msgid "Returns the long URL behind a short code"
|
||||
msgstr "Devuelve la URL larga detrás de un código corto"
|
||||
|
||||
|
@ -185,3 +197,19 @@ msgstr "URL larga:"
|
|||
#, php-format
|
||||
msgid "Provided short code \"%s\" has an invalid format."
|
||||
msgstr "El código corto proporcionado \"%s\" tiene un formato inválido."
|
||||
|
||||
msgid "Processes visits where location is not set yet"
|
||||
msgstr "Procesa las visitas donde la localización no ha sido establecida aún"
|
||||
|
||||
msgid "Processing IP"
|
||||
msgstr "Procesando IP"
|
||||
|
||||
msgid "Ignored localhost address"
|
||||
msgstr "Ignorada IP de localhost"
|
||||
|
||||
#, php-format
|
||||
msgid "Address located at \"%s\""
|
||||
msgstr "Dirección localizada en \"%s\""
|
||||
|
||||
msgid "Finished processing all IPs"
|
||||
msgstr "Finalizado el procesado de todas las IPs"
|
||||
|
|
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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -4,30 +4,35 @@ use Doctrine\Common\Cache\Cache;
|
|||
use Doctrine\ORM\EntityManager;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Common\Factory\CacheFactory;
|
||||
use Shlinkio\Shlink\Common\Factory\EntityManagerFactory;
|
||||
use Shlinkio\Shlink\Common\Factory\LoggerFactory;
|
||||
use Shlinkio\Shlink\Common\Factory\TranslatorFactory;
|
||||
use Shlinkio\Shlink\Common\Factory;
|
||||
use Shlinkio\Shlink\Common\Image;
|
||||
use Shlinkio\Shlink\Common\Middleware\LocaleMiddleware;
|
||||
use Shlinkio\Shlink\Common\Service\IpLocationResolver;
|
||||
use Shlinkio\Shlink\Common\Service;
|
||||
use Shlinkio\Shlink\Common\Twig\Extension\TranslatorExtension;
|
||||
use Symfony\Component\Filesystem\Filesystem;
|
||||
use Zend\I18n\Translator\Translator;
|
||||
use Zend\ServiceManager\Factory\InvokableFactory;
|
||||
|
||||
return [
|
||||
|
||||
'dependencies' => [
|
||||
'invokables' => [
|
||||
Filesystem::class => Filesystem::class,
|
||||
],
|
||||
'factories' => [
|
||||
EntityManager::class => EntityManagerFactory::class,
|
||||
EntityManager::class => Factory\EntityManagerFactory::class,
|
||||
GuzzleHttp\Client::class => InvokableFactory::class,
|
||||
Cache::class => CacheFactory::class,
|
||||
'Logger_Shlink' => LoggerFactory::class,
|
||||
Cache::class => Factory\CacheFactory::class,
|
||||
'Logger_Shlink' => Factory\LoggerFactory::class,
|
||||
|
||||
Translator::class => TranslatorFactory::class,
|
||||
Translator::class => Factory\TranslatorFactory::class,
|
||||
TranslatorExtension::class => AnnotatedFactory::class,
|
||||
LocaleMiddleware::class => AnnotatedFactory::class,
|
||||
|
||||
IpLocationResolver::class => AnnotatedFactory::class,
|
||||
Image\ImageBuilder::class => Image\ImageBuilderFactory::class,
|
||||
|
||||
Service\IpLocationResolver::class => AnnotatedFactory::class,
|
||||
Service\PreviewGenerator::class => AnnotatedFactory::class,
|
||||
],
|
||||
'aliases' => [
|
||||
'em' => EntityManager::class,
|
||||
|
|
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));
|
||||
}
|
||||
}
|
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;
|
||||
}
|
31
module/Common/src/Image/ImageBuilderFactory.php
Normal file
31
module/Common/src/Image/ImageBuilderFactory.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?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 ImageBuilderFactory implements FactoryInterface
|
||||
{
|
||||
/**
|
||||
* Create an object
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @param string $requestedName
|
||||
* @param null|array $options
|
||||
* @return object
|
||||
* @throws ServiceNotFoundException if unable to resolve the service.
|
||||
* @throws ServiceNotCreatedException if an exception is raised when
|
||||
* creating a service.
|
||||
* @throws ContainerException if any other error occurs
|
||||
*/
|
||||
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
|
||||
{
|
||||
return new ImageBuilder($container, ['factories' => [
|
||||
Image::class => ImageFactory::class,
|
||||
]]);
|
||||
}
|
||||
}
|
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));
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ return [
|
|||
// Middleware
|
||||
Action\RedirectAction::class => AnnotatedFactory::class,
|
||||
Action\QrCodeAction::class => AnnotatedFactory::class,
|
||||
Action\PreviewAction::class => AnnotatedFactory::class,
|
||||
Middleware\QrCodeCacheMiddleware::class => AnnotatedFactory::class,
|
||||
],
|
||||
],
|
||||
|
|
|
@ -13,6 +13,23 @@ return [
|
|||
],
|
||||
[
|
||||
'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]+}]',
|
||||
'middleware' => [
|
||||
Middleware\QrCodeCacheMiddleware::class,
|
||||
|
|
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');
|
||||
}
|
||||
}
|
||||
}
|
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());
|
||||
}
|
||||
}
|
Binary file not shown.
|
@ -1,8 +1,8 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Shlink 1.0\n"
|
||||
"POT-Creation-Date: 2016-08-07 20:19+0200\n"
|
||||
"PO-Revision-Date: 2016-08-07 20:21+0200\n"
|
||||
"POT-Creation-Date: 2016-08-18 17:27+0200\n"
|
||||
"PO-Revision-Date: 2016-08-18 17:27+0200\n"
|
||||
"Last-Translator: Alejandro Celaya <alejandro@alejandrocelaya.com>\n"
|
||||
"Language-Team: \n"
|
||||
"Language: es_ES\n"
|
||||
|
@ -29,21 +29,19 @@ msgid "A URL was not provided"
|
|||
msgstr "No se ha proporcionado una URL"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided URL \"%s\" is invalid. Try with a different one."
|
||||
msgstr ""
|
||||
"La URL \"%s\" proporcionada es inválida. Inténtalo de nuevo con una "
|
||||
"diferente."
|
||||
msgid "Provided URL %s is invalid. Try with a different one."
|
||||
msgstr "La URL proporcionada \"%s\" es inválida. Prueba con una diferente."
|
||||
|
||||
msgid "Unexpected error occurred"
|
||||
msgstr "Ocurrió un error inesperado"
|
||||
|
||||
#, php-format
|
||||
msgid "Provided short code \"%s\" is invalid"
|
||||
msgstr "El código corto \"%s\" proporcionado es inválido"
|
||||
msgid "Provided short code %s does not exist"
|
||||
msgstr "El código corto \"%s\" proporcionado no existe"
|
||||
|
||||
#, php-format
|
||||
msgid "No URL found for shortcode \"%s\""
|
||||
msgstr "No se ha encontrado una URL para el código corto \"%s\""
|
||||
msgid "No URL found for short code \"%s\""
|
||||
msgstr "No se ha encontrado ninguna URL para el código corto \"%s\""
|
||||
|
||||
#, php-format
|
||||
msgid "Provided short code \"%s\" has an invalid format"
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
</rule>
|
||||
|
||||
<!-- Paths to check -->
|
||||
<file>bin</file>
|
||||
<file>module</file>
|
||||
<file>config</file>
|
||||
<file>public/index.php</file>
|
||||
|
|
Loading…
Reference in a new issue