diff --git a/module/Core/src/Paginator/Adapter/ShortUrlRepositoryAdapter.php b/module/Core/src/Paginator/Adapter/ShortUrlRepositoryAdapter.php index 59d48a82..8f339dc0 100644 --- a/module/Core/src/Paginator/Adapter/ShortUrlRepositoryAdapter.php +++ b/module/Core/src/Paginator/Adapter/ShortUrlRepositoryAdapter.php @@ -7,16 +7,19 @@ namespace Shlinkio\Shlink\Core\Paginator\Adapter; use Laminas\Paginator\Adapter\AdapterInterface; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; +use Shlinkio\Shlink\Rest\Entity\ApiKey; class ShortUrlRepositoryAdapter implements AdapterInterface { private ShortUrlRepositoryInterface $repository; private ShortUrlsParams $params; + private ?ApiKey $apiKey; - public function __construct(ShortUrlRepositoryInterface $repository, ShortUrlsParams $params) + public function __construct(ShortUrlRepositoryInterface $repository, ShortUrlsParams $params, ?ApiKey $apiKey) { $this->repository = $repository; $this->params = $params; + $this->apiKey = $apiKey; } public function getItems($offset, $itemCountPerPage): array // phpcs:ignore @@ -28,6 +31,7 @@ class ShortUrlRepositoryAdapter implements AdapterInterface $this->params->tags(), $this->params->orderBy(), $this->params->dateRange(), + $this->apiKey !== null ? $this->apiKey->spec() : null, ); } @@ -37,6 +41,7 @@ class ShortUrlRepositoryAdapter implements AdapterInterface $this->params->searchTerm(), $this->params->tags(), $this->params->dateRange(), + $this->apiKey !== null ? $this->apiKey->spec() : null, ); } } diff --git a/module/Core/src/Repository/ShortUrlRepository.php b/module/Core/src/Repository/ShortUrlRepository.php index fc6ace41..363d3290 100644 --- a/module/Core/src/Repository/ShortUrlRepository.php +++ b/module/Core/src/Repository/ShortUrlRepository.php @@ -4,9 +4,10 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Repository; -use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; +use Happyr\DoctrineSpecification\EntitySpecificationRepository; +use Happyr\DoctrineSpecification\Specification\Specification; use Shlinkio\Shlink\Common\Doctrine\Type\ChronosDateTimeType; use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Entity\ShortUrl; @@ -19,7 +20,7 @@ use function array_key_exists; use function count; use function Functional\contains; -class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryInterface +class ShortUrlRepository extends EntitySpecificationRepository implements ShortUrlRepositoryInterface { /** * @param string[] $tags @@ -31,9 +32,10 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI ?string $searchTerm = null, array $tags = [], ?ShortUrlsOrdering $orderBy = null, - ?DateRange $dateRange = null + ?DateRange $dateRange = null, + ?Specification $spec = null ): array { - $qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange); + $qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange, $spec); $qb->select('DISTINCT s') ->setMaxResults($limit) ->setFirstResult($offset); @@ -75,9 +77,13 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI return $qb->getQuery()->getResult(); } - public function countList(?string $searchTerm = null, array $tags = [], ?DateRange $dateRange = null): int - { - $qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange); + public function countList( + ?string $searchTerm = null, + array $tags = [], + ?DateRange $dateRange = null, + ?Specification $spec = null + ): int { + $qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange, $spec); $qb->select('COUNT(DISTINCT s)'); return (int) $qb->getQuery()->getSingleScalarResult(); @@ -86,7 +92,8 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI private function createListQueryBuilder( ?string $searchTerm = null, array $tags = [], - ?DateRange $dateRange = null + ?DateRange $dateRange = null, + ?Specification $spec = null ): QueryBuilder { $qb = $this->getEntityManager()->createQueryBuilder(); $qb->from(ShortUrl::class, 's') @@ -125,6 +132,10 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI ->andWhere($qb->expr()->in('t.name', $tags)); } + if ($spec) { + $this->applySpecification($qb, $spec, 's'); + } + return $qb; } diff --git a/module/Core/src/Repository/ShortUrlRepositoryInterface.php b/module/Core/src/Repository/ShortUrlRepositoryInterface.php index 1d6f38a8..98bfe778 100644 --- a/module/Core/src/Repository/ShortUrlRepositoryInterface.php +++ b/module/Core/src/Repository/ShortUrlRepositoryInterface.php @@ -5,13 +5,15 @@ declare(strict_types=1); namespace Shlinkio\Shlink\Core\Repository; use Doctrine\Persistence\ObjectRepository; +use Happyr\DoctrineSpecification\EntitySpecificationRepositoryInterface; +use Happyr\DoctrineSpecification\Specification\Specification; use Shlinkio\Shlink\Common\Util\DateRange; use Shlinkio\Shlink\Core\Entity\ShortUrl; use Shlinkio\Shlink\Core\Model\ShortUrlMeta; use Shlinkio\Shlink\Core\Model\ShortUrlsOrdering; use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl; -interface ShortUrlRepositoryInterface extends ObjectRepository +interface ShortUrlRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface { public function findList( ?int $limit = null, @@ -19,10 +21,16 @@ interface ShortUrlRepositoryInterface extends ObjectRepository ?string $searchTerm = null, array $tags = [], ?ShortUrlsOrdering $orderBy = null, - ?DateRange $dateRange = null + ?DateRange $dateRange = null, + ?Specification $spec = null ): array; - public function countList(?string $searchTerm = null, array $tags = [], ?DateRange $dateRange = null): int; + public function countList( + ?string $searchTerm = null, + array $tags = [], + ?DateRange $dateRange = null, + ?Specification $spec = null + ): int; public function findOneWithDomainFallback(string $shortCode, ?string $domain = null): ?ShortUrl; diff --git a/module/Core/src/Service/ShortUrlService.php b/module/Core/src/Service/ShortUrlService.php index 9159ef63..410e0f16 100644 --- a/module/Core/src/Service/ShortUrlService.php +++ b/module/Core/src/Service/ShortUrlService.php @@ -17,6 +17,7 @@ use Shlinkio\Shlink\Core\Repository\ShortUrlRepository; use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface; use Shlinkio\Shlink\Core\Util\TagManagerTrait; use Shlinkio\Shlink\Core\Util\UrlValidatorInterface; +use Shlinkio\Shlink\Rest\Entity\ApiKey; class ShortUrlService implements ShortUrlServiceInterface { @@ -39,11 +40,11 @@ class ShortUrlService implements ShortUrlServiceInterface /** * @return ShortUrl[]|Paginator */ - public function listShortUrls(ShortUrlsParams $params): Paginator + public function listShortUrls(ShortUrlsParams $params, ?ApiKey $apiKey = null): Paginator { /** @var ShortUrlRepository $repo */ $repo = $this->em->getRepository(ShortUrl::class); - $paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $params)); + $paginator = new Paginator(new ShortUrlRepositoryAdapter($repo, $params, $apiKey)); $paginator->setItemCountPerPage($params->itemsPerPage()) ->setCurrentPageNumber($params->page()); diff --git a/module/Core/src/Service/ShortUrlServiceInterface.php b/module/Core/src/Service/ShortUrlServiceInterface.php index 3c09e7e9..b3582ac2 100644 --- a/module/Core/src/Service/ShortUrlServiceInterface.php +++ b/module/Core/src/Service/ShortUrlServiceInterface.php @@ -11,13 +11,14 @@ use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException; use Shlinkio\Shlink\Core\Model\ShortUrlEdit; use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; +use Shlinkio\Shlink\Rest\Entity\ApiKey; interface ShortUrlServiceInterface { /** * @return ShortUrl[]|Paginator */ - public function listShortUrls(ShortUrlsParams $params): Paginator; + public function listShortUrls(ShortUrlsParams $params, ?ApiKey $apiKey = null): Paginator; /** * @param string[] $tags diff --git a/module/Core/test/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php b/module/Core/test/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php index 9f541ebe..c3848aa5 100644 --- a/module/Core/test/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php +++ b/module/Core/test/Paginator/Adapter/ShortUrlRepositoryAdapterTest.php @@ -11,6 +11,7 @@ use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Paginator\Adapter\ShortUrlRepositoryAdapter; use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface; +use Shlinkio\Shlink\Rest\Entity\ApiKey; class ShortUrlRepositoryAdapterTest extends TestCase { @@ -41,11 +42,11 @@ class ShortUrlRepositoryAdapterTest extends TestCase 'endDate' => $endDate, 'orderBy' => $orderBy, ]); - $adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), $params); + $adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), $params, null); $orderBy = $params->orderBy(); $dateRange = $params->dateRange(); - $this->repo->findList(10, 5, $searchTerm, $tags, $orderBy, $dateRange)->shouldBeCalledOnce(); + $this->repo->findList(10, 5, $searchTerm, $tags, $orderBy, $dateRange, null)->shouldBeCalledOnce(); $adapter->getItems(5, 10); } @@ -65,10 +66,11 @@ class ShortUrlRepositoryAdapterTest extends TestCase 'startDate' => $startDate, 'endDate' => $endDate, ]); - $adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), $params); + $apiKey = new ApiKey(); + $adapter = new ShortUrlRepositoryAdapter($this->repo->reveal(), $params, $apiKey); $dateRange = $params->dateRange(); - $this->repo->countList($searchTerm, $tags, $dateRange)->shouldBeCalledOnce(); + $this->repo->countList($searchTerm, $tags, $dateRange, $apiKey->spec())->shouldBeCalledOnce(); $adapter->count(); } diff --git a/module/Rest/src/Action/ShortUrl/ListShortUrlsAction.php b/module/Rest/src/Action/ShortUrl/ListShortUrlsAction.php index 10a0effc..35273dcc 100644 --- a/module/Rest/src/Action/ShortUrl/ListShortUrlsAction.php +++ b/module/Rest/src/Action/ShortUrl/ListShortUrlsAction.php @@ -12,6 +12,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface; use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer; use Shlinkio\Shlink\Rest\Action\AbstractRestAction; +use Shlinkio\Shlink\Rest\Middleware\AuthenticationMiddleware; class ListShortUrlsAction extends AbstractRestAction { @@ -31,7 +32,10 @@ class ListShortUrlsAction extends AbstractRestAction public function handle(Request $request): Response { - $shortUrls = $this->shortUrlService->listShortUrls(ShortUrlsParams::fromRawData($request->getQueryParams())); + $shortUrls = $this->shortUrlService->listShortUrls( + ShortUrlsParams::fromRawData($request->getQueryParams()), + AuthenticationMiddleware::apiKeyFromRequest($request), + ); return new JsonResponse(['shortUrls' => $this->serializePaginator($shortUrls, new ShortUrlDataTransformer( $this->domainConfig, ))]); diff --git a/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php b/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php index 741eceb5..7c4d47f7 100644 --- a/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php +++ b/module/Rest/test/Action/ShortUrl/ListShortUrlsActionTest.php @@ -15,6 +15,7 @@ use Prophecy\Prophecy\ObjectProphecy; use Shlinkio\Shlink\Core\Model\ShortUrlsParams; use Shlinkio\Shlink\Core\Service\ShortUrlService; use Shlinkio\Shlink\Rest\Action\ShortUrl\ListShortUrlsAction; +use Shlinkio\Shlink\Rest\Entity\ApiKey; class ListShortUrlsActionTest extends TestCase { @@ -46,6 +47,8 @@ class ListShortUrlsActionTest extends TestCase ?string $startDate = null, ?string $endDate = null ): void { + $apiKey = new ApiKey(); + $request = (new ServerRequest())->withQueryParams($query)->withAttribute(ApiKey::class, $apiKey); $listShortUrls = $this->service->listShortUrls(ShortUrlsParams::fromRawData([ 'page' => $expectedPage, 'searchTerm' => $expectedSearchTerm, @@ -53,10 +56,10 @@ class ListShortUrlsActionTest extends TestCase 'orderBy' => $expectedOrderBy, 'startDate' => $startDate, 'endDate' => $endDate, - ]))->willReturn(new Paginator(new ArrayAdapter())); + ]), $apiKey)->willReturn(new Paginator(new ArrayAdapter())); /** @var JsonResponse $response */ - $response = $this->action->handle((new ServerRequest())->withQueryParams($query)); + $response = $this->action->handle($request); $payload = $response->getPayload(); self::assertArrayHasKey('shortUrls', $payload);