From 1da285a63a9aadef213925e4d7c9d40589cfa6fd Mon Sep 17 00:00:00 2001
From: Alejandro Celaya <alejandro@alejandrocelaya.com>
Date: Sun, 21 Aug 2016 16:52:26 +0200
Subject: [PATCH] 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 @@
+<?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 $out($request, $response->withStatus(404), 'Not found');
+        }
+    }
+}