Compare commits

...

32 Commits

Author SHA1 Message Date
Alejandro Celaya
179ddc5bd7 Merge pull request #925 from acelaya-forks/feature/db-socket-connection
Feature/db socket connection
2020-11-29 20:08:51 +01:00
Alejandro Celaya
bfd886604e Updated changelog 2020-11-29 19:50:39 +01:00
Alejandro Celaya
f34033aa9c Documented how to provide the unix socket to connect to mysql, maria and postgres databases 2020-11-29 19:46:34 +01:00
Alejandro Celaya
e54745b250 #833 Enabled unix socket option during installation 2020-11-29 14:01:26 +01:00
Alejandro Celaya
1975a35837 Updated to lcobucci/json 4.0 stable 2020-11-29 12:54:22 +01:00
Alejandro Celaya
5db66dcf0e Merge pull request #923 from acelaya-forks/feature/qr-codes-query-size
Feature/qr codes query size
2020-11-27 18:00:01 +01:00
Alejandro Celaya
cfdf2f9480 #917 Updated changelog 2020-11-27 17:50:09 +01:00
Alejandro Celaya
c13adb04ef #917 Documented QR endpoint with query size and path size 2020-11-27 17:47:52 +01:00
Alejandro Celaya
4f1ab977a1 #917 Added tests covering the different ways to provide sizes to the QR codes 2020-11-27 17:42:33 +01:00
Alejandro Celaya
fe59a5ad86 #917 Fixed cast to int on QR code action 2020-11-27 17:16:54 +01:00
Alejandro Celaya
a72dc16d85 #917 2020-11-27 17:05:13 +01:00
Alejandro Celaya
74108a19e5 Merge pull request #915 from acelaya-forks/feature/remove-plates
Feature/remove plates
2020-11-22 18:42:19 +01:00
Alejandro Celaya
abe0fc16df #912 Updated changelog 2020-11-22 18:13:12 +01:00
Alejandro Celaya
39bda5113b #912 Fixed unit tests 2020-11-22 18:11:31 +01:00
Alejandro Celaya
49ea5cc78b #912 Removed dependency on league/plates 2020-11-22 18:03:27 +01:00
Alejandro Celaya
8acde332b2 Merge pull request #914 from acelaya-forks/feature/mercure-10-compat
Feature/mercure 10 compat
2020-11-22 16:41:26 +01:00
Alejandro Celaya
600f7a7388 #869 Updated changelog 2020-11-22 16:27:24 +01:00
Alejandro Celaya
fd007ea4a9 #869 Updated dependencies to support mercure 0.10 2020-11-22 16:26:17 +01:00
Alejandro Celaya
b66922b3d5 Ensured lcobucci/jwt stays in alpha 2020-11-22 10:44:13 +01:00
Alejandro Celaya
7d981434e1 Merge pull request #910 from acelaya-forks/feature/swoole-bug
Feature/swoole bug
2020-11-22 10:41:10 +01:00
Alejandro Celaya
c672d35b4a #827 Updated changelog 2020-11-22 10:26:18 +01:00
Alejandro Celaya
6259c73b33 #827 Fixed swoole config getting loaded on non-swoole contexts when running CLI command first 2020-11-22 10:24:06 +01:00
Alejandro Celaya
e4b00e832a Merge pull request #909 from acelaya-forks/feature/geolite-temp-dir
Feature/geolite temp dir
2020-11-21 12:48:28 +01:00
Alejandro Celaya
a452aeaf7e #899 Updated changelog 2020-11-21 12:38:14 +01:00
Alejandro Celaya
6e83b90028 #899 Changed temp directory in which geolite DB files are downloaded 2020-11-21 12:36:30 +01:00
Alejandro Celaya
45ffdce312 Merge pull request #908 from acelaya-forks/feature/domains-list
Feature/domains list
2020-11-21 09:46:16 +01:00
Alejandro Celaya
5485efc9ae #901 Fixed condition type 2020-11-21 08:51:30 +01:00
Alejandro Celaya
850360dd2b #901 Updated changelog 2020-11-21 08:45:57 +01:00
Alejandro Celaya
8d3ceaf462 #901 Ensured only domains in use are returned to lists 2020-11-21 08:44:28 +01:00
Alejandro Celaya
bb6c5de697 Merge pull request #907 from acelaya-forks/feature/missing-swagger-info
Feature/missing swagger info
2020-11-21 08:18:14 +01:00
Alejandro Celaya
ca4c1b00dc #904 Updated changelog 2020-11-21 08:16:22 +01:00
Alejandro Celaya
dda6d30c12 #904 Explicitly added missing Domains and Integrations tags to swagger docs 2020-11-21 08:13:29 +01:00
37 changed files with 362 additions and 235 deletions

View File

@@ -4,6 +4,47 @@ 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).
## [Unreleased]
### Added
* [#869](https://github.com/shlinkio/shlink/issues/869) Added support for Mercure Hub 0.10.
* [#833](https://github.com/shlinkio/shlink/issues/833) Added support to connect through unix socket when using an external MySQL, MariaDB or Postgres database.
It can be provided during the installation, or as the `DB_UNIX_SOCKET` env var for the docker image.
### Changed
* [#912](https://github.com/shlinkio/shlink/issues/912) Changed error templates to be plain html files, removing the dependency on `league/plates` package.
### Deprecated
* [#917](https://github.com/shlinkio/shlink/issues/917) Deprecated `/{shortCode}/qr-code/{size}` URL, in favor of providing the size in the query instead, `/{shortCode}/qr-code?size={size}`.
* [#924](https://github.com/shlinkio/shlink/issues/924) Deprecated mechanism to provide config options to the docker image through volumes. Use the env vars instead as a direct replacement.
### Removed
* *Nothing*
### Fixed
* *Nothing*
## [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*

View File

@@ -32,13 +32,12 @@
"laminas/laminas-paginator": "^2.8",
"laminas/laminas-servicemanager": "^3.4",
"laminas/laminas-stdlib": "^3.2",
"lcobucci/jwt": "^4.0@alpha",
"lcobucci/jwt": "^4.0",
"league/uri": "^6.2",
"lstrojny/functional-php": "^1.9",
"mezzio/mezzio": "^3.2",
"mezzio/mezzio-fastroute": "^3.0",
"mezzio/mezzio-helpers": "^5.3",
"mezzio/mezzio-platesrenderer": "^2.1",
"mezzio/mezzio-problem-details": "^1.1",
"mezzio/mezzio-swoole": "^2.6.4",
"monolog/monolog": "^2.0",
@@ -49,16 +48,16 @@
"predis/predis": "^1.1",
"pugx/shortid-php": "^0.5",
"ramsey/uuid": "^3.9",
"shlinkio/shlink-common": "^3.3.0",
"shlinkio/shlink-common": "^3.3.2",
"shlinkio/shlink-config": "^1.0",
"shlinkio/shlink-event-dispatcher": "^1.4",
"shlinkio/shlink-importer": "^2.0.1",
"shlinkio/shlink-installer": "^5.1.0",
"shlinkio/shlink-installer": "^5.2.0",
"shlinkio/shlink-ip-geolocation": "^1.5",
"symfony/console": "^5.1",
"symfony/filesystem": "^5.1",
"symfony/lock": "^5.1",
"symfony/mercure": "^0.3.0",
"symfony/mercure": "^0.4.0",
"symfony/process": "^5.1",
"symfony/string": "^5.1"
},

View File

@@ -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',
];

View File

@@ -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
],
];

View File

@@ -14,6 +14,7 @@ return [
Option\Database\DatabasePortConfigOption::class,
Option\Database\DatabaseUserConfigOption::class,
Option\Database\DatabasePasswordConfigOption::class,
Option\Database\DatabaseUnixSocketConfigOption::class,
Option\Database\DatabaseSqlitePathConfigOption::class,
Option\Database\DatabaseMySqlOptionsConfigOption::class,
Option\UrlShortener\ShortDomainHostConfigOption::class,

View File

@@ -1,17 +0,0 @@
<?php
declare(strict_types=1);
return [
'templates' => [
'extension' => 'phtml',
],
'plates' => [
'extensions' => [
// extension service names or instances
],
],
];

View File

@@ -15,7 +15,6 @@ return (new ConfigAggregator\ConfigAggregator([
Mezzio\ConfigProvider::class,
Mezzio\Router\ConfigProvider::class,
Mezzio\Router\FastRouteRouter\ConfigProvider::class,
Mezzio\Plates\ConfigProvider::class,
Mezzio\Swoole\ConfigProvider::class,
ProblemDetails\ConfigProvider::class,
Diactoros\ConfigProvider::class,

View File

@@ -131,7 +131,7 @@ services:
shlink_mercure:
container_name: shlink_mercure
image: dunglas/mercure:v0.9
image: dunglas/mercure:v0.10
ports:
- "3080:80"
environment:

View File

@@ -157,6 +157,7 @@ This is the complete list of supported env vars:
* **mysql** or **maria** -> `3306`
* **postgres** -> `5432`
* **mssql** -> `1433`
* `DB_UNIX_SOCKET`: Alternatively to the `DB_HOST`, you can provide this to connect through unix sockets when using `mysql`, `maria` or `postgres` drivers.
* `DISABLE_TRACK_PARAM`: The name of a query param that can be used to visit short URLs avoiding the visit to be tracked. This feature won't be available if not value is provided.
* `DELETE_SHORT_URL_THRESHOLD`: The amount of visits on short URLs which will not allow them to be deleted. Defaults to `15`.
* `VALIDATE_URLS`: Boolean which tells if shlink should validate a status 20x is returned (after following redirects) when trying to shorten a URL. Defaults to `false`.
@@ -215,7 +216,11 @@ docker run \
shlinkio/shlink:stable
```
## Provide config via volumes
## [DEPRECATED] Provide config via volumes
> As of v2.5.0, providing config through volumes is deprecated, and no new options will be added anymore. Use env vars instead.
>
> Support for config options through volumes will be removed in Shlink v3.0.0
Rather than providing custom configuration via env vars, it is also possible ot provide config files in json format.

View File

@@ -34,6 +34,7 @@ $helper = new class {
public function getDbConfig(): array
{
$driver = env('DB_DRIVER');
$isMysql = contains(['maria', 'mysql'], $driver);
if ($driver === null || $driver === 'sqlite') {
return [
'driver' => 'pdo_sqlite',
@@ -41,7 +42,7 @@ $helper = new class {
];
}
$driverOptions = ! contains(['maria', 'mysql'], $driver) ? [] : [
$driverOptions = ! $isMysql ? [] : [
// 1002 -> PDO::MYSQL_ATTR_INIT_COMMAND
1002 => 'SET NAMES utf8',
// 1000 -> PDO::MYSQL_ATTR_USE_BUFFERED_QUERY
@@ -52,9 +53,10 @@ $helper = new class {
'dbname' => env('DB_NAME', 'shlink'),
'user' => env('DB_USER'),
'password' => env('DB_PASSWORD'),
'host' => env('DB_HOST'),
'host' => env('DB_HOST', $driver === 'postgres' ? env('DB_UNIX_SOCKET') : null),
'port' => env('DB_PORT', self::DB_PORTS_MAP[$driver]),
'driverOptions' => $driverOptions,
'unix_socket' => $isMysql ? env('DB_UNIX_SOCKET') : null,
];
}
@@ -99,8 +101,6 @@ $helper = new class {
return [
'config_cache_enabled' => false,
'app_options' => [
'disable_track_param' => env('DISABLE_TRACK_PARAM'),
],

View File

@@ -18,7 +18,7 @@
},
{
"name": "size",
"in": "path",
"in": "query",
"description": "The size of the image to be returned.",
"required": false,
"schema": {

View File

@@ -0,0 +1,66 @@
{
"get": {
"operationId": "shortUrlQrCodeSize",
"deprecated": true,
"tags": [
"URL Shortener"
],
"summary": "Short URL QR code",
"description": "Generates a QR code image pointing to a short URL",
"parameters": [
{
"name": "shortCode",
"in": "path",
"description": "The short code to resolve.",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "size",
"in": "path",
"description": "The size of the image to be returned.",
"required": false,
"schema": {
"type": "integer",
"minimum": 50,
"maximum": 1000,
"default": 300
}
},
{
"name": "format",
"in": "query",
"description": "The format for the QR code image, being valid values png and svg. Not providing the param or providing any other value will fall back to png.",
"required": false,
"schema": {
"type": "string",
"enum": [
"png",
"svg"
]
}
}
],
"responses": {
"200": {
"description": "QR code in PNG format",
"content": {
"image/png": {
"schema": {
"type": "string",
"format": "binary"
}
},
"image/svg+xml": {
"schema": {
"type": "string",
"format": "binary"
}
}
}
}
}
}
}

View File

@@ -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"
@@ -108,6 +116,9 @@
},
"/{shortCode}/qr-code": {
"$ref": "paths/{shortCode}_qr-code.json"
},
"/{shortCode}/qr-code/{size}": {
"$ref": "paths/{shortCode}_qr-code_{size}.json"
}
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\CLI;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\ConfigProvider;
@@ -21,7 +22,9 @@ class ConfigProviderTest extends TestCase
{
$config = ($this->configProvider)();
self::assertCount(3, $config);
self::assertArrayHasKey('cli', $config);
self::assertArrayHasKey('dependencies', $config);
self::assertArrayHasKey(ConfigAbstractFactory::class, $config);
}
}

View File

@@ -5,7 +5,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use Mezzio\Template\TemplateRendererInterface;
use Laminas\ServiceManager\Factory\InvokableFactory;
use Psr\EventDispatcher\EventDispatcherInterface;
use Shlinkio\Shlink\Core\ErrorHandler;
use Shlinkio\Shlink\Core\Options\NotFoundRedirectOptions;
@@ -16,7 +16,7 @@ return [
'dependencies' => [
'factories' => [
ErrorHandler\NotFoundRedirectHandler::class => ConfigAbstractFactory::class,
ErrorHandler\NotFoundTemplateHandler::class => ConfigAbstractFactory::class,
ErrorHandler\NotFoundTemplateHandler::class => InvokableFactory::class,
Options\AppOptions::class => ConfigAbstractFactory::class,
Options\DeleteShortUrlsOptions::class => ConfigAbstractFactory::class,
@@ -60,7 +60,6 @@ return [
Util\RedirectResponseHelper::class,
'config.router.base_path',
],
ErrorHandler\NotFoundTemplateHandler::class => [TemplateRendererInterface::class],
Options\AppOptions::class => ['config.app_options'],
Options\DeleteShortUrlsOptions::class => ['config.delete_short_urls'],

View File

@@ -1,14 +0,0 @@
<?php
declare(strict_types=1);
return [
'mezzio' => [
'error_handler' => [
'template_404' => 'ShlinkCore::error/404',
'template_error' => 'ShlinkCore::error/error',
],
],
];

View File

@@ -29,7 +29,17 @@ return [
],
[
'name' => Action\QrCodeAction::class,
'path' => '/{shortCode}/qr-code[/{size:[0-9]+}]',
'path' => '/{shortCode}/qr-code',
'middleware' => [
Action\QrCodeAction::class,
],
'allowed_methods' => [RequestMethod::METHOD_GET],
],
// Deprecated
[
'name' => 'old_' . Action\QrCodeAction::class,
'path' => '/{shortCode}/qr-code/{size:[0-9]+}',
'middleware' => [
Action\QrCodeAction::class,
],

View File

@@ -1,13 +0,0 @@
<?php
declare(strict_types=1);
return [
'templates' => [
'paths' => [
'ShlinkCore' => __DIR__ . '/../templates',
],
],
];

View File

@@ -41,7 +41,7 @@ abstract class AbstractTrackingAction implements MiddlewareInterface, RequestMet
$this->urlResolver = $urlResolver;
$this->visitTracker = $visitTracker;
$this->appOptions = $appOptions;
$this->logger = $logger ?: new NullLogger();
$this->logger = $logger ?? new NullLogger();
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface

View File

@@ -34,7 +34,7 @@ class QrCodeAction implements MiddlewareInterface
) {
$this->urlResolver = $urlResolver;
$this->domainConfig = $domainConfig;
$this->logger = $logger ?: new NullLogger();
$this->logger = $logger ?? new NullLogger();
}
public function process(Request $request, RequestHandlerInterface $handler): Response
@@ -48,11 +48,15 @@ class QrCodeAction implements MiddlewareInterface
return $handler->handle($request);
}
$query = $request->getQueryParams();
// Size attribute is deprecated
$size = $this->normalizeSize((int) $request->getAttribute('size', $query['size'] ?? self::DEFAULT_SIZE));
$qrCode = new QrCode($shortUrl->toString($this->domainConfig));
$qrCode->setSize($this->getSizeParam($request));
$qrCode->setSize($size);
$qrCode->setMargin(0);
$format = $request->getQueryParams()['format'] ?? 'png';
$format = $query['format'] ?? 'png';
if ($format === 'svg') {
$qrCode->setWriter(new SvgWriter());
}
@@ -60,9 +64,8 @@ class QrCodeAction implements MiddlewareInterface
return new QrCodeResponse($qrCode);
}
private function getSizeParam(Request $request): int
private function normalizeSize(int $size): int
{
$size = (int) $request->getAttribute('size', self::DEFAULT_SIZE);
if ($size < self::MIN_SIZE) {
return self::MIN_SIZE;
}

View File

@@ -15,6 +15,7 @@ use function Functional\contains;
use function Functional\reduce_left;
use function uksort;
/** @deprecated */
class SimplifiedConfigParser
{
private const SIMPLIFIED_CONFIG_MAPPING = [

View File

@@ -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'))

View File

@@ -4,40 +4,37 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\ErrorHandler;
use Closure;
use Fig\Http\Message\StatusCodeInterface;
use InvalidArgumentException;
use Laminas\Diactoros\Response;
use Mezzio\Router\RouteResult;
use Mezzio\Template\TemplateRendererInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function file_get_contents;
use function sprintf;
class NotFoundTemplateHandler implements RequestHandlerInterface
{
public const NOT_FOUND_TEMPLATE = 'ShlinkCore::error/404';
public const INVALID_SHORT_CODE_TEMPLATE = 'ShlinkCore::invalid-short-code';
private const TEMPLATES_BASE_DIR = __DIR__ . '/../../templates';
public const NOT_FOUND_TEMPLATE = '404.html';
public const INVALID_SHORT_CODE_TEMPLATE = 'invalid-short-code.html';
private Closure $readFile;
private TemplateRendererInterface $renderer;
public function __construct(TemplateRendererInterface $renderer)
public function __construct(?callable $readFile = null)
{
$this->renderer = $renderer;
$this->readFile = $readFile ? Closure::fromCallable($readFile) : fn (string $file) => file_get_contents($file);
}
/**
* Dispatch the next available middleware and return the response.
*
*
* @throws InvalidArgumentException
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
/** @var RouteResult $routeResult */
$routeResult = $request->getAttribute(RouteResult::class, RouteResult::fromRouteFailure(null));
$routeResult = $request->getAttribute(RouteResult::class) ?? RouteResult::fromRouteFailure(null);
$status = StatusCodeInterface::STATUS_NOT_FOUND;
$template = $routeResult->isFailure() ? self::NOT_FOUND_TEMPLATE : self::INVALID_SHORT_CODE_TEMPLATE;
return new Response\HtmlResponse($this->renderer->render($template), $status);
$templateContent = ($this->readFile)(sprintf('%s/%s', self::TEMPLATES_BASE_DIR, $template));
return new Response\HtmlResponse($templateContent, $status);
}
}

View File

@@ -19,12 +19,6 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
$this->params = $params;
}
/**
* Returns a collection of items for a page.
*
* @param int $offset Page offset
* @param int $itemCountPerPage Number of items per page
*/
public function getItems($offset, $itemCountPerPage): array // phpcs:ignore
{
return $this->repository->findList(
@@ -37,15 +31,6 @@ class ShortUrlRepositoryAdapter implements AdapterInterface
);
}
/**
* Count elements of an object
* @link http://php.net/manual/en/countable.count.php
* @return int The custom count as an integer.
* </p>
* <p>
* The return value is cast to an integer.
* @since 5.1.0
*/
public function count(): int
{
return $this->repository->countList(

View File

@@ -33,15 +33,9 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
?DateRange $dateRange = null
): array {
$qb = $this->createListQueryBuilder($searchTerm, $tags, $dateRange);
$qb->select('DISTINCT s');
// Set limit and offset
if ($limit !== null) {
$qb->setMaxResults($limit);
}
if ($offset !== null) {
$qb->setFirstResult($offset);
}
$qb->select('DISTINCT s')
->setMaxResults($limit)
->setFirstResult($offset);
// In case the ordering has been specified, the query could be more complex. Process it
if ($orderBy !== null && $orderBy->hasOrderField()) {
@@ -147,7 +141,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
WHERE s.shortCode = :shortCode
AND (s.domain IS NULL OR d.authority = :domain)
ORDER BY s.domain {$ordering}
DQL;
DQL;
$query = $this->getEntityManager()->createQuery($dql);
$query->setMaxResults(1)
@@ -220,9 +214,8 @@ DQL;
}
if ($meta->hasValidUntil()) {
$qb->andWhere($qb->expr()->eq('s.validUntil', ':validUntil'))
->setParameter('validUntil', $meta->getValidUntil());
->setParameter('validUntil', $meta->getValidUntil());
}
if ($meta->hasDomain()) {
$qb->join('s.domain', 'd')
->andWhere($qb->expr()->eq('d.authority', ':domain'))

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Not Found | Shlink</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<link rel="shortcut icon" href="/favicon.ico">
<style>
html, body {height: 100%}
.app {height: 100vh; display: flex; align-items: center; justify-content: center; flex-flow: column;}
p {margin-bottom: 20px;}
body {text-align: center;}
</style>
</head>
<body>
<div class="app">
<main class="container">
<h1>404</h1>
<hr>
<h3>Page not found.</h3>
<p>The page you requested could not be found.</p>
</main>
</div>
</body>
</html>

View File

@@ -1,19 +0,0 @@
<?php $this->layout('ShlinkCore::layout/default') ?>
<?php $this->start('title') ?>
Not Found
<?php $this->end() ?>
<?php $this->start('stylesheets') ?>
<style>
p {margin-bottom: 20px;}
body {text-align: center;}
</style>
<?php $this->end() ?>
<?php $this->start('main') ?>
<h1>404</h1>
<hr>
<h3>Page not found.</h3>
<p>The page you requested could not be found.</p>
<?php $this->end() ?>

View File

@@ -1,25 +0,0 @@
<?php $this->layout('ShlinkCore::layout/default') ?>
<?php $this->start('title') ?>
<?= $this->e($status . ' ' . $reason) ?>
<?php $this->end() ?>
<?php $this->start('stylesheets') ?>
<style>
p {margin-bottom: 20px;}
body {text-align: center;}
</style>
<?php $this->end() ?>
<?php $this->start('main') ?>
<h1>Oops!</h1>
<hr>
<?php if ($status !== 404): ?>
<p><?= sprintf('We encountered a %s %s error.', $status, $reason) ?></p>
<?php else: ?>
<p>'This short URL doesn't seem to be valid.</p>
<p>'Make sure you included all the characters, with no extra punctuation.</p>
<?php endif; ?>
<?php $this->end() ?>

View File

@@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Invalid Short URL | Shlink</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<link rel="shortcut icon" href="/favicon.ico">
<style>
html, body {height: 100%}
.app {height: 100vh; display: flex; align-items: center; justify-content: center; flex-flow: column;}
p {margin-bottom: 20px;}
body {text-align: center;}
</style>
</head>
<body>
<div class="app">
<main class="container">
<h1>Oops!</h1>
<hr>
<p>This short URL doesn't seem to be valid.</p>
<p>Make sure you included all the characters, with no extra punctuation.</p>
</main>
</div>
</body>
</html>

View File

@@ -1,19 +0,0 @@
<?php $this->layout('ShlinkCore::layout/default') ?>
<?php $this->start('title') ?>
Invalid Short URL
<?php $this->end() ?>
<?php $this->start('stylesheets') ?>
<style>
p {margin-bottom: 20px;}
body {text-align: center;}
</style>
<?php $this->end() ?>
<?php $this->start('main') ?>
<h1>Oops!</h1>
<hr>
<p>This short URL doesn't seem to be valid.</p>
<p>Make sure you included all the characters, with no extra punctuation.</p>
<?php $this->end() ?>

View File

@@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title><?= $this->section('title', '') ?> | Shlink</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<link rel="shortcut icon" href="/favicon.ico">
<style>
html, body {height: 100%}
.app {height: 100vh; display: flex; align-items: center; justify-content: center; flex-flow: column;}
</style>
<?= $this->section('stylesheets', '') ?>
</head>
<body>
<div class="app">
<main class="container">
<?= $this->section('main', '') ?>
</main>
</div>
</body>
</html>

View File

@@ -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;
}
},
);
}
}

View File

@@ -6,11 +6,13 @@ namespace ShlinkioTest\Shlink\Core\Action;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequest;
use Laminas\Diactoros\ServerRequestFactory;
use Mezzio\Router\RouterInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Shlinkio\Shlink\Common\Response\QrCodeResponse;
use Shlinkio\Shlink\Core\Action\QrCodeAction;
@@ -19,6 +21,8 @@ use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\Model\ShortUrlIdentifier;
use Shlinkio\Shlink\Core\Service\ShortUrl\ShortUrlResolverInterface;
use function getimagesizefromstring;
class QrCodeActionTest extends TestCase
{
use ProphecyTrait;
@@ -51,21 +55,6 @@ class QrCodeActionTest extends TestCase
$process->shouldHaveBeenCalledOnce();
}
/** @test */
public function anInvalidShortCodeWillReturnNotFoundResponse(): void
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
->willThrow(ShortUrlNotFoundException::class)
->shouldBeCalledOnce();
$delegate = $this->prophesize(RequestHandlerInterface::class);
$process = $delegate->handle(Argument::any())->willReturn(new Response());
$this->action->process((new ServerRequest())->withAttribute('shortCode', $shortCode), $delegate->reveal());
$process->shouldHaveBeenCalledOnce();
}
/** @test */
public function aCorrectRequestReturnsTheQrCodeResponse(): void
{
@@ -110,4 +99,31 @@ class QrCodeActionTest extends TestCase
yield 'svg format' => [['format' => 'svg'], 'image/svg+xml'];
yield 'unsupported format' => [['format' => 'jpg'], 'image/png'];
}
/**
* @test
* @dataProvider provideRequestsWithSize
*/
public function imageIsReturnedWithExpectedSize(ServerRequestInterface $req, int $expectedSize): void
{
$code = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(new ShortUrl(''));
$delegate = $this->prophesize(RequestHandlerInterface::class);
$resp = $this->action->process($req->withAttribute('shortCode', $code), $delegate->reveal());
[$size] = getimagesizefromstring((string) $resp->getBody());
self::assertEquals($expectedSize, $size);
}
public function provideRequestsWithSize(): iterable
{
yield 'no size' => [ServerRequestFactory::fromGlobals(), 300];
yield 'size in attr' => [ServerRequestFactory::fromGlobals()->withAttribute('size', '400'), 400];
yield 'size in query' => [ServerRequestFactory::fromGlobals()->withQueryParams(['size' => '123']), 123];
yield 'size in query and attr' => [
ServerRequestFactory::fromGlobals()->withAttribute('size', '350')->withQueryParams(['size' => '123']),
350,
];
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\ConfigProvider;
@@ -19,11 +20,13 @@ class ConfigProviderTest extends TestCase
/** @test */
public function properConfigIsReturned(): void
{
$config = $this->configProvider->__invoke();
$config = ($this->configProvider)();
self::assertCount(5, $config);
self::assertArrayHasKey('routes', $config);
self::assertArrayHasKey('dependencies', $config);
self::assertArrayHasKey('templates', $config);
self::assertArrayHasKey('mezzio', $config);
self::assertArrayHasKey('entity_manager', $config);
self::assertArrayHasKey('events', $config);
self::assertArrayHasKey(ConfigAbstractFactory::class, $config);
}
}

View File

@@ -4,29 +4,30 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\ErrorHandler;
use Closure;
use Laminas\Diactoros\Response;
use Laminas\Diactoros\ServerRequestFactory;
use Mezzio\Router\Route;
use Mezzio\Router\RouteResult;
use Mezzio\Template\TemplateRendererInterface;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Shlinkio\Shlink\Core\ErrorHandler\NotFoundTemplateHandler;
class NotFoundTemplateHandlerTest extends TestCase
{
use ProphecyTrait;
private NotFoundTemplateHandler $handler;
private ObjectProphecy $renderer;
private Closure $readFile;
private bool $readFileCalled;
public function setUp(): void
{
$this->renderer = $this->prophesize(TemplateRendererInterface::class);
$this->handler = new NotFoundTemplateHandler($this->renderer->reveal());
$this->readFileCalled = false;
$this->readFile = function (string $fileName): string {
$this->readFileCalled = true;
return $fileName;
};
$this->handler = new NotFoundTemplateHandler($this->readFile);
}
/**
@@ -35,13 +36,11 @@ class NotFoundTemplateHandlerTest extends TestCase
*/
public function properErrorTemplateIsRendered(ServerRequestInterface $request, string $expectedTemplate): void
{
$request = $request->withHeader('Accept', 'text/html');
$render = $this->renderer->render($expectedTemplate)->willReturn('');
$resp = $this->handler->handle($request);
$resp = $this->handler->handle($request->withHeader('Accept', 'text/html'));
self::assertInstanceOf(Response\HtmlResponse::class, $resp);
$render->shouldHaveBeenCalledOnce();
self::assertStringContainsString($expectedTemplate, (string) $resp->getBody());
self::assertTrue($this->readFileCalled);
}
public function provideTemplates(): iterable

View 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();
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Rest;
use Laminas\ServiceManager\AbstractFactory\ConfigAbstractFactory;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Rest\ConfigProvider;
@@ -21,8 +22,12 @@ class ConfigProviderTest extends TestCase
{
$config = ($this->configProvider)();
self::assertCount(5, $config);
self::assertArrayHasKey('routes', $config);
self::assertArrayHasKey('dependencies', $config);
self::assertArrayHasKey('auth', $config);
self::assertArrayHasKey('entity_manager', $config);
self::assertArrayHasKey(ConfigAbstractFactory::class, $config);
}
/**