mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 20:23:12 +08:00
Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d945c28a75 | ||
|
|
b66922b3d5 | ||
|
|
7d981434e1 | ||
|
|
c672d35b4a | ||
|
|
6259c73b33 | ||
|
|
e4b00e832a | ||
|
|
a452aeaf7e | ||
|
|
6e83b90028 | ||
|
|
45ffdce312 | ||
|
|
5485efc9ae | ||
|
|
850360dd2b | ||
|
|
8d3ceaf462 | ||
|
|
bb6c5de697 | ||
|
|
ca4c1b00dc | ||
|
|
dda6d30c12 | ||
|
|
7bb40c74c1 | ||
|
|
4515a83e9b | ||
|
|
907a282b73 | ||
|
|
5154638ddf | ||
|
|
52c9994eb4 | ||
|
|
912f287a27 | ||
|
|
e99ab66afd | ||
|
|
fb022eae68 | ||
|
|
259c52a698 | ||
|
|
deeca582db | ||
|
|
4dbcf6857e | ||
|
|
5190a03113 | ||
|
|
d60c3a4aa9 | ||
|
|
ce1c70fd7c |
14
.github/workflows/docker-image-build.yml
vendored
14
.github/workflows/docker-image-build.yml
vendored
@@ -13,12 +13,16 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install buildx
|
||||
id: buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
with:
|
||||
buildx-version: latest
|
||||
version: latest
|
||||
- name: Login to docker hub
|
||||
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build the image
|
||||
run: bash ./docker/build
|
||||
|
||||
39
CHANGELOG.md
39
CHANGELOG.md
@@ -4,6 +4,45 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||
|
||||
## [2.4.2] - 2020-11-22
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#904](https://github.com/shlinkio/shlink/issues/904) Explicitly added missing "Domains" and "Integrations" tags to swagger docs.
|
||||
* [#901](https://github.com/shlinkio/shlink/issues/901) Ensured domains which are not in use on any short URL are not returned on the list of domains.
|
||||
* [#899](https://github.com/shlinkio/shlink/issues/899) Avoided filesystem errors produced while downloading geolite DB files on several shlink instances that share the same filesystem.
|
||||
* [#827](https://github.com/shlinkio/shlink/issues/827) Fixed swoole config getting loaded in config cache if a console command is run before any web execution, when swoole extension is enabled, making subsequent non-swoole web requests fail.
|
||||
|
||||
|
||||
## [2.4.1] - 2020-11-10
|
||||
### Added
|
||||
* *Nothing*
|
||||
|
||||
### Changed
|
||||
* *Nothing*
|
||||
|
||||
### Deprecated
|
||||
* *Nothing*
|
||||
|
||||
### Removed
|
||||
* *Nothing*
|
||||
|
||||
### Fixed
|
||||
* [#891](https://github.com/shlinkio/shlink/issues/891) Fixed error when running migrations in postgres due to incorrect return type hint.
|
||||
* [#846](https://github.com/shlinkio/shlink/issues/846) Fixed base image used for the PHP-FPM dev container.
|
||||
* [#867](https://github.com/shlinkio/shlink/issues/867) Fixed not-found redirects not using proper status (301 or 302) as configured during installation.
|
||||
|
||||
|
||||
## [2.4.0] - 2020-11-08
|
||||
### Added
|
||||
* [#829](https://github.com/shlinkio/shlink/issues/829) Added support for QR codes in SVG format, by passing `?format=svg` to the QR code URL.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
FROM php:7.4.11-alpine3.12 as base
|
||||
|
||||
ARG SHLINK_VERSION=2.3.0
|
||||
ARG SHLINK_VERSION=2.4.0
|
||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||
ENV SWOOLE_VERSION 4.5.5
|
||||
ENV LC_ALL "C"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"laminas/laminas-paginator": "^2.8",
|
||||
"laminas/laminas-servicemanager": "^3.4",
|
||||
"laminas/laminas-stdlib": "^3.2",
|
||||
"lcobucci/jwt": "^4.0@alpha",
|
||||
"lcobucci/jwt": "^4.0@alpha <4.0@beta",
|
||||
"league/uri": "^6.2",
|
||||
"lstrojny/functional-php": "^1.9",
|
||||
"mezzio/mezzio": "^3.2",
|
||||
|
||||
@@ -7,6 +7,9 @@ use Laminas\ConfigAggregator\ConfigAggregator;
|
||||
return [
|
||||
|
||||
'debug' => false,
|
||||
ConfigAggregator::ENABLE_CACHE => true,
|
||||
|
||||
// Disabling config cache for cli, ensures it's never used for swoole and also that console commands don't generate
|
||||
// a cache file that's then used by non-swoole web executions
|
||||
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',
|
||||
|
||||
];
|
||||
|
||||
@@ -6,8 +6,8 @@ return [
|
||||
|
||||
'geolite2' => [
|
||||
'db_location' => __DIR__ . '/../../data/GeoLite2-City.mmdb',
|
||||
'temp_dir' => sys_get_temp_dir(),
|
||||
'license_key' => 'G4Lm0C60yJsnkdPi',
|
||||
'temp_dir' => __DIR__ . '/../../data',
|
||||
'license_key' => 'G4Lm0C60yJsnkdPi', // Deprecated. Remove hardcoded license on v3
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM php:7.4.11-alpine3.12
|
||||
FROM php:7.4.11-fpm-alpine3.12
|
||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||
|
||||
ENV APCU_VERSION 5.1.18
|
||||
|
||||
@@ -60,7 +60,10 @@ final class Version20201102113208 extends AbstractMigration
|
||||
->execute();
|
||||
}
|
||||
|
||||
private function resolveOneApiKeyId(Result $result): ?string
|
||||
/**
|
||||
* @return string|int|null
|
||||
*/
|
||||
private function resolveOneApiKeyId(Result $result)
|
||||
{
|
||||
$results = [];
|
||||
while ($row = $result->fetchAssociative()) {
|
||||
|
||||
@@ -99,8 +99,6 @@ $helper = new class {
|
||||
|
||||
return [
|
||||
|
||||
'config_cache_enabled' => false,
|
||||
|
||||
'app_options' => [
|
||||
'disable_track_param' => env('DISABLE_TRACK_PARAM'),
|
||||
],
|
||||
|
||||
@@ -50,6 +50,14 @@
|
||||
"name": "Visits",
|
||||
"description": "Operations to manage visits on short URLs"
|
||||
},
|
||||
{
|
||||
"name": "Domains",
|
||||
"description": "Operations to manage domains used on short URLs"
|
||||
},
|
||||
{
|
||||
"name": "Integrations",
|
||||
"description": "Handle services with which shlink is integrated"
|
||||
},
|
||||
{
|
||||
"name": "Monitoring",
|
||||
"description": "Public endpoints designed to monitor the service"
|
||||
|
||||
@@ -36,6 +36,7 @@ return [
|
||||
|
||||
Util\UrlValidator::class => ConfigAbstractFactory::class,
|
||||
Util\DoctrineBatchHelper::class => ConfigAbstractFactory::class,
|
||||
Util\RedirectResponseHelper::class => ConfigAbstractFactory::class,
|
||||
|
||||
Action\RedirectAction::class => ConfigAbstractFactory::class,
|
||||
Action\PixelAction::class => ConfigAbstractFactory::class,
|
||||
@@ -54,7 +55,11 @@ return [
|
||||
],
|
||||
|
||||
ConfigAbstractFactory::class => [
|
||||
ErrorHandler\NotFoundRedirectHandler::class => [NotFoundRedirectOptions::class, 'config.router.base_path'],
|
||||
ErrorHandler\NotFoundRedirectHandler::class => [
|
||||
NotFoundRedirectOptions::class,
|
||||
Util\RedirectResponseHelper::class,
|
||||
'config.router.base_path',
|
||||
],
|
||||
ErrorHandler\NotFoundTemplateHandler::class => [TemplateRendererInterface::class],
|
||||
|
||||
Options\AppOptions::class => ['config.app_options'],
|
||||
@@ -88,12 +93,13 @@ return [
|
||||
|
||||
Util\UrlValidator::class => ['httpClient', Options\UrlShortenerOptions::class],
|
||||
Util\DoctrineBatchHelper::class => ['em'],
|
||||
Util\RedirectResponseHelper::class => [Options\UrlShortenerOptions::class],
|
||||
|
||||
Action\RedirectAction::class => [
|
||||
Service\ShortUrl\ShortUrlResolver::class,
|
||||
Service\VisitsTracker::class,
|
||||
Options\AppOptions::class,
|
||||
Options\UrlShortenerOptions::class,
|
||||
Util\RedirectResponseHelper::class,
|
||||
'Logger_Shlink',
|
||||
],
|
||||
Action\PixelAction::class => [
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Action;
|
||||
|
||||
use Fig\Http\Message\RequestMethodInterface;
|
||||
use GuzzleHttp\Psr7\Query;
|
||||
use League\Uri\Uri;
|
||||
use Mezzio\Router\Middleware\ImplicitHeadMiddleware;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
@@ -23,8 +24,6 @@ use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_merge;
|
||||
use function GuzzleHttp\Psr7\build_query;
|
||||
use function GuzzleHttp\Psr7\parse_query;
|
||||
|
||||
abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMethodInterface
|
||||
{
|
||||
@@ -68,13 +67,13 @@ abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMet
|
||||
private function buildUrlToRedirectTo(ShortUrl $shortUrl, array $currentQuery, ?string $disableTrackParam): string
|
||||
{
|
||||
$uri = Uri::createFromString($shortUrl->getLongUrl());
|
||||
$hardcodedQuery = parse_query($uri->getQuery() ?? '');
|
||||
$hardcodedQuery = Query::parse($uri->getQuery() ?? '');
|
||||
if ($disableTrackParam !== null) {
|
||||
unset($currentQuery[$disableTrackParam]);
|
||||
}
|
||||
$mergedQuery = array_merge($hardcodedQuery, $currentQuery);
|
||||
|
||||
return (string) (empty($mergedQuery) ? $uri : $uri->withQuery(build_query($mergedQuery)));
|
||||
return (string) (empty($mergedQuery) ? $uri : $uri->withQuery(Query::build($mergedQuery)));
|
||||
}
|
||||
|
||||
private function shouldTrackRequest(ServerRequestInterface $request, array $query, ?string $disableTrackParam): bool
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Action;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Laminas\Diactoros\Response\RedirectResponse;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
@@ -13,32 +12,26 @@ use Psr\Log\LoggerInterface;
|
||||
use Shlinkio\Shlink\Core\Options;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
|
||||
use function sprintf;
|
||||
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||
|
||||
class RedirectAction extends AbstractTrackingAction implements StatusCodeInterface
|
||||
{
|
||||
private Options\UrlShortenerOptions $urlShortenerOptions;
|
||||
private RedirectResponseHelperInterface $redirectResponseHelper;
|
||||
|
||||
public function __construct(
|
||||
ShortUrlResolverInterface $urlResolver,
|
||||
VisitsTrackerInterface $visitTracker,
|
||||
Options\AppOptions $appOptions,
|
||||
Options\UrlShortenerOptions $urlShortenerOptions,
|
||||
RedirectResponseHelperInterface $redirectResponseHelper,
|
||||
?LoggerInterface $logger = null
|
||||
) {
|
||||
parent::__construct($urlResolver, $visitTracker, $appOptions, $logger);
|
||||
$this->urlShortenerOptions = $urlShortenerOptions;
|
||||
$this->redirectResponseHelper = $redirectResponseHelper;
|
||||
}
|
||||
|
||||
protected function createSuccessResp(string $longUrl): Response
|
||||
{
|
||||
$statusCode = $this->urlShortenerOptions->redirectStatusCode();
|
||||
$headers = $statusCode === self::STATUS_FOUND ? [] : [
|
||||
'Cache-Control' => sprintf('private,max-age=%s', $this->urlShortenerOptions->redirectCacheLifetime()),
|
||||
];
|
||||
|
||||
return new RedirectResponse($longUrl, $statusCode, $headers);
|
||||
return $this->redirectResponseHelper->buildRedirectResponse($longUrl);
|
||||
}
|
||||
|
||||
protected function createErrorResp(ServerRequestInterface $request, RequestHandlerInterface $handler): Response
|
||||
|
||||
@@ -5,7 +5,9 @@ declare(strict_types=1);
|
||||
namespace Shlinkio\Shlink\Core\Domain\Repository;
|
||||
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
|
||||
class DomainRepository extends EntityRepository implements DomainRepositoryInterface
|
||||
{
|
||||
@@ -14,7 +16,9 @@ class DomainRepository extends EntityRepository implements DomainRepositoryInter
|
||||
*/
|
||||
public function findDomainsWithout(?string $excludedAuthority = null): array
|
||||
{
|
||||
$qb = $this->createQueryBuilder('d')->orderBy('d.authority', 'ASC');
|
||||
$qb = $this->createQueryBuilder('d');
|
||||
$qb->join(ShortUrl::class, 's', Join::WITH, 's.domain = d')
|
||||
->orderBy('d.authority', 'ASC');
|
||||
|
||||
if ($excludedAuthority !== null) {
|
||||
$qb->where($qb->expr()->neq('d.authority', ':excludedAuthority'))
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ErrorHandler;
|
||||
|
||||
use Laminas\Diactoros\Response;
|
||||
use Mezzio\Router\RouteResult;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
@@ -12,19 +11,25 @@ use Psr\Http\Message\UriInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use Shlinkio\Shlink\Core\Options;
|
||||
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||
|
||||
use function rtrim;
|
||||
|
||||
class NotFoundRedirectHandler implements MiddlewareInterface
|
||||
{
|
||||
private NotFoundRedirectOptions $redirectOptions;
|
||||
private Options\NotFoundRedirectOptions $redirectOptions;
|
||||
private RedirectResponseHelperInterface $redirectResponseHelper;
|
||||
private string $shlinkBasePath;
|
||||
|
||||
public function __construct(NotFoundRedirectOptions $redirectOptions, string $shlinkBasePath)
|
||||
{
|
||||
public function __construct(
|
||||
Options\NotFoundRedirectOptions $redirectOptions,
|
||||
RedirectResponseHelperInterface $redirectResponseHelper,
|
||||
string $shlinkBasePath
|
||||
) {
|
||||
$this->redirectOptions = $redirectOptions;
|
||||
$this->shlinkBasePath = $shlinkBasePath;
|
||||
$this->redirectResponseHelper = $redirectResponseHelper;
|
||||
}
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
@@ -41,11 +46,13 @@ class NotFoundRedirectHandler implements MiddlewareInterface
|
||||
$isBaseUrl = rtrim($uri->getPath(), '/') === $this->shlinkBasePath;
|
||||
|
||||
if ($isBaseUrl && $this->redirectOptions->hasBaseUrlRedirect()) {
|
||||
return new Response\RedirectResponse($this->redirectOptions->getBaseUrlRedirect());
|
||||
return $this->redirectResponseHelper->buildRedirectResponse($this->redirectOptions->getBaseUrlRedirect());
|
||||
}
|
||||
|
||||
if (!$isBaseUrl && $routeResult->isFailure() && $this->redirectOptions->hasRegular404Redirect()) {
|
||||
return new Response\RedirectResponse($this->redirectOptions->getRegular404Redirect());
|
||||
return $this->redirectResponseHelper->buildRedirectResponse(
|
||||
$this->redirectOptions->getRegular404Redirect(),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -53,7 +60,9 @@ class NotFoundRedirectHandler implements MiddlewareInterface
|
||||
$routeResult->getMatchedRouteName() === RedirectAction::class &&
|
||||
$this->redirectOptions->hasInvalidShortUrlRedirect()
|
||||
) {
|
||||
return new Response\RedirectResponse($this->redirectOptions->getInvalidShortUrlRedirect());
|
||||
return $this->redirectResponseHelper->buildRedirectResponse(
|
||||
$this->redirectOptions->getInvalidShortUrlRedirect(),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
32
module/Core/src/Util/RedirectResponseHelper.php
Normal file
32
module/Core/src/Util/RedirectResponseHelper.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Util;
|
||||
|
||||
use Fig\Http\Message\StatusCodeInterface;
|
||||
use Laminas\Diactoros\Response\RedirectResponse;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class RedirectResponseHelper implements RedirectResponseHelperInterface
|
||||
{
|
||||
private UrlShortenerOptions $options;
|
||||
|
||||
public function __construct(UrlShortenerOptions $options)
|
||||
{
|
||||
$this->options = $options;
|
||||
}
|
||||
|
||||
public function buildRedirectResponse(string $location): ResponseInterface
|
||||
{
|
||||
$statusCode = $this->options->redirectStatusCode();
|
||||
$headers = $statusCode === StatusCodeInterface::STATUS_FOUND ? [] : [
|
||||
'Cache-Control' => sprintf('private,max-age=%s', $this->options->redirectCacheLifetime()),
|
||||
];
|
||||
|
||||
return new RedirectResponse($location, $statusCode, $headers);
|
||||
}
|
||||
}
|
||||
12
module/Core/src/Util/RedirectResponseHelperInterface.php
Normal file
12
module/Core/src/Util/RedirectResponseHelperInterface.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\Util;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
interface RedirectResponseHelperInterface
|
||||
{
|
||||
public function buildRedirectResponse(string $location): ResponseInterface;
|
||||
}
|
||||
@@ -6,11 +6,15 @@ namespace ShlinkioTest\Shlink\Core\Domain\Repository;
|
||||
|
||||
use Shlinkio\Shlink\Core\Domain\Repository\DomainRepository;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
use Shlinkio\Shlink\TestUtils\DbTest\DatabaseTestCase;
|
||||
|
||||
class DomainRepositoryTest extends DatabaseTestCase
|
||||
{
|
||||
protected const ENTITIES_TO_EMPTY = [Domain::class];
|
||||
protected const ENTITIES_TO_EMPTY = [ShortUrl::class, Domain::class];
|
||||
|
||||
private DomainRepository $repo;
|
||||
|
||||
@@ -23,12 +27,23 @@ class DomainRepositoryTest extends DatabaseTestCase
|
||||
public function findDomainsReturnsExpectedResult(): void
|
||||
{
|
||||
$fooDomain = new Domain('foo.com');
|
||||
$barDomain = new Domain('bar.com');
|
||||
$bazDomain = new Domain('baz.com');
|
||||
|
||||
$this->getEntityManager()->persist($fooDomain);
|
||||
$fooShortUrl = $this->createShortUrl($fooDomain);
|
||||
$this->getEntityManager()->persist($fooShortUrl);
|
||||
|
||||
$barDomain = new Domain('bar.com');
|
||||
$this->getEntityManager()->persist($barDomain);
|
||||
$barShortUrl = $this->createShortUrl($barDomain);
|
||||
$this->getEntityManager()->persist($barShortUrl);
|
||||
|
||||
$bazDomain = new Domain('baz.com');
|
||||
$this->getEntityManager()->persist($bazDomain);
|
||||
$bazShortUrl = $this->createShortUrl($bazDomain);
|
||||
$this->getEntityManager()->persist($bazShortUrl);
|
||||
|
||||
$detachedDomain = new Domain('detached.com');
|
||||
$this->getEntityManager()->persist($detachedDomain);
|
||||
|
||||
$this->getEntityManager()->flush();
|
||||
|
||||
self::assertEquals([$barDomain, $bazDomain, $fooDomain], $this->repo->findDomainsWithout());
|
||||
@@ -36,4 +51,30 @@ class DomainRepositoryTest extends DatabaseTestCase
|
||||
self::assertEquals([$bazDomain, $fooDomain], $this->repo->findDomainsWithout('bar.com'));
|
||||
self::assertEquals([$barDomain, $fooDomain], $this->repo->findDomainsWithout('baz.com'));
|
||||
}
|
||||
|
||||
private function createShortUrl(Domain $domain): ShortUrl
|
||||
{
|
||||
return new ShortUrl(
|
||||
'foo',
|
||||
ShortUrlMeta::fromRawData(['domain' => $domain->getAuthority()]),
|
||||
new class ($domain) implements ShortUrlRelationResolverInterface {
|
||||
private Domain $domain;
|
||||
|
||||
public function __construct(Domain $domain)
|
||||
{
|
||||
$this->domain = $domain;
|
||||
}
|
||||
|
||||
public function resolveDomain(?string $domain): ?Domain
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
public function resolveApiKey(?string $key): ?ApiKey
|
||||
{
|
||||
return null;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\Options;
|
||||
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
|
||||
use Shlinkio\Shlink\Core\Service\VisitsTrackerInterface;
|
||||
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||
|
||||
use function array_key_exists;
|
||||
|
||||
@@ -30,19 +31,19 @@ class RedirectActionTest extends TestCase
|
||||
private RedirectAction $action;
|
||||
private ObjectProphecy $urlResolver;
|
||||
private ObjectProphecy $visitTracker;
|
||||
private Options\UrlShortenerOptions $shortenerOpts;
|
||||
private ObjectProphecy $redirectRespHelper;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->urlResolver = $this->prophesize(ShortUrlResolverInterface::class);
|
||||
$this->visitTracker = $this->prophesize(VisitsTrackerInterface::class);
|
||||
$this->shortenerOpts = new Options\UrlShortenerOptions();
|
||||
$this->redirectRespHelper = $this->prophesize(RedirectResponseHelperInterface::class);
|
||||
|
||||
$this->action = new RedirectAction(
|
||||
$this->urlResolver->reveal(),
|
||||
$this->visitTracker->reveal(),
|
||||
new Options\AppOptions(['disableTrackParam' => 'foobar']),
|
||||
$this->shortenerOpts,
|
||||
$this->redirectRespHelper->reveal(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -59,14 +60,14 @@ class RedirectActionTest extends TestCase
|
||||
)->willReturn($shortUrl);
|
||||
$track = $this->visitTracker->track(Argument::cetera())->will(function (): void {
|
||||
});
|
||||
$expectedResp = new Response\RedirectResponse($expectedUrl);
|
||||
$buildResp = $this->redirectRespHelper->buildRedirectResponse($expectedUrl)->willReturn($expectedResp);
|
||||
|
||||
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)->withQueryParams($query);
|
||||
$response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
||||
|
||||
self::assertInstanceOf(Response\RedirectResponse::class, $response);
|
||||
self::assertEquals(302, $response->getStatusCode());
|
||||
self::assertTrue($response->hasHeader('Location'));
|
||||
self::assertEquals($expectedUrl, $response->getHeaderLine('Location'));
|
||||
self::assertSame($expectedResp, $response);
|
||||
$buildResp->shouldHaveBeenCalledOnce();
|
||||
$shortCodeToUrl->shouldHaveBeenCalledOnce();
|
||||
$track->shouldHaveBeenCalledTimes(array_key_exists('foobar', $query) ? 0 : 1);
|
||||
}
|
||||
@@ -107,6 +108,9 @@ class RedirectActionTest extends TestCase
|
||||
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn($shortUrl);
|
||||
$track = $this->visitTracker->track(Argument::cetera())->will(function (): void {
|
||||
});
|
||||
$buildResp = $this->redirectRespHelper->buildRedirectResponse(
|
||||
'http://domain.com/foo/bar?some=thing',
|
||||
)->willReturn(new Response\RedirectResponse(''));
|
||||
|
||||
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)
|
||||
->withAttribute(
|
||||
@@ -115,42 +119,7 @@ class RedirectActionTest extends TestCase
|
||||
);
|
||||
$this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
||||
|
||||
$buildResp->shouldHaveBeenCalled();
|
||||
$track->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideRedirectConfigs
|
||||
*/
|
||||
public function expectedStatusCodeAndCacheIsReturnedBasedOnConfig(
|
||||
int $configuredStatus,
|
||||
int $configuredLifetime,
|
||||
int $expectedStatus,
|
||||
?string $expectedCacheControl
|
||||
): void {
|
||||
$this->shortenerOpts->redirectStatusCode = $configuredStatus;
|
||||
$this->shortenerOpts->redirectCacheLifetime = $configuredLifetime;
|
||||
|
||||
$shortUrl = new ShortUrl('http://domain.com/foo/bar');
|
||||
$shortCode = $shortUrl->getShortCode();
|
||||
$this->urlResolver->resolveEnabledShortUrl(Argument::cetera())->willReturn($shortUrl);
|
||||
|
||||
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode);
|
||||
$response = $this->action->process($request, $this->prophesize(RequestHandlerInterface::class)->reveal());
|
||||
|
||||
self::assertInstanceOf(Response\RedirectResponse::class, $response);
|
||||
self::assertEquals($expectedStatus, $response->getStatusCode());
|
||||
self::assertEquals($response->hasHeader('Cache-Control'), $expectedCacheControl !== null);
|
||||
self::assertEquals($response->getHeaderLine('Cache-Control'), $expectedCacheControl ?? '');
|
||||
}
|
||||
|
||||
public function provideRedirectConfigs(): iterable
|
||||
{
|
||||
yield 'status 302' => [302, 20, 302, null];
|
||||
yield 'status over 302' => [400, 20, 302, null];
|
||||
yield 'status below 301' => [201, 20, 302, null];
|
||||
yield 'status 301 with valid expiration' => [301, 20, 301, 'private,max-age=20'];
|
||||
yield 'status 301 with zero expiration' => [301, 0, 301, 'private,max-age=30'];
|
||||
yield 'status 301 with negative expiration' => [301, -20, 301, 'private,max-age=30'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,13 +10,16 @@ use Laminas\Diactoros\Uri;
|
||||
use Mezzio\Router\Route;
|
||||
use Mezzio\Router\RouteResult;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Prophecy\Prophecy\ObjectProphecy;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Action\RedirectAction;
|
||||
use Shlinkio\Shlink\Core\ErrorHandler\NotFoundRedirectHandler;
|
||||
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
|
||||
use Shlinkio\Shlink\Core\Util\RedirectResponseHelperInterface;
|
||||
|
||||
class NotFoundRedirectHandlerTest extends TestCase
|
||||
{
|
||||
@@ -24,11 +27,13 @@ class NotFoundRedirectHandlerTest extends TestCase
|
||||
|
||||
private NotFoundRedirectHandler $middleware;
|
||||
private NotFoundRedirectOptions $redirectOptions;
|
||||
private ObjectProphecy $helper;
|
||||
|
||||
public function setUp(): void
|
||||
{
|
||||
$this->redirectOptions = new NotFoundRedirectOptions();
|
||||
$this->middleware = new NotFoundRedirectHandler($this->redirectOptions, '');
|
||||
$this->helper = $this->prophesize(RedirectResponseHelperInterface::class);
|
||||
$this->middleware = new NotFoundRedirectHandler($this->redirectOptions, $this->helper->reveal(), '');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -43,13 +48,16 @@ class NotFoundRedirectHandlerTest extends TestCase
|
||||
$this->redirectOptions->regular404 = 'regular404';
|
||||
$this->redirectOptions->baseUrl = 'baseUrl';
|
||||
|
||||
$expectedResp = new Response();
|
||||
$buildResp = $this->helper->buildRedirectResponse($expectedRedirectTo)->willReturn($expectedResp);
|
||||
|
||||
$next = $this->prophesize(RequestHandlerInterface::class);
|
||||
$handle = $next->handle($request)->willReturn(new Response());
|
||||
|
||||
$resp = $this->middleware->process($request, $next->reveal());
|
||||
|
||||
self::assertInstanceOf(Response\RedirectResponse::class, $resp);
|
||||
self::assertEquals($expectedRedirectTo, $resp->getHeaderLine('Location'));
|
||||
self::assertSame($expectedResp, $resp);
|
||||
$buildResp->shouldHaveBeenCalledOnce();
|
||||
$handle->shouldNotHaveBeenCalled();
|
||||
}
|
||||
|
||||
@@ -91,12 +99,15 @@ class NotFoundRedirectHandlerTest extends TestCase
|
||||
$req = ServerRequestFactory::fromGlobals();
|
||||
$resp = new Response();
|
||||
|
||||
$buildResp = $this->helper->buildRedirectResponse(Argument::cetera());
|
||||
|
||||
$next = $this->prophesize(RequestHandlerInterface::class);
|
||||
$handle = $next->handle($req)->willReturn($resp);
|
||||
|
||||
$result = $this->middleware->process($req, $next->reveal());
|
||||
|
||||
self::assertSame($resp, $result);
|
||||
$buildResp->shouldNotHaveBeenCalled();
|
||||
$handle->shouldHaveBeenCalledOnce();
|
||||
}
|
||||
}
|
||||
|
||||
55
module/Core/test/Util/RedirectResponseHelperTest.php
Normal file
55
module/Core/test/Util/RedirectResponseHelperTest.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\Util;
|
||||
|
||||
use Laminas\Diactoros\Response\RedirectResponse;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\Util\RedirectResponseHelper;
|
||||
|
||||
class RedirectResponseHelperTest extends TestCase
|
||||
{
|
||||
private RedirectResponseHelper $helper;
|
||||
private UrlShortenerOptions $shortenerOpts;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->shortenerOpts = new UrlShortenerOptions();
|
||||
$this->helper = new RedirectResponseHelper($this->shortenerOpts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @test
|
||||
* @dataProvider provideRedirectConfigs
|
||||
*/
|
||||
public function expectedStatusCodeAndCacheIsReturnedBasedOnConfig(
|
||||
int $configuredStatus,
|
||||
int $configuredLifetime,
|
||||
int $expectedStatus,
|
||||
?string $expectedCacheControl
|
||||
): void {
|
||||
$this->shortenerOpts->redirectStatusCode = $configuredStatus;
|
||||
$this->shortenerOpts->redirectCacheLifetime = $configuredLifetime;
|
||||
|
||||
$response = $this->helper->buildRedirectResponse('destination');
|
||||
|
||||
self::assertInstanceOf(RedirectResponse::class, $response);
|
||||
self::assertEquals($expectedStatus, $response->getStatusCode());
|
||||
self::assertTrue($response->hasHeader('Location'));
|
||||
self::assertEquals('destination', $response->getHeaderLine('Location'));
|
||||
self::assertEquals($expectedCacheControl !== null, $response->hasHeader('Cache-Control'));
|
||||
self::assertEquals($expectedCacheControl ?? '', $response->getHeaderLine('Cache-Control'));
|
||||
}
|
||||
|
||||
public function provideRedirectConfigs(): iterable
|
||||
{
|
||||
yield 'status 302' => [302, 20, 302, null];
|
||||
yield 'status over 302' => [400, 20, 302, null];
|
||||
yield 'status below 301' => [201, 20, 302, null];
|
||||
yield 'status 301 with valid expiration' => [301, 20, 301, 'private,max-age=20'];
|
||||
yield 'status 301 with zero expiration' => [301, 0, 301, 'private,max-age=30'];
|
||||
yield 'status 301 with negative expiration' => [301, -20, 301, 'private,max-age=30'];
|
||||
}
|
||||
}
|
||||
19
module/Rest/test-api/Fixtures/DomainFixture.php
Normal file
19
module/Rest/test-api/Fixtures/DomainFixture.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioApiTest\Shlink\Rest\Fixtures;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||
|
||||
class DomainFixture extends AbstractFixture
|
||||
{
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$orphanDomain = new Domain('this_domain_is_detached.com');
|
||||
$manager->persist($orphanDomain);
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user