mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-09 16:53:11 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1ebbaa52f | ||
|
|
7abe6af5ec | ||
|
|
c98ea6055b | ||
|
|
3e3d255edf | ||
|
|
816d4851e7 | ||
|
|
4110c702c0 | ||
|
|
57eb29c3c8 | ||
|
|
5267c4eee6 | ||
|
|
1453ebe8ca | ||
|
|
5bf84144e7 | ||
|
|
d9adff5749 | ||
|
|
a1cd8baf3e | ||
|
|
87cadce0ac | ||
|
|
f22f50afa2 | ||
|
|
d0fa6f7e03 | ||
|
|
d29c58dce5 | ||
|
|
9ea8f3b590 | ||
|
|
48d3ab0cb4 |
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -80,7 +80,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php-version }}
|
php-version: ${{ matrix.php-version }}
|
||||||
tools: composer
|
tools: composer
|
||||||
extensions: openswoole-4.9.1, pdo_sqlsrv-5.10.0beta2
|
extensions: openswoole-4.9.1, pdo_sqlsrv-5.10.0
|
||||||
coverage: pcov
|
coverage: pcov
|
||||||
ini-values: pcov.directory=module
|
ini-values: pcov.directory=module
|
||||||
- run: composer install --no-interaction --prefer-dist
|
- run: composer install --no-interaction --prefer-dist
|
||||||
|
|||||||
2
.github/workflows/publish-swagger-spec.yml
vendored
2
.github/workflows/publish-swagger-spec.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
|||||||
- run: composer install --no-interaction --prefer-dist
|
- run: composer install --no-interaction --prefer-dist
|
||||||
- run: composer swagger:inline
|
- run: composer swagger:inline
|
||||||
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
- run: mkdir ${{ steps.determine_version.outputs.version }}
|
||||||
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/oas.json
|
- run: mv docs/swagger/swagger-inlined.json ${{ steps.determine_version.outputs.version }}/open-api-spec.json
|
||||||
- name: Publish spec
|
- name: Publish spec
|
||||||
uses: JamesIves/github-pages-deploy-action@4.1.7
|
uses: JamesIves/github-pages-deploy-action@4.1.7
|
||||||
with:
|
with:
|
||||||
|
|||||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -4,6 +4,60 @@ 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).
|
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
|
||||||
|
|
||||||
|
## [3.0.3] - 2022-02-19
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* [#1382](https://github.com/shlinkio/shlink/issues/1382) Updated docker image to PHP 8.1.3.
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1377](https://github.com/shlinkio/shlink/issues/1377) Fixed installer always setting delete threshold with value 1.
|
||||||
|
* [#1379](https://github.com/shlinkio/shlink/issues/1379) Ensured API keys cannot be created with a domain-only role linked to default domain.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.0.2] - 2022-02-10
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1373](https://github.com/shlinkio/shlink/issues/1373) Fixed incorrect config import when updating from Shlink 2.x using SQLite.
|
||||||
|
* [#1369](https://github.com/shlinkio/shlink/issues/1369) Fixed slow regexps in `.htaccess` file.
|
||||||
|
|
||||||
|
|
||||||
|
## [3.0.1] - 2022-02-04
|
||||||
|
### Added
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
* *Nothing*
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* [#1363](https://github.com/shlinkio/shlink/issues/1363) Fixed titles being resolved no matter what when `validateUrl` is not set or is explicitly set to true.
|
||||||
|
* [#1352](https://github.com/shlinkio/shlink/issues/1352) Updated to stable pdo_sqlsrv in docker image.
|
||||||
|
|
||||||
|
|
||||||
## [3.0.0] - 2022-01-28
|
## [3.0.0] - 2022-01-28
|
||||||
### Added
|
### Added
|
||||||
* [#767](https://github.com/shlinkio/shlink/issues/767) Added full support to use emojis everywhere, whether it is custom slugs, titles, referrers, etc.
|
* [#767](https://github.com/shlinkio/shlink/issues/767) Added full support to use emojis everywhere, whether it is custom slugs, titles, referrers, etc.
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
FROM php:8.1.1-alpine3.15 as base
|
FROM php:8.1.3-alpine3.15 as base
|
||||||
|
|
||||||
ARG SHLINK_VERSION=latest
|
ARG SHLINK_VERSION=latest
|
||||||
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
ENV SHLINK_VERSION ${SHLINK_VERSION}
|
||||||
ENV OPENSWOOLE_VERSION 4.9.1
|
ENV OPENSWOOLE_VERSION 4.9.1
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.0beta2
|
ENV PDO_SQLSRV_VERSION 5.10.0
|
||||||
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
||||||
ENV LC_ALL "C"
|
ENV LC_ALL "C"
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
"shlinkio/shlink-config": "^1.6",
|
"shlinkio/shlink-config": "^1.6",
|
||||||
"shlinkio/shlink-event-dispatcher": "^2.3",
|
"shlinkio/shlink-event-dispatcher": "^2.3",
|
||||||
"shlinkio/shlink-importer": "^2.5",
|
"shlinkio/shlink-importer": "^2.5",
|
||||||
"shlinkio/shlink-installer": "^7.0",
|
"shlinkio/shlink-installer": "^7.0.2",
|
||||||
"shlinkio/shlink-ip-geolocation": "^2.2",
|
"shlinkio/shlink-ip-geolocation": "^2.2",
|
||||||
"symfony/console": "^6.0",
|
"symfony/console": "^6.0",
|
||||||
"symfony/filesystem": "^6.0",
|
"symfony/filesystem": "^6.0",
|
||||||
@@ -128,7 +128,7 @@
|
|||||||
],
|
],
|
||||||
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox",
|
"test:unit": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-php build/coverage-unit.cov --testdox",
|
||||||
"test:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
"test:unit:ci": "@test:unit --coverage-xml=build/coverage-unit/coverage-xml --log-junit=build/coverage-unit/junit.xml",
|
||||||
"test:unit:pretty": "@php vendor/bin/phpunit --order-by=random --colors=always --coverage-html build/coverage-unit/coverage-html",
|
"test:unit:pretty": "@test:unit --coverage-html build/coverage-unit/coverage-html",
|
||||||
"test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
"test:db": "@parallel test:db:sqlite:ci test:db:mysql test:db:maria test:db:postgres test:db:ms",
|
||||||
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
|
"test:db:sqlite": "APP_ENV=test php vendor/bin/phpunit --order-by=random --colors=always --testdox -c phpunit-db.xml",
|
||||||
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml",
|
"test:db:sqlite:ci": "@test:db:sqlite --coverage-php build/coverage-db.cov --coverage-xml=build/coverage-db/coverage-xml --log-junit=build/coverage-db/junit.xml",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ const MIN_SHORT_CODES_LENGTH = 4;
|
|||||||
const DEFAULT_REDIRECT_STATUS_CODE = StatusCodeInterface::STATUS_FOUND;
|
const DEFAULT_REDIRECT_STATUS_CODE = StatusCodeInterface::STATUS_FOUND;
|
||||||
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
const DEFAULT_REDIRECT_CACHE_LIFETIME = 30;
|
||||||
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
const LOCAL_LOCK_FACTORY = 'Shlinkio\Shlink\LocalLockFactory';
|
||||||
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside an html title tag
|
const TITLE_TAG_VALUE = '/<title[^>]*>(.*?)<\/title>/i'; // Matches the value inside a html title tag
|
||||||
const DEFAULT_QR_CODE_SIZE = 300;
|
const DEFAULT_QR_CODE_SIZE = 300;
|
||||||
const DEFAULT_QR_CODE_MARGIN = 0;
|
const DEFAULT_QR_CODE_MARGIN = 0;
|
||||||
const DEFAULT_QR_CODE_FORMAT = 'png';
|
const DEFAULT_QR_CODE_FORMAT = 'png';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
FROM php:8.1.1-fpm-alpine3.15
|
FROM php:8.1.3-fpm-alpine3.15
|
||||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.21
|
ENV APCU_VERSION 5.1.21
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.0beta2
|
ENV PDO_SQLSRV_VERSION 5.10.0
|
||||||
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
FROM php:8.1.1-alpine3.15
|
FROM php:8.1.3-alpine3.15
|
||||||
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
MAINTAINER Alejandro Celaya <alejandro@alejandrocelaya.com>
|
||||||
|
|
||||||
ENV APCU_VERSION 5.1.21
|
ENV APCU_VERSION 5.1.21
|
||||||
ENV INOTIFY_VERSION 3.0.0
|
ENV INOTIFY_VERSION 3.0.0
|
||||||
ENV OPENSWOOLE_VERSION 4.9.1
|
ENV OPENSWOOLE_VERSION 4.9.1
|
||||||
ENV PDO_SQLSRV_VERSION 5.10.0beta2
|
ENV PDO_SQLSRV_VERSION 5.10.0
|
||||||
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
ENV MS_ODBC_SQL_VERSION 17.5.2.2
|
||||||
|
|
||||||
RUN apk update
|
RUN apk update
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ return [
|
|||||||
TrackingOptions::class,
|
TrackingOptions::class,
|
||||||
],
|
],
|
||||||
Util\ProcessRunner::class => [SymfonyCli\Helper\ProcessHelper::class],
|
Util\ProcessRunner::class => [SymfonyCli\Helper\ProcessHelper::class],
|
||||||
ApiKey\RoleResolver::class => [DomainService::class],
|
ApiKey\RoleResolver::class => [DomainService::class, 'config.url_shortener.domain.hostname'],
|
||||||
|
|
||||||
Command\ShortUrl\CreateShortUrlCommand::class => [
|
Command\ShortUrl\CreateShortUrlCommand::class => [
|
||||||
Service\UrlShortener::class,
|
Service\UrlShortener::class,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Shlinkio\Shlink\CLI\ApiKey;
|
namespace Shlinkio\Shlink\CLI\ApiKey;
|
||||||
|
|
||||||
|
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
|
||||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
@@ -12,24 +13,33 @@ use function is_string;
|
|||||||
|
|
||||||
class RoleResolver implements RoleResolverInterface
|
class RoleResolver implements RoleResolverInterface
|
||||||
{
|
{
|
||||||
public function __construct(private DomainServiceInterface $domainService)
|
public function __construct(private DomainServiceInterface $domainService, private string $defaultDomain)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public function determineRoles(InputInterface $input): array
|
public function determineRoles(InputInterface $input): array
|
||||||
{
|
{
|
||||||
$domainAuthority = $input->getOption('domain-only');
|
$domainAuthority = $input->getOption(self::DOMAIN_ONLY_PARAM);
|
||||||
$author = $input->getOption('author-only');
|
$author = $input->getOption(self::AUTHOR_ONLY_PARAM);
|
||||||
|
|
||||||
$roleDefinitions = [];
|
$roleDefinitions = [];
|
||||||
if ($author) {
|
if ($author) {
|
||||||
$roleDefinitions[] = RoleDefinition::forAuthoredShortUrls();
|
$roleDefinitions[] = RoleDefinition::forAuthoredShortUrls();
|
||||||
}
|
}
|
||||||
if (is_string($domainAuthority)) {
|
if (is_string($domainAuthority)) {
|
||||||
$domain = $this->domainService->getOrCreate($domainAuthority);
|
$roleDefinitions[] = $this->resolveRoleForAuthority($domainAuthority);
|
||||||
$roleDefinitions[] = RoleDefinition::forDomain($domain);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return $roleDefinitions;
|
return $roleDefinitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveRoleForAuthority(string $domainAuthority): RoleDefinition
|
||||||
|
{
|
||||||
|
if ($domainAuthority === $this->defaultDomain) {
|
||||||
|
throw InvalidRoleConfigException::forDomainOnlyWithDefaultDomain();
|
||||||
|
}
|
||||||
|
|
||||||
|
$domain = $this->domainService->getOrCreate($domainAuthority);
|
||||||
|
return RoleDefinition::forDomain($domain);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
module/CLI/src/Exception/InvalidRoleConfigException.php
Normal file
22
module/CLI/src/Exception/InvalidRoleConfigException.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Shlinkio\Shlink\CLI\Exception;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
class InvalidRoleConfigException extends InvalidArgumentException implements ExceptionInterface
|
||||||
|
{
|
||||||
|
public static function forDomainOnlyWithDefaultDomain(): self
|
||||||
|
{
|
||||||
|
return new self(sprintf(
|
||||||
|
'You cannot create an API key with the "%s" role attached to the default domain. '
|
||||||
|
. 'The role is currently limited to non-default domains.',
|
||||||
|
Role::DOMAIN_SPECIFIC,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ use PHPUnit\Framework\TestCase;
|
|||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
use Prophecy\Prophecy\ObjectProphecy;
|
use Prophecy\Prophecy\ObjectProphecy;
|
||||||
use Shlinkio\Shlink\CLI\ApiKey\RoleResolver;
|
use Shlinkio\Shlink\CLI\ApiKey\RoleResolver;
|
||||||
|
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
|
||||||
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||||
use Shlinkio\Shlink\Core\Entity\Domain;
|
use Shlinkio\Shlink\Core\Entity\Domain;
|
||||||
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
|
||||||
@@ -23,7 +24,7 @@ class RoleResolverTest extends TestCase
|
|||||||
protected function setUp(): void
|
protected function setUp(): void
|
||||||
{
|
{
|
||||||
$this->domainService = $this->prophesize(DomainServiceInterface::class);
|
$this->domainService = $this->prophesize(DomainServiceInterface::class);
|
||||||
$this->resolver = new RoleResolver($this->domainService->reveal());
|
$this->resolver = new RoleResolver($this->domainService->reveal(), 'default.com');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,4 +95,16 @@ class RoleResolverTest extends TestCase
|
|||||||
1,
|
1,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function exceptionIsThrownWhenTryingToAddDomainOnlyLinkedToDefaultDomain(): void
|
||||||
|
{
|
||||||
|
$input = $this->prophesize(InputInterface::class);
|
||||||
|
$input->getOption(RoleResolver::DOMAIN_ONLY_PARAM)->willReturn('default.com');
|
||||||
|
$input->getOption(RoleResolver::AUTHOR_ONLY_PARAM)->willReturn(null);
|
||||||
|
|
||||||
|
$this->expectException(InvalidRoleConfigException::class);
|
||||||
|
|
||||||
|
$this->resolver->determineRoles($input->reveal());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
module/CLI/test/Exception/InvalidRoleConfigExceptionTest.php
Normal file
26
module/CLI/test/Exception/InvalidRoleConfigExceptionTest.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace ShlinkioTest\Shlink\CLI\Exception;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Shlinkio\Shlink\CLI\Exception\InvalidRoleConfigException;
|
||||||
|
use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
|
class InvalidRoleConfigExceptionTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @test */
|
||||||
|
public function forDomainOnlyWithDefaultDomainGeneratesExpectedException(): void
|
||||||
|
{
|
||||||
|
$e = InvalidRoleConfigException::forDomainOnlyWithDefaultDomain();
|
||||||
|
|
||||||
|
self::assertEquals(sprintf(
|
||||||
|
'You cannot create an API key with the "%s" role attached to the default domain. '
|
||||||
|
. 'The role is currently limited to non-default domains.',
|
||||||
|
Role::DOMAIN_SPECIFIC,
|
||||||
|
), $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ final class ShortUrlEdit implements TitleResolutionModelInterface
|
|||||||
private bool $titlePropWasProvided = false;
|
private bool $titlePropWasProvided = false;
|
||||||
private ?string $title = null;
|
private ?string $title = null;
|
||||||
private bool $titleWasAutoResolved = false;
|
private bool $titleWasAutoResolved = false;
|
||||||
private ?bool $validateUrl = null;
|
private bool $validateUrl = false;
|
||||||
private bool $crawlablePropWasProvided = false;
|
private bool $crawlablePropWasProvided = false;
|
||||||
private bool $crawlable = false;
|
private bool $crawlable = false;
|
||||||
private bool $forwardQueryPropWasProvided = false;
|
private bool $forwardQueryPropWasProvided = false;
|
||||||
@@ -72,7 +72,7 @@ final class ShortUrlEdit implements TitleResolutionModelInterface
|
|||||||
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE));
|
$this->validSince = parseDateField($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE));
|
||||||
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL));
|
$this->validUntil = parseDateField($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL));
|
||||||
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS);
|
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS);
|
||||||
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL);
|
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL) ?? false;
|
||||||
$this->tags = $inputFilter->getValue(ShortUrlInputFilter::TAGS);
|
$this->tags = $inputFilter->getValue(ShortUrlInputFilter::TAGS);
|
||||||
$this->title = $inputFilter->getValue(ShortUrlInputFilter::TITLE);
|
$this->title = $inputFilter->getValue(ShortUrlInputFilter::TITLE);
|
||||||
$this->crawlable = $inputFilter->getValue(ShortUrlInputFilter::CRAWLABLE);
|
$this->crawlable = $inputFilter->getValue(ShortUrlInputFilter::CRAWLABLE);
|
||||||
@@ -166,7 +166,7 @@ final class ShortUrlEdit implements TitleResolutionModelInterface
|
|||||||
return $copy;
|
return $copy;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function doValidateUrl(): ?bool
|
public function doValidateUrl(): bool
|
||||||
{
|
{
|
||||||
return $this->validateUrl;
|
return $this->validateUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ final class ShortUrlMeta implements TitleResolutionModelInterface
|
|||||||
private ?bool $findIfExists = null;
|
private ?bool $findIfExists = null;
|
||||||
private ?string $domain = null;
|
private ?string $domain = null;
|
||||||
private int $shortCodeLength = 5;
|
private int $shortCodeLength = 5;
|
||||||
private ?bool $validateUrl = null;
|
private bool $validateUrl = false;
|
||||||
private ?ApiKey $apiKey = null;
|
private ?ApiKey $apiKey = null;
|
||||||
private array $tags = [];
|
private array $tags = [];
|
||||||
private ?string $title = null;
|
private ?string $title = null;
|
||||||
@@ -73,7 +73,7 @@ final class ShortUrlMeta implements TitleResolutionModelInterface
|
|||||||
$this->customSlug = $inputFilter->getValue(ShortUrlInputFilter::CUSTOM_SLUG);
|
$this->customSlug = $inputFilter->getValue(ShortUrlInputFilter::CUSTOM_SLUG);
|
||||||
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS);
|
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS);
|
||||||
$this->findIfExists = $inputFilter->getValue(ShortUrlInputFilter::FIND_IF_EXISTS);
|
$this->findIfExists = $inputFilter->getValue(ShortUrlInputFilter::FIND_IF_EXISTS);
|
||||||
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL);
|
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL) ?? false;
|
||||||
$this->domain = $inputFilter->getValue(ShortUrlInputFilter::DOMAIN);
|
$this->domain = $inputFilter->getValue(ShortUrlInputFilter::DOMAIN);
|
||||||
$this->shortCodeLength = getOptionalIntFromInputFilter(
|
$this->shortCodeLength = getOptionalIntFromInputFilter(
|
||||||
$inputFilter,
|
$inputFilter,
|
||||||
@@ -151,7 +151,7 @@ final class ShortUrlMeta implements TitleResolutionModelInterface
|
|||||||
return $this->shortCodeLength;
|
return $this->shortCodeLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function doValidateUrl(): ?bool
|
public function doValidateUrl(): bool
|
||||||
{
|
{
|
||||||
return $this->validateUrl;
|
return $this->validateUrl;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,20 +10,9 @@ class UrlShortenerOptions extends AbstractOptions
|
|||||||
{
|
{
|
||||||
protected $__strictMode__ = false; // phpcs:ignore
|
protected $__strictMode__ = false; // phpcs:ignore
|
||||||
|
|
||||||
private bool $validateUrl = true;
|
|
||||||
private bool $autoResolveTitles = false;
|
private bool $autoResolveTitles = false;
|
||||||
private bool $appendExtraPath = false;
|
private bool $appendExtraPath = false;
|
||||||
|
|
||||||
public function isUrlValidationEnabled(): bool
|
|
||||||
{
|
|
||||||
return $this->validateUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function setValidateUrl(bool $validateUrl): void
|
|
||||||
{
|
|
||||||
$this->validateUrl = $validateUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function autoResolveTitles(): bool
|
public function autoResolveTitles(): bool
|
||||||
{
|
{
|
||||||
return $this->autoResolveTitles;
|
return $this->autoResolveTitles;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface TitleResolutionModelInterface
|
|||||||
|
|
||||||
public function getLongUrl(): string;
|
public function getLongUrl(): string;
|
||||||
|
|
||||||
public function doValidateUrl(): ?bool;
|
public function doValidateUrl(): bool;
|
||||||
|
|
||||||
public function withResolvedTitle(string $title): self;
|
public function withResolvedTitle(string $title): self;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,8 @@ class UrlValidator implements UrlValidatorInterface, RequestMethodInterface
|
|||||||
/**
|
/**
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
*/
|
*/
|
||||||
public function validateUrl(string $url, ?bool $doValidate): void
|
public function validateUrl(string $url, bool $doValidate): void
|
||||||
{
|
{
|
||||||
// If the URL validation is not enabled, or it was explicitly set to not validate, skip check
|
|
||||||
$doValidate = $doValidate ?? $this->options->isUrlValidationEnabled();
|
|
||||||
if (! $doValidate) {
|
if (! $doValidate) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -41,15 +39,14 @@ class UrlValidator implements UrlValidatorInterface, RequestMethodInterface
|
|||||||
$this->validateUrlAndGetResponse($url, true);
|
$this->validateUrlAndGetResponse($url, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function validateUrlWithTitle(string $url, ?bool $doValidate): ?string
|
public function validateUrlWithTitle(string $url, bool $doValidate): ?string
|
||||||
{
|
{
|
||||||
$doValidate = $doValidate ?? $this->options->isUrlValidationEnabled();
|
|
||||||
if (! $doValidate && ! $this->options->autoResolveTitles()) {
|
if (! $doValidate && ! $this->options->autoResolveTitles()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$response = $this->validateUrlAndGetResponse($url, $doValidate);
|
$response = $this->validateUrlAndGetResponse($url, $doValidate);
|
||||||
if ($response === null) {
|
if ($response === null || ! $this->options->autoResolveTitles()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ interface UrlValidatorInterface
|
|||||||
/**
|
/**
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
*/
|
*/
|
||||||
public function validateUrl(string $url, ?bool $doValidate): void;
|
public function validateUrl(string $url, bool $doValidate): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @throws InvalidUrlException
|
* @throws InvalidUrlException
|
||||||
*/
|
*/
|
||||||
public function validateUrlWithTitle(string $url, ?bool $doValidate): ?string;
|
public function validateUrlWithTitle(string $url, bool $doValidate): ?string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ class ShortUrlTitleResolutionHelperTest extends TestCase
|
|||||||
ShortUrlMeta::fromRawData(['longUrl' => $longUrl, 'title' => $title]),
|
ShortUrlMeta::fromRawData(['longUrl' => $longUrl, 'title' => $title]),
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->urlValidator->validateUrlWithTitle($longUrl, null)->shouldHaveBeenCalledTimes(
|
$this->urlValidator->validateUrlWithTitle($longUrl, false)->shouldHaveBeenCalledTimes(
|
||||||
$validateWithTitleCallsNum,
|
$validateWithTitleCallsNum,
|
||||||
);
|
);
|
||||||
$this->urlValidator->validateUrl($longUrl, null)->shouldHaveBeenCalledTimes($validateCallsNum);
|
$this->urlValidator->validateUrl($longUrl, false)->shouldHaveBeenCalledTimes($validateCallsNum);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideTitles(): iterable
|
public function provideTitles(): iterable
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class UrlValidatorTest extends TestCase
|
|||||||
$request->shouldBeCalledOnce();
|
$request->shouldBeCalledOnce();
|
||||||
$this->expectException(InvalidUrlException::class);
|
$this->expectException(InvalidUrlException::class);
|
||||||
|
|
||||||
$this->urlValidator->validateUrl('http://foobar.com/12345/hello?foo=bar', null);
|
$this->urlValidator->validateUrl('http://foobar.com/12345/hello?foo=bar', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
@@ -65,50 +65,33 @@ class UrlValidatorTest extends TestCase
|
|||||||
}),
|
}),
|
||||||
)->willReturn(new Response());
|
)->willReturn(new Response());
|
||||||
|
|
||||||
$this->urlValidator->validateUrl($expectedUrl, null);
|
$this->urlValidator->validateUrl($expectedUrl, true);
|
||||||
|
|
||||||
$request->shouldHaveBeenCalledOnce();
|
$request->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @test */
|
||||||
* @test
|
public function noCheckIsPerformedWhenUrlValidationIsDisabled(): void
|
||||||
* @dataProvider provideDisabledCombinations
|
|
||||||
*/
|
|
||||||
public function noCheckIsPerformedWhenUrlValidationIsDisabled(?bool $doValidate, bool $validateUrl): void
|
|
||||||
{
|
{
|
||||||
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
|
$request = $this->httpClient->request(Argument::cetera())->willReturn(new Response());
|
||||||
$this->options->validateUrl = $validateUrl;
|
|
||||||
|
|
||||||
$this->urlValidator->validateUrl('', $doValidate);
|
$this->urlValidator->validateUrl('', false);
|
||||||
|
|
||||||
$request->shouldNotHaveBeenCalled();
|
$request->shouldNotHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @test */
|
||||||
* @test
|
public function validateUrlWithTitleReturnsNullWhenRequestFailsAndValidationIsDisabled(): void
|
||||||
* @dataProvider provideDisabledCombinations
|
{
|
||||||
*/
|
|
||||||
public function validateUrlWithTitleReturnsNullWhenRequestFailsAndValidationIsDisabled(
|
|
||||||
?bool $doValidate,
|
|
||||||
bool $validateUrl,
|
|
||||||
): void {
|
|
||||||
$request = $this->httpClient->request(Argument::cetera())->willThrow(ClientException::class);
|
$request = $this->httpClient->request(Argument::cetera())->willThrow(ClientException::class);
|
||||||
$this->options->validateUrl = $validateUrl;
|
|
||||||
$this->options->autoResolveTitles = true;
|
$this->options->autoResolveTitles = true;
|
||||||
|
|
||||||
$result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', $doValidate);
|
$result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', false);
|
||||||
|
|
||||||
self::assertNull($result);
|
self::assertNull($result);
|
||||||
$request->shouldHaveBeenCalledOnce();
|
$request->shouldHaveBeenCalledOnce();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function provideDisabledCombinations(): iterable
|
|
||||||
{
|
|
||||||
yield 'config is disabled and no runtime option is provided' => [null, false];
|
|
||||||
yield 'config is enabled but runtime option is disabled' => [false, true];
|
|
||||||
yield 'both config and runtime option are disabled' => [false, false];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function validateUrlWithTitleReturnsNullWhenAutoResolutionIsDisabled(): void
|
public function validateUrlWithTitleReturnsNullWhenAutoResolutionIsDisabled(): void
|
||||||
{
|
{
|
||||||
@@ -121,6 +104,18 @@ class UrlValidatorTest extends TestCase
|
|||||||
$request->shouldNotHaveBeenCalled();
|
$request->shouldNotHaveBeenCalled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @test */
|
||||||
|
public function validateUrlWithTitleReturnsNullWhenAutoResolutionIsDisabledAndValidationIsEnabled(): void
|
||||||
|
{
|
||||||
|
$request = $this->httpClient->request(Argument::cetera())->willReturn($this->respWithTitle());
|
||||||
|
$this->options->autoResolveTitles = false;
|
||||||
|
|
||||||
|
$result = $this->urlValidator->validateUrlWithTitle('http://foobar.com/12345/hello?foo=bar', true);
|
||||||
|
|
||||||
|
self::assertNull($result);
|
||||||
|
$request->shouldHaveBeenCalledOnce();
|
||||||
|
}
|
||||||
|
|
||||||
/** @test */
|
/** @test */
|
||||||
public function validateUrlWithTitleResolvesTitleWhenAutoResolutionIsEnabled(): void
|
public function validateUrlWithTitleResolvesTitleWhenAutoResolutionIsEnabled(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ class CreateShortUrlTest extends ApiTestCase
|
|||||||
{
|
{
|
||||||
$expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url);
|
$expectedDetail = sprintf('Provided URL %s is invalid. Try with a different one.', $url);
|
||||||
|
|
||||||
[$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url]);
|
[$statusCode, $payload] = $this->createShortUrl(['longUrl' => $url, 'validateUrl' => true]);
|
||||||
|
|
||||||
self::assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
|
self::assertEquals(self::STATUS_BAD_REQUEST, $statusCode);
|
||||||
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
|
self::assertEquals(self::STATUS_BAD_REQUEST, $payload['status']);
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ class EditShortUrlTest extends ApiTestCase
|
|||||||
|
|
||||||
$resp = $this->callApiWithKey(self::METHOD_PATCH, $url, [RequestOptions::JSON => [
|
$resp = $this->callApiWithKey(self::METHOD_PATCH, $url, [RequestOptions::JSON => [
|
||||||
'longUrl' => $longUrl,
|
'longUrl' => $longUrl,
|
||||||
|
'validateUrl' => true,
|
||||||
]]);
|
]]);
|
||||||
|
|
||||||
self::assertEquals($expectedStatus, $resp->getStatusCode());
|
self::assertEquals($expectedStatus, $resp->getStatusCode());
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ RewriteEngine On
|
|||||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||||
RewriteCond %{REQUEST_FILENAME} -l [OR]
|
RewriteCond %{REQUEST_FILENAME} -l [OR]
|
||||||
RewriteCond %{REQUEST_FILENAME} -d
|
RewriteCond %{REQUEST_FILENAME} -d
|
||||||
RewriteRule ^.*$ - [NC,L]
|
RewriteRule ^ - [NC,L]
|
||||||
|
|
||||||
# The following rewrites all other queries to index.php. The
|
# The following rewrites all other queries to index.php. The
|
||||||
# condition ensures that if you are using Apache aliases to do
|
# condition ensures that if you are using Apache aliases to do
|
||||||
@@ -12,6 +12,6 @@ RewriteRule ^.*$ - [NC,L]
|
|||||||
# allow proper resolution of the index.php file; it will work
|
# allow proper resolution of the index.php file; it will work
|
||||||
# in non-aliased environments as well, providing a safe, one-size
|
# in non-aliased environments as well, providing a safe, one-size
|
||||||
# fits all solution.
|
# fits all solution.
|
||||||
RewriteCond %{REQUEST_URI}::$1 ^(/.+)(.+)::\2$
|
RewriteCond $0::%{REQUEST_URI} ^([^:]*+(?::[^:]*+)*?)::(/.+?)\1$
|
||||||
RewriteRule ^(.*) - [E=BASE:%1]
|
RewriteRule .+ - [E=BASE:%2]
|
||||||
RewriteRule ^(.*)$ %{ENV:BASE}index.php [NC,L]
|
RewriteRule .* %{ENV:BASE}index.php [NC,L]
|
||||||
|
|||||||
Reference in New Issue
Block a user