From a2c2bc166c7283b0ccef36cbec0579890f085c40 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Fri, 19 Aug 2016 16:12:39 +0200 Subject: [PATCH 01/13] Removed wrong spaces --- module/Rest/src/Middleware/CrossDomainMiddleware.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/Rest/src/Middleware/CrossDomainMiddleware.php b/module/Rest/src/Middleware/CrossDomainMiddleware.php index 3019badf..4327df9e 100644 --- a/module/Rest/src/Middleware/CrossDomainMiddleware.php +++ b/module/Rest/src/Middleware/CrossDomainMiddleware.php @@ -48,7 +48,7 @@ class CrossDomainMiddleware implements MiddlewareInterface // Add OPTIONS-specific headers 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-Allow-Headers' => $request->getHeaderLine('Access-Control-Request-Headers'), ] as $key => $value) { From 2ca7ab4ccfe19985bafd292d50c2e639e5f274d2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 09:41:21 +0200 Subject: [PATCH 02/13] Created new entity Tag and migration to create new tables --- data/migrations/Version20160820191203.php | 72 +++++++++++++++++++++++ module/Core/src/Entity/ShortUrl.php | 38 ++++++++++++ module/Core/src/Entity/Tag.php | 40 +++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 data/migrations/Version20160820191203.php create mode 100644 module/Core/src/Entity/Tag.php diff --git a/data/migrations/Version20160820191203.php b/data/migrations/Version20160820191203.php new file mode 100644 index 00000000..10fb2b08 --- /dev/null +++ b/data/migrations/Version20160820191203.php @@ -0,0 +1,72 @@ +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'); + } +} diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index 1100a363..f8964eb5 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -42,6 +42,16 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable * @ORM\OneToMany(targetEntity=Visit::class, mappedBy="shortUrl", fetch="EXTRA_LAZY") */ 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. @@ -125,6 +135,34 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable 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 * @link http://php.net/manual/en/jsonserializable.jsonserialize.php diff --git a/module/Core/src/Entity/Tag.php b/module/Core/src/Entity/Tag.php new file mode 100644 index 00000000..7f10cfdf --- /dev/null +++ b/module/Core/src/Entity/Tag.php @@ -0,0 +1,40 @@ +name; + } + + /** + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + return $this; + } +} From e3021120e3daa7e152a9c7f8935a2a0a624a03d2 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 09:45:36 +0200 Subject: [PATCH 03/13] Checked tables do not exist before creating them --- data/migrations/Version20160820191203.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/data/migrations/Version20160820191203.php b/data/migrations/Version20160820191203.php index 10fb2b08..29e81040 100644 --- a/data/migrations/Version20160820191203.php +++ b/data/migrations/Version20160820191203.php @@ -16,6 +16,14 @@ class Version20160820191203 extends AbstractMigration */ 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); } From 2b89556c0959365c2c97c7f467ee21ea0dbdad3c Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 10:17:17 +0200 Subject: [PATCH 04/13] Allowed to display tags in the shortcode:list command --- .../Shortcode/ListShortcodesCommand.php | 30 +++++++++++++++++-- .../Shortcode/ListShortcodesCommandTest.php | 17 +++++++++++ module/Core/src/Entity/ShortUrl.php | 2 ++ 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php index d57d3311..a3f77903 100644 --- a/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php +++ b/module/CLI/src/Command/Shortcode/ListShortcodesCommand.php @@ -55,12 +55,20 @@ class ListShortcodesCommand extends Command PaginableRepositoryAdapter::ITEMS_PER_PAGE ), 1 + ) + ->addOption( + 'tags', + 't', + InputOption::VALUE_NONE, + $this->translator->translate('Whether to display the tags or not') ); } public function execute(InputInterface $input, OutputInterface $output) { $page = intval($input->getOption('page')); + $showTags = $input->getOption('tags'); + /** @var QuestionHelper $helper */ $helper = $this->getHelper('question'); @@ -68,15 +76,31 @@ class ListShortcodesCommand extends Command $result = $this->shortUrlService->listShortUrls($page); $page++; $table = new Table($output); - $table->setHeaders([ + + $headers = [ $this->translator->translate('Short code'), $this->translator->translate('Original URL'), $this->translator->translate('Date created'), $this->translator->translate('Visits count'), - ]); + ]; + if ($showTags) { + $headers[] = $this->translator->translate('Tags'); + } + $table->setHeaders($headers); foreach ($result as $row) { - $table->addRow(array_values($row->jsonSerialize())); + $shortUrl = $row->jsonSerialize(); + if ($showTags) { + $shortUrl['tags'] = []; + foreach ($row->getTags() as $tag) { + $shortUrl['tags'][] = $tag->getName(); + } + $shortUrl['tags'] = implode(', ', $shortUrl['tags']); + } else { + unset($shortUrl['tags']); + } + + $table->addRow(array_values($shortUrl)); } $table->render(); diff --git a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php index d8e4a685..a426eb06 100644 --- a/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php +++ b/module/CLI/test/Command/Shortcode/ListShortcodesCommandTest.php @@ -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) { $stream = fopen('php://memory', 'r+', false); diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index f8964eb5..c89324ad 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -61,6 +61,7 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable $this->setDateCreated(new \DateTime()); $this->setVisits(new ArrayCollection()); $this->setShortCode(''); + $this->tags = new ArrayCollection(); } /** @@ -177,6 +178,7 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable 'originalUrl' => $this->originalUrl, 'dateCreated' => isset($this->dateCreated) ? $this->dateCreated->format(\DateTime::ISO8601) : null, 'visitsCount' => count($this->visits), + 'tags' => $this->tags, ]; } } From 1cf6c9300760c92f12a0467443580f8c3d782d8d Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 10:39:00 +0200 Subject: [PATCH 05/13] Added option to pass tags when creating a short code from the command line --- .../Shortcode/GenerateShortcodeCommand.php | 22 +++++++++++++--- .../GenerateShortcodeCommandTest.php | 8 +++--- module/Core/src/Service/UrlShortener.php | 11 +++++--- .../src/Service/UrlShortenerInterface.php | 3 ++- module/Core/src/Util/TagManagerTrait.php | 26 +++++++++++++++++++ .../Rest/src/Action/CreateShortcodeAction.php | 4 +-- 6 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 module/Core/src/Util/TagManagerTrait.php diff --git a/module/CLI/src/Command/Shortcode/GenerateShortcodeCommand.php b/module/CLI/src/Command/Shortcode/GenerateShortcodeCommand.php index 2830fb40..eaa2d634 100644 --- a/module/CLI/src/Command/Shortcode/GenerateShortcodeCommand.php +++ b/module/CLI/src/Command/Shortcode/GenerateShortcodeCommand.php @@ -9,6 +9,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\Question; use Zend\Diactoros\Uri; @@ -54,7 +55,13 @@ class GenerateShortcodeCommand extends Command ->setDescription( $this->translator->translate('Generates a short code for provided URL and returns the short URL') ) - ->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse')); + ->addArgument('longUrl', InputArgument::REQUIRED, $this->translator->translate('The long URL to parse')) + ->addOption( + 'tags', + 't', + InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, + $this->translator->translate('Tags to apply to the new short URL') + ); } public function interact(InputInterface $input, OutputInterface $output) @@ -80,6 +87,13 @@ class GenerateShortcodeCommand extends Command public function execute(InputInterface $input, OutputInterface $output) { $longUrl = $input->getArgument('longUrl'); + $tags = $input->getOption('tags'); + $processedTags = []; + foreach ($tags as $key => $tag) { + $explodedTags = explode(',', $tag); + $processedTags = array_merge($processedTags, $explodedTags); + } + $tags = $processedTags; try { if (! isset($longUrl)) { @@ -87,10 +101,10 @@ class GenerateShortcodeCommand extends Command return; } - $shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); + $shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl), $tags); $shortUrl = (new Uri())->withPath($shortCode) - ->withScheme($this->domainConfig['schema']) - ->withHost($this->domainConfig['hostname']); + ->withScheme($this->domainConfig['schema']) + ->withHost($this->domainConfig['hostname']); $output->writeln([ sprintf('%s %s', $this->translator->translate('Processed URL:'), $longUrl), diff --git a/module/CLI/test/Command/Shortcode/GenerateShortcodeCommandTest.php b/module/CLI/test/Command/Shortcode/GenerateShortcodeCommandTest.php index 43367f31..e2bf9c6e 100644 --- a/module/CLI/test/Command/Shortcode/GenerateShortcodeCommandTest.php +++ b/module/CLI/test/Command/Shortcode/GenerateShortcodeCommandTest.php @@ -39,8 +39,8 @@ class GenerateShortcodeCommandTest extends TestCase */ public function properShortCodeIsCreatedIfLongUrlIsCorrect() { - $this->urlShortener->urlToShortCode(Argument::any())->willReturn('abc123') - ->shouldBeCalledTimes(1); + $this->urlShortener->urlToShortCode(Argument::cetera())->willReturn('abc123') + ->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shortcode:generate', @@ -55,8 +55,8 @@ class GenerateShortcodeCommandTest extends TestCase */ public function exceptionWhileParsingLongUrlOutputsError() { - $this->urlShortener->urlToShortCode(Argument::any())->willThrow(new InvalidUrlException()) - ->shouldBeCalledTimes(1); + $this->urlShortener->urlToShortCode(Argument::cetera())->willThrow(new InvalidUrlException()) + ->shouldBeCalledTimes(1); $this->commandTester->execute([ 'command' => 'shortcode:generate', diff --git a/module/Core/src/Service/UrlShortener.php b/module/Core/src/Service/UrlShortener.php index ed87b604..38be02f5 100644 --- a/module/Core/src/Service/UrlShortener.php +++ b/module/Core/src/Service/UrlShortener.php @@ -12,9 +12,12 @@ use Shlinkio\Shlink\Common\Exception\RuntimeException; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Exception\InvalidUrlException; +use Shlinkio\Shlink\Core\Util\TagManagerTrait; class UrlShortener implements UrlShortenerInterface { + use TagManagerTrait; + const DEFAULT_CHARS = '123456789bcdfghjkmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ'; /** @@ -59,15 +62,16 @@ class UrlShortener implements UrlShortenerInterface * Creates and persists a unique shortcode generated for provided url * * @param UriInterface $url + * @param string[] $tags * @return string * @throws InvalidUrlException * @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 $shortUrl = $this->em->getRepository(ShortUrl::class)->findOneBy([ - 'originalUrl' => $url + 'originalUrl' => $url, ]); if (isset($shortUrl)) { return $shortUrl->getShortCode(); @@ -88,7 +92,8 @@ class UrlShortener implements UrlShortenerInterface // Generate the short code and persist it $shortCode = $this->convertAutoincrementIdToShortCode($shortUrl->getId()); - $shortUrl->setShortCode($shortCode); + $shortUrl->setShortCode($shortCode) + ->setTags($this->tagNamesToEntities($this->em, $tags)); $this->em->flush(); $this->em->commit(); diff --git a/module/Core/src/Service/UrlShortenerInterface.php b/module/Core/src/Service/UrlShortenerInterface.php index 07d45401..4ddf4b7b 100644 --- a/module/Core/src/Service/UrlShortenerInterface.php +++ b/module/Core/src/Service/UrlShortenerInterface.php @@ -12,11 +12,12 @@ interface UrlShortenerInterface * Creates and persists a unique shortcode generated for provided url * * @param UriInterface $url + * @param string[] $tags * @return string * @throws InvalidUrlException * @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 diff --git a/module/Core/src/Util/TagManagerTrait.php b/module/Core/src/Util/TagManagerTrait.php new file mode 100644 index 00000000..f4dec468 --- /dev/null +++ b/module/Core/src/Util/TagManagerTrait.php @@ -0,0 +1,26 @@ +getRepository(Tag::class)->findOneBy(['name' => $tagName]) ?: (new Tag())->setName($tagName); + $em->persist($tag); + $entities[] = $tag; + } + + return new Collections\ArrayCollection($entities); + } +} diff --git a/module/Rest/src/Action/CreateShortcodeAction.php b/module/Rest/src/Action/CreateShortcodeAction.php index 5dd1221e..3f249c0d 100644 --- a/module/Rest/src/Action/CreateShortcodeAction.php +++ b/module/Rest/src/Action/CreateShortcodeAction.php @@ -16,7 +16,7 @@ use Zend\I18n\Translator\TranslatorInterface; class CreateShortcodeAction extends AbstractRestAction { /** - * @var UrlShortener|UrlShortenerInterface + * @var UrlShortenerInterface */ private $urlShortener; /** @@ -31,7 +31,7 @@ class CreateShortcodeAction extends AbstractRestAction /** * GenerateShortcodeMiddleware constructor. * - * @param UrlShortenerInterface|UrlShortener $urlShortener + * @param UrlShortenerInterface $urlShortener * @param TranslatorInterface $translator * @param array $domainConfig * @param LoggerInterface|null $logger From 322180bde40dea2e92259932f02914eba614b52b Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 12:48:31 +0200 Subject: [PATCH 06/13] Added tag property to json serialization of ShortUrl --- module/Core/src/Entity/ShortUrl.php | 2 +- module/Core/src/Entity/Tag.php | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php index c89324ad..cf1ed87e 100644 --- a/module/Core/src/Entity/ShortUrl.php +++ b/module/Core/src/Entity/ShortUrl.php @@ -178,7 +178,7 @@ class ShortUrl extends AbstractEntity implements \JsonSerializable 'originalUrl' => $this->originalUrl, 'dateCreated' => isset($this->dateCreated) ? $this->dateCreated->format(\DateTime::ISO8601) : null, 'visitsCount' => count($this->visits), - 'tags' => $this->tags, + 'tags' => $this->tags->toArray(), ]; } } diff --git a/module/Core/src/Entity/Tag.php b/module/Core/src/Entity/Tag.php index 7f10cfdf..47123ff9 100644 --- a/module/Core/src/Entity/Tag.php +++ b/module/Core/src/Entity/Tag.php @@ -12,7 +12,7 @@ use Shlinkio\Shlink\Common\Entity\AbstractEntity; * @ORM\Entity() * @ORM\Table(name="tags") */ -class Tag extends AbstractEntity +class Tag extends AbstractEntity implements \JsonSerializable { /** * @var string @@ -37,4 +37,16 @@ class Tag extends AbstractEntity $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 json_encode, + * which is a value of any type other than a resource. + * @since 5.4.0 + */ + public function jsonSerialize() + { + return $this->name; + } } From b6fee0ebaf68ee552704500c535fa11d9b630a57 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 13:07:12 +0200 Subject: [PATCH 07/13] Added option to set tags while creating short code from rest API --- module/Rest/src/Action/CreateShortcodeAction.php | 3 ++- .../test/Action/CreateShortcodeActionTest.php | 15 +++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/module/Rest/src/Action/CreateShortcodeAction.php b/module/Rest/src/Action/CreateShortcodeAction.php index 3f249c0d..fdecedef 100644 --- a/module/Rest/src/Action/CreateShortcodeAction.php +++ b/module/Rest/src/Action/CreateShortcodeAction.php @@ -66,9 +66,10 @@ class CreateShortcodeAction extends AbstractRestAction ], 400); } $longUrl = $postData['longUrl']; + $tags = isset($postData['tags']) && is_array($postData['tags']) ? $postData['tags'] : []; try { - $shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl)); + $shortCode = $this->urlShortener->urlToShortCode(new Uri($longUrl), $tags); $shortUrl = (new Uri())->withPath($shortCode) ->withScheme($this->domainConfig['schema']) ->withHost($this->domainConfig['hostname']); diff --git a/module/Rest/test/Action/CreateShortcodeActionTest.php b/module/Rest/test/Action/CreateShortcodeActionTest.php index dcc56bf6..c3b9e3ff 100644 --- a/module/Rest/test/Action/CreateShortcodeActionTest.php +++ b/module/Rest/test/Action/CreateShortcodeActionTest.php @@ -47,8 +47,9 @@ class CreateShortcodeActionTest extends TestCase */ public function properShortcodeConversionReturnsData() { - $this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willReturn('abc123') - ->shouldBeCalledTimes(1); + $this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array')) + ->willReturn('abc123') + ->shouldBeCalledTimes(1); $request = ServerRequestFactory::fromGlobals()->withParsedBody([ 'longUrl' => 'http://www.domain.com/foo/bar', @@ -63,8 +64,9 @@ class CreateShortcodeActionTest extends TestCase */ public function anInvalidUrlReturnsError() { - $this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willThrow(InvalidUrlException::class) - ->shouldBeCalledTimes(1); + $this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array')) + ->willThrow(InvalidUrlException::class) + ->shouldBeCalledTimes(1); $request = ServerRequestFactory::fromGlobals()->withParsedBody([ 'longUrl' => 'http://www.domain.com/foo/bar', @@ -79,8 +81,9 @@ class CreateShortcodeActionTest extends TestCase */ public function aGenericExceptionWillReturnError() { - $this->urlShortener->urlToShortCode(Argument::type(Uri::class))->willThrow(\Exception::class) - ->shouldBeCalledTimes(1); + $this->urlShortener->urlToShortCode(Argument::type(Uri::class), Argument::type('array')) + ->willThrow(\Exception::class) + ->shouldBeCalledTimes(1); $request = ServerRequestFactory::fromGlobals()->withParsedBody([ 'longUrl' => 'http://www.domain.com/foo/bar', From 372488cbb4c2d65f544c0601c144d1d403882f70 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 13:52:15 +0200 Subject: [PATCH 08/13] Created middleware to parse PUT request bodies in rest requests --- module/Rest/config/dependencies.config.php | 2 + .../config/middleware-pipeline.config.php | 1 + .../src/Middleware/BodyParserMiddleware.php | 52 ++++++++++++ .../Middleware/BodyParserMiddlewareTest.php | 79 +++++++++++++++++++ 4 files changed, 134 insertions(+) create mode 100644 module/Rest/src/Middleware/BodyParserMiddleware.php create mode 100644 module/Rest/test/Middleware/BodyParserMiddlewareTest.php diff --git a/module/Rest/config/dependencies.config.php b/module/Rest/config/dependencies.config.php index 685de2c3..a6b51cb7 100644 --- a/module/Rest/config/dependencies.config.php +++ b/module/Rest/config/dependencies.config.php @@ -18,7 +18,9 @@ return [ Action\ResolveUrlAction::class => AnnotatedFactory::class, Action\GetVisitsAction::class => AnnotatedFactory::class, Action\ListShortcodesAction::class => AnnotatedFactory::class, + Action\EditTagsAction::class => AnnotatedFactory::class, + Middleware\BodyParserMiddleware::class => AnnotatedFactory::class, Middleware\CrossDomainMiddleware::class => InvokableFactory::class, Middleware\CheckAuthenticationMiddleware::class => AnnotatedFactory::class, ], diff --git a/module/Rest/config/middleware-pipeline.config.php b/module/Rest/config/middleware-pipeline.config.php index 78c20c38..817fc568 100644 --- a/module/Rest/config/middleware-pipeline.config.php +++ b/module/Rest/config/middleware-pipeline.config.php @@ -7,6 +7,7 @@ return [ 'rest' => [ 'path' => '/rest', 'middleware' => [ + Middleware\BodyParserMiddleware::class, Middleware\CheckAuthenticationMiddleware::class, Middleware\CrossDomainMiddleware::class, ], diff --git a/module/Rest/src/Middleware/BodyParserMiddleware.php b/module/Rest/src/Middleware/BodyParserMiddleware.php new file mode 100644 index 00000000..598f53ca --- /dev/null +++ b/module/Rest/src/Middleware/BodyParserMiddleware.php @@ -0,0 +1,52 @@ +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); + } +} diff --git a/module/Rest/test/Middleware/BodyParserMiddlewareTest.php b/module/Rest/test/Middleware/BodyParserMiddlewareTest.php new file mode 100644 index 00000000..17c3057b --- /dev/null +++ b/module/Rest/test/Middleware/BodyParserMiddlewareTest.php @@ -0,0 +1,79 @@ +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()); + }); + } +} From 1da285a63a9aadef213925e4d7c9d40589cfa6fd Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 16:52:26 +0200 Subject: [PATCH 09/13] Created action to set the togas for a short url --- .../Exception/InvalidShortCodeException.php | 7 +- module/Core/src/Service/ShortUrlService.php | 26 +++++++ .../src/Service/ShortUrlServiceInterface.php | 9 +++ module/Core/src/Service/UrlShortener.php | 2 +- module/Core/src/Util/TagManagerTrait.php | 12 ++++ module/Rest/config/routes.config.php | 6 ++ module/Rest/src/Action/EditTagsAction.php | 70 +++++++++++++++++++ 7 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 module/Rest/src/Action/EditTagsAction.php diff --git a/module/Core/src/Exception/InvalidShortCodeException.php b/module/Core/src/Exception/InvalidShortCodeException.php index b1e23d0b..85cc3d54 100644 --- a/module/Core/src/Exception/InvalidShortCodeException.php +++ b/module/Core/src/Exception/InvalidShortCodeException.php @@ -5,7 +5,7 @@ use Shlinkio\Shlink\Common\Exception\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; return new static( @@ -14,4 +14,9 @@ class InvalidShortCodeException extends RuntimeException $previous ); } + + public static function fromNotFoundShortCode($shortCode) + { + return new static(sprintf('Provided short code "%s" does not belong to a short URL', $shortCode)); + } } diff --git a/module/Core/src/Service/ShortUrlService.php b/module/Core/src/Service/ShortUrlService.php index 84552115..60845b88 100644 --- a/module/Core/src/Service/ShortUrlService.php +++ b/module/Core/src/Service/ShortUrlService.php @@ -5,11 +5,15 @@ use Acelaya\ZsmAnnotatedServices\Annotation\Inject; use Doctrine\ORM\EntityManagerInterface; use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter; use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Shlinkio\Shlink\Core\Repository\ShortUrlRepository; +use Shlinkio\Shlink\Core\Util\TagManagerTrait; use Zend\Paginator\Paginator; class ShortUrlService implements ShortUrlServiceInterface { + use TagManagerTrait; + /** * @var EntityManagerInterface */ @@ -40,4 +44,26 @@ class ShortUrlService implements ShortUrlServiceInterface 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; + } } diff --git a/module/Core/src/Service/ShortUrlServiceInterface.php b/module/Core/src/Service/ShortUrlServiceInterface.php index c6885c39..5ad304ee 100644 --- a/module/Core/src/Service/ShortUrlServiceInterface.php +++ b/module/Core/src/Service/ShortUrlServiceInterface.php @@ -2,6 +2,7 @@ namespace Shlinkio\Shlink\Core\Service; use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Exception\InvalidShortCodeException; use Zend\Paginator\Paginator; interface ShortUrlServiceInterface @@ -11,4 +12,12 @@ interface ShortUrlServiceInterface * @return ShortUrl[]|Paginator */ public function listShortUrls($page = 1); + + /** + * @param string $shortCode + * @param string[] $tags + * @return ShortUrl + * @throws InvalidShortCodeException + */ + public function setTagsByShortCode($shortCode, array $tags = []); } diff --git a/module/Core/src/Service/UrlShortener.php b/module/Core/src/Service/UrlShortener.php index 38be02f5..bc422cab 100644 --- a/module/Core/src/Service/UrlShortener.php +++ b/module/Core/src/Service/UrlShortener.php @@ -161,7 +161,7 @@ class UrlShortener implements UrlShortenerInterface // Validate short code format if (! preg_match('|[' . $this->chars . "]+|", $shortCode)) { - throw InvalidShortCodeException::fromShortCode($shortCode, $this->chars); + throw InvalidShortCodeException::fromCharset($shortCode, $this->chars); } /** @var ShortUrl $shortUrl */ diff --git a/module/Core/src/Util/TagManagerTrait.php b/module/Core/src/Util/TagManagerTrait.php index f4dec468..9ca02b92 100644 --- a/module/Core/src/Util/TagManagerTrait.php +++ b/module/Core/src/Util/TagManagerTrait.php @@ -16,6 +16,7 @@ trait TagManagerTrait { $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; @@ -23,4 +24,15 @@ trait TagManagerTrait 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))); + } } diff --git a/module/Rest/config/routes.config.php b/module/Rest/config/routes.config.php index 4cc0f510..d27b9c6d 100644 --- a/module/Rest/config/routes.config.php +++ b/module/Rest/config/routes.config.php @@ -34,6 +34,12 @@ return [ 'middleware' => Action\GetVisitsAction::class, 'allowed_methods' => ['GET', 'OPTIONS'], ], + [ + 'name' => 'rest-edit-tags', + 'path' => '/rest/short-codes/{shortCode}/tags', + 'middleware' => Action\EditTagsAction::class, + 'allowed_methods' => ['PUT', 'OPTIONS'], + ], ], ]; diff --git a/module/Rest/src/Action/EditTagsAction.php b/module/Rest/src/Action/EditTagsAction.php new file mode 100644 index 00000000..680980ed --- /dev/null +++ b/module/Rest/src/Action/EditTagsAction.php @@ -0,0 +1,70 @@ +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 $out($request, $response->withStatus(404), 'Not found'); + } + } +} From d7b18776f1610e8ffcc48a1536e3a623f89328d9 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 17:55:26 +0200 Subject: [PATCH 10/13] Improved error response in edit tag request --- module/Rest/src/Action/EditTagsAction.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/module/Rest/src/Action/EditTagsAction.php b/module/Rest/src/Action/EditTagsAction.php index 680980ed..3dd76333 100644 --- a/module/Rest/src/Action/EditTagsAction.php +++ b/module/Rest/src/Action/EditTagsAction.php @@ -64,7 +64,10 @@ class EditTagsAction extends AbstractRestAction $shortUrl = $this->shortUrlService->setTagsByShortCode($shortCode, $tags); return new JsonResponse(['tags' => $shortUrl->getTags()->toArray()]); } catch (InvalidShortCodeException $e) { - return $out($request, $response->withStatus(404), 'Not found'); + return new JsonResponse([ + 'error' => RestUtils::getRestErrorCodeFromException($e), + 'message' => sprintf($this->translator->translate('No URL found for short code "%s"'), $shortCode), + ], 404); } } } From 33767251523b5250e04ae288506937f4a5c24545 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 18:06:34 +0200 Subject: [PATCH 11/13] Created EditTagsActiontest --- .../Rest/test/Action/EditTagsActionTest.php | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 module/Rest/test/Action/EditTagsActionTest.php diff --git a/module/Rest/test/Action/EditTagsActionTest.php b/module/Rest/test/Action/EditTagsActionTest.php new file mode 100644 index 00000000..b0813519 --- /dev/null +++ b/module/Rest/test/Action/EditTagsActionTest.php @@ -0,0 +1,76 @@ +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()); + } +} From 4185015befd7d585229b89a3ef87dbc5be3e93f7 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 18:15:25 +0200 Subject: [PATCH 12/13] Improved tests --- module/Core/test/Entity/TagTest.php | 18 +++++++++ .../Core/test/Service/ShortUrlServiceTest.php | 40 +++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 module/Core/test/Entity/TagTest.php diff --git a/module/Core/test/Entity/TagTest.php b/module/Core/test/Entity/TagTest.php new file mode 100644 index 00000000..b41d7a36 --- /dev/null +++ b/module/Core/test/Entity/TagTest.php @@ -0,0 +1,18 @@ +setName('This is my name'); + $this->assertEquals($tag->getName(), $tag->jsonSerialize()); + } +} diff --git a/module/Core/test/Service/ShortUrlServiceTest.php b/module/Core/test/Service/ShortUrlServiceTest.php index 1244d3a5..a87f91e1 100644 --- a/module/Core/test/Service/ShortUrlServiceTest.php +++ b/module/Core/test/Service/ShortUrlServiceTest.php @@ -2,10 +2,12 @@ namespace ShlinkioTest\Shlink\Core\Service; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; use PHPUnit_Framework_TestCase as TestCase; use Prophecy\Argument; use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Entity\ShortUrl; +use Shlinkio\Shlink\Core\Entity\Tag; use Shlinkio\Shlink\Core\Repository\ShortUrlRepository; use Shlinkio\Shlink\Core\Service\ShortUrlService; @@ -23,6 +25,8 @@ class ShortUrlServiceTest extends TestCase public function setUp() { $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()); } @@ -46,4 +50,40 @@ class ShortUrlServiceTest extends TestCase $list = $this->service->listShortUrls(); $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']); + } } From ad6ef22b72d504149485d877fa262399b37b5345 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 21 Aug 2016 18:17:39 +0200 Subject: [PATCH 13/13] Updated languages --- module/CLI/lang/es.mo | Bin 5982 -> 6214 bytes module/CLI/lang/es.po | 13 +++++++++++-- module/Core/lang/es.mo | Bin 1036 -> 1084 bytes module/Core/lang/es.po | 6 +++--- module/Rest/lang/es.mo | Bin 2189 -> 2283 bytes module/Rest/lang/es.po | 13 ++++++++----- 6 files changed, 22 insertions(+), 10 deletions(-) diff --git a/module/CLI/lang/es.mo b/module/CLI/lang/es.mo index f0702c6dee8ec57585a6aa0e11110de3bb60a955..876787157c7561472ec5d4d9ad930cbf7d2d0184 100644 GIT binary patch delta 1547 zcmX}sSx8h-9LMqhq?zWln3-kUaY=0(w_GYK%Y{loO2r}yE_q37bZllUqF{;&iU^_w ze2R#O9(oazilCRE_g;d)>?w-sDX1VM`u?V|Klj|vnLGEMbMF6tW~M@~D-%u#i!;@CvNQ7OcQ?$eQdi z>iH+A48Fp0=C`zLGY;FpiEGw?%1AR7;cl$P6R1+$#WtM4bvTWASjH-q@@=T=J*e@{ zBZpn%r13_v0AFAtXO`JpI!n3mndK=10rK61#n^#;*otGg8h_z>%wK4>5}UCTJ=ESA zLuFz%_0hNms8ZG-HMh;E=Q?u8zrHx0`eGQ%`Fs)A;$zgCy+x(?8)~=y#!|e&Xx;b} zwa_4S)Px6-HCPli{zcTDxr)lnLsVwoQu{iWJB9EWNmg6_u&Jq!k?&d%c-BnsYkYrwI=8&m7TZ?&!TqkOFV$@u@37f ztZF;RNpEr)b^ShS!gr|Denn;G4?0{!Is&M^O4~rw#Kp9Yv;_CrX4(dt22ffQjsHk) zICU#hpGj0QRKHNFzb2*7PmGuSA7%8EVJ)hfrYvjo>F7^d_peLTQ?b5<1F` z_Jl&4Zzhx__HHs%l6snIuexugDQao@&roT!4;1=kX%8r}^;-NNBx|DIs8Xw_q!rOr zJEgaVmW=U_>8mqJy<;)!=H%iL=M4{s&!`jc_uY_x+Qs@K(YWi`-!VQbI}|AF?f3a6 zsyq4yV#8sNo4GTt$0JcU6uA`&_GVUBxvGXORdu(n2i=_1hw&%DjI7$$_`t~#Kkmis zf6sZ$8Xh?6Md^h-H#Fj(@-(CKjxx8`=Z!zlyA&wx8gMb+_4zU1bAypsJjw%MFXsH2 MAzeiCH|7`p1=Gi;&Hw-a delta 1333 zcmYk+OGs2v9LMqhG|r6XBOhsFqc)Q{nN6c(He+I`JzQjAqJfKAh$KCRT$B;D7{W~p zp~V!`BHBtwi)IrQR3vSJT0}vc38}E4sHK~v@9)knI(Y8q+%w+u`2WwnFFW$_OuoA4 zrcriN8>pW>X2ZDPEstgR3D@H?!Fgs}#-urAa@12}<4F^&(g9G6fF|A{FqVwQI7Mb=_z z)bkfm5uC+V=C>C#O7T1LXDeJf(MzX7pTt2tf^B#MYw;;6CGXt+AE@VptX2zd=CTnx zuxgFj5C*wF!78-Ti_d3F8!E65&ne1xikhUhP&=twfQk;0&Vh<{ z(d@K%Bak!aoNg_CD^;(Ua;vm{}zQ+rgnvt2Ft?%_a@g@?k~w*shjm=TY`~4cxi7* diff --git a/module/CLI/lang/es.po b/module/CLI/lang/es.po index d5e49206..b2ac0670 100644 --- a/module/CLI/lang/es.po +++ b/module/CLI/lang/es.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: Shlink 1.0\n" -"POT-Creation-Date: 2016-08-18 17:24+0200\n" -"PO-Revision-Date: 2016-08-18 17:26+0200\n" +"POT-Creation-Date: 2016-08-21 18:16+0200\n" +"PO-Revision-Date: 2016-08-21 18:16+0200\n" "Last-Translator: Alejandro Celaya \n" "Language-Team: \n" "Language: es_ES\n" @@ -104,6 +104,9 @@ msgstr "" msgid "The long URL to parse" msgstr "La URL larga a procesar" +msgid "Tags to apply to the new short URL" +msgstr "Etiquetas a aplicar a la nueva URL acortada" + msgid "A long URL was not provided. Which URL do you want to shorten?:" msgstr "No se ha proporcionado una URL larga. ¿Qué URL deseas acortar?" @@ -159,6 +162,9 @@ msgstr "Listar todas las URLs cortas" msgid "The first page to list (%s items per page)" msgstr "La primera página a listar (%s elementos por página)" +msgid "Whether to display the tags or not" +msgstr "Si se desea mostrar las etiquetas o no" + msgid "Short code" msgstr "Código corto" @@ -171,6 +177,9 @@ msgstr "Fecha de creación" msgid "Visits count" msgstr "Número de visitas" +msgid "Tags" +msgstr "Etiquetas" + msgid "You have reached last page" msgstr "Has alcanzado la última página" diff --git a/module/Core/lang/es.mo b/module/Core/lang/es.mo index d34bb83b2b79815b5e36833dd99a0c223c0b994a..efc80eca6eb74a8dedc805d72356ebe012fa890b 100644 GIT binary patch delta 159 zcmeC-*uycwN6(3gfx(Lrh#43ddYKs**n#v7AguwU9{_1VAkE3bz@Px6rCBysaWHaO z=o%R+7+P2vnosUwlnX>~wGE6647hv}i%WDviW2jRa}rDPi>wqJb5gSs^HPfP6`WIZ c5-SrGY!YE24scd7SV}KBKi6*aX+}pT0I%C8+yDRo delta 111 zcmdnP(ZeypM^A`}fx(Lrh#43dVwo8j*n#v4AguwUHvnltAbk@^D*)-&%p0pX7`e=K xjSLkG&8$ofCigJPDZ;si+6G1j23$Ug#U;8SMTvREIf*6tMOF%%e=|BV0RZ}E7McJ6 diff --git a/module/Core/lang/es.po b/module/Core/lang/es.po index 3393f072..3a3d4809 100644 --- a/module/Core/lang/es.po +++ b/module/Core/lang/es.po @@ -1,9 +1,9 @@ msgid "" msgstr "" "Project-Id-Version: Shlink 1.0\n" -"POT-Creation-Date: 2016-07-21 16:50+0200\n" -"PO-Revision-Date: 2016-07-21 16:51+0200\n" -"Last-Translator: \n" +"POT-Creation-Date: 2016-08-21 18:17+0200\n" +"PO-Revision-Date: 2016-08-21 18:17+0200\n" +"Last-Translator: Alejandro Celaya \n" "Language-Team: \n" "Language: es_ES\n" "MIME-Version: 1.0\n" diff --git a/module/Rest/lang/es.mo b/module/Rest/lang/es.mo index a8a16fdf49ce14f7be433dbf851b506e4ee3b20f..070394c048e2f126405912a9b4245104227e4eb2 100644 GIT binary patch delta 385 zcmXxgJ4?e*7{>7@N!xgb6by>Dw)lGTzW`552~Zetsd(BL;N zqHjy9xQQEhiP`f9oWn01$5BUG#|pB(rE?mO$sSHKaaWWU@EPyXElD+e#5I&F)uD>J zc=0!W!ffAI-|vTP;SS?HT)`XUUGw6)yarT}hG@AvFYeSJ$^HiCi9PBk1(H>%J7rCb z)Xi}tPR!|nNy5W8owwSyyBwIHS_`_j&g_4mzB#X!-#iKP<)PV+Oq4Xvno$zQX|MQh F{{U0(FfRZA delta 331 zcmXxfy=uZx7{>7@F(3VKiJ%}#j6+8YmP*9U;MUEl&DwSS@{!eNjc=LPDAvx#GZ}1dkUp41Nq)N4^dn%w-lu2Ys4ea704l%-aJjV?V zux?8eyh5}8jAyvSCVpdrJ2dYn74ET>DWwV7cyOf!zTpUOJt@Xl9OEb6VB7y&r`TgX zM>F^jPSHJ*F7X\n" "Language-Team: \n" "Language: es_ES\n" @@ -35,14 +35,17 @@ 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 does not exist" -msgstr "El código corto \"%s\" proporcionado no existe" +msgid "A list of tags was not provided" +msgstr "No se ha proporcionado una lista de etiquetas" #, php-format 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 does not exist" +msgstr "El código corto \"%s\" proporcionado no existe" + #, php-format msgid "Provided short code \"%s\" has an invalid format" msgstr "El código corto proporcionado \"%s\" tiene un formato no inválido"