mirror of
https://github.com/shlinkio/shlink.git
synced 2026-03-10 17:23:12 +08:00
Remove device long URLs support
This commit is contained in:
@@ -4,6 +4,7 @@ namespace Shlinkio\Shlink\Core\RedirectRule\Entity;
|
||||
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectConditionType;
|
||||
|
||||
use function Shlinkio\Shlink\Core\acceptLanguageToLocales;
|
||||
@@ -11,6 +12,7 @@ use function Shlinkio\Shlink\Core\ArrayUtils\some;
|
||||
use function Shlinkio\Shlink\Core\normalizeLocale;
|
||||
use function Shlinkio\Shlink\Core\splitLocale;
|
||||
use function sprintf;
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
|
||||
class RedirectCondition extends AbstractEntity
|
||||
@@ -39,6 +41,14 @@ class RedirectCondition extends AbstractEntity
|
||||
return new self($name, $type, $language);
|
||||
}
|
||||
|
||||
public static function forDevice(DeviceType $device): self
|
||||
{
|
||||
$type = RedirectConditionType::DEVICE;
|
||||
$name = sprintf('%s-%s', $type->value, $device->value);
|
||||
|
||||
return new self($name, $type, $device->value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if this condition matches provided request
|
||||
*/
|
||||
@@ -47,6 +57,7 @@ class RedirectCondition extends AbstractEntity
|
||||
return match ($this->type) {
|
||||
RedirectConditionType::QUERY_PARAM => $this->matchesQueryParam($request),
|
||||
RedirectConditionType::LANGUAGE => $this->matchesLanguage($request),
|
||||
RedirectConditionType::DEVICE => $this->matchesDevice($request),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,4 +92,10 @@ class RedirectCondition extends AbstractEntity
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private function matchesDevice(ServerRequestInterface $request): bool
|
||||
{
|
||||
$device = DeviceType::matchFromUserAgent($request->getHeaderLine('User-Agent'));
|
||||
return $device !== null && $device->value === strtolower($this->matchValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ namespace Shlinkio\Shlink\Core\RedirectRule;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
|
||||
@@ -27,7 +26,6 @@ readonly class ShortUrlRedirectionResolver implements ShortUrlRedirectionResolve
|
||||
}
|
||||
}
|
||||
|
||||
$device = DeviceType::matchFromUserAgent($request->getHeaderLine('User-Agent'));
|
||||
return $shortUrl->longUrlForDevice($device);
|
||||
return $shortUrl->getLongUrl();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Entity;
|
||||
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\DeviceLongUrlPair;
|
||||
|
||||
class DeviceLongUrl extends AbstractEntity
|
||||
{
|
||||
private function __construct(
|
||||
private readonly ShortUrl $shortUrl, // No need to read this field. It's used by doctrine
|
||||
public readonly DeviceType $deviceType,
|
||||
private string $longUrl,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromShortUrlAndPair(ShortUrl $shortUrl, DeviceLongUrlPair $pair): self
|
||||
{
|
||||
return new self($shortUrl, $pair->deviceType, $pair->longUrl);
|
||||
}
|
||||
|
||||
public function longUrl(): string
|
||||
{
|
||||
return $this->longUrl;
|
||||
}
|
||||
|
||||
public function updateLongUrl(string $longUrl): void
|
||||
{
|
||||
$this->longUrl = $longUrl;
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,6 @@ use Doctrine\Common\Collections\Selectable;
|
||||
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
|
||||
use Shlinkio\Shlink\Core\Domain\Entity\Domain;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\DeviceLongUrlPair;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
|
||||
@@ -26,10 +24,7 @@ use Shlinkio\Shlink\Core\Visit\Model\VisitType;
|
||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
use function array_fill_keys;
|
||||
use function array_map;
|
||||
use function count;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
use function Shlinkio\Shlink\Core\generateRandomShortCode;
|
||||
use function Shlinkio\Shlink\Core\normalizeDate;
|
||||
use function Shlinkio\Shlink\Core\normalizeOptionalDate;
|
||||
@@ -42,8 +37,6 @@ class ShortUrl extends AbstractEntity
|
||||
private Chronos $dateCreated;
|
||||
/** @var Collection<int, Visit> & Selectable */
|
||||
private Collection & Selectable $visits;
|
||||
/** @var Collection<string, DeviceLongUrl> */
|
||||
private Collection $deviceLongUrls;
|
||||
/** @var Collection<int, Tag> */
|
||||
private Collection $tags;
|
||||
private ?Chronos $validSince = null;
|
||||
@@ -91,10 +84,6 @@ class ShortUrl extends AbstractEntity
|
||||
$instance->longUrl = $creation->getLongUrl();
|
||||
$instance->dateCreated = Chronos::now();
|
||||
$instance->visits = new ArrayCollection();
|
||||
$instance->deviceLongUrls = new ArrayCollection(array_map(
|
||||
fn (DeviceLongUrlPair $pair) => DeviceLongUrl::fromShortUrlAndPair($instance, $pair),
|
||||
$creation->deviceLongUrls,
|
||||
));
|
||||
$instance->tags = $relationResolver->resolveTags($creation->tags);
|
||||
$instance->validSince = $creation->validSince;
|
||||
$instance->validUntil = $creation->validUntil;
|
||||
@@ -177,21 +166,6 @@ class ShortUrl extends AbstractEntity
|
||||
if ($shortUrlEdit->forwardQueryWasProvided()) {
|
||||
$this->forwardQuery = $shortUrlEdit->forwardQuery;
|
||||
}
|
||||
|
||||
// Update device long URLs, removing, editing or creating where appropriate
|
||||
foreach ($shortUrlEdit->devicesToRemove as $deviceType) {
|
||||
$this->deviceLongUrls->remove($deviceType->value);
|
||||
}
|
||||
foreach ($shortUrlEdit->deviceLongUrls as $deviceLongUrlPair) {
|
||||
$key = $deviceLongUrlPair->deviceType->value;
|
||||
$deviceLongUrl = $this->deviceLongUrls->get($key);
|
||||
|
||||
if ($deviceLongUrl !== null) {
|
||||
$deviceLongUrl->updateLongUrl($deviceLongUrlPair->longUrl);
|
||||
} else {
|
||||
$this->deviceLongUrls->set($key, DeviceLongUrl::fromShortUrlAndPair($this, $deviceLongUrlPair));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getLongUrl(): string
|
||||
@@ -199,12 +173,6 @@ class ShortUrl extends AbstractEntity
|
||||
return $this->longUrl;
|
||||
}
|
||||
|
||||
public function longUrlForDevice(?DeviceType $deviceType): string
|
||||
{
|
||||
$deviceLongUrl = $deviceType === null ? null : $this->deviceLongUrls->get($deviceType->value);
|
||||
return $deviceLongUrl?->longUrl() ?? $this->longUrl;
|
||||
}
|
||||
|
||||
public function getShortCode(): string
|
||||
{
|
||||
return $this->shortCode;
|
||||
@@ -332,14 +300,4 @@ class ShortUrl extends AbstractEntity
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deviceLongUrls(): array
|
||||
{
|
||||
$data = array_fill_keys(enumValues(DeviceType::class), null);
|
||||
foreach ($this->deviceLongUrls as $deviceUrl) {
|
||||
$data[$deviceUrl->deviceType->value] = $deviceUrl->longUrl();
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Model;
|
||||
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
|
||||
use function trim;
|
||||
|
||||
final class DeviceLongUrlPair
|
||||
{
|
||||
private function __construct(public readonly DeviceType $deviceType, public readonly string $longUrl)
|
||||
{
|
||||
}
|
||||
|
||||
public static function fromRawTypeAndLongUrl(string $type, string $longUrl): self
|
||||
{
|
||||
return new self(DeviceType::from($type), trim($longUrl));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with two values.
|
||||
* * The first one is a list of mapped instances for those entries in the map with non-null value
|
||||
* * The second is a list of DeviceTypes which have been provided with value null
|
||||
*
|
||||
* @param array<string, string|null> $map
|
||||
* @return array{array<string, self>, DeviceType[]}
|
||||
*/
|
||||
public static function fromMapToChangeSet(array $map): array
|
||||
{
|
||||
$pairsToKeep = [];
|
||||
$deviceTypesToRemove = [];
|
||||
|
||||
foreach ($map as $deviceType => $longUrl) {
|
||||
if ($longUrl === null) {
|
||||
$deviceTypesToRemove[] = DeviceType::from($deviceType);
|
||||
} else {
|
||||
$pairsToKeep[$deviceType] = self::fromRawTypeAndLongUrl($deviceType, $longUrl);
|
||||
}
|
||||
}
|
||||
|
||||
return [$pairsToKeep, $deviceTypesToRemove];
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,10 @@ final readonly class ShortUrlCreation implements TitleResolutionModelInterface
|
||||
{
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @param DeviceLongUrlPair[] $deviceLongUrls
|
||||
*/
|
||||
private function __construct(
|
||||
public string $longUrl,
|
||||
public ShortUrlMode $shortUrlMode,
|
||||
public array $deviceLongUrls = [],
|
||||
public ?Chronos $validSince = null,
|
||||
public ?Chronos $validUntil = null,
|
||||
public ?string $customSlug = null,
|
||||
@@ -55,14 +53,9 @@ final readonly class ShortUrlCreation implements TitleResolutionModelInterface
|
||||
throw ValidationException::fromInputFilter($inputFilter);
|
||||
}
|
||||
|
||||
[$deviceLongUrls] = DeviceLongUrlPair::fromMapToChangeSet(
|
||||
$inputFilter->getValue(ShortUrlInputFilter::DEVICE_LONG_URLS) ?? [],
|
||||
);
|
||||
|
||||
return new self(
|
||||
longUrl: $inputFilter->getValue(ShortUrlInputFilter::LONG_URL),
|
||||
shortUrlMode: $options->mode,
|
||||
deviceLongUrls: $deviceLongUrls,
|
||||
validSince: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE)),
|
||||
validUntil: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL)),
|
||||
customSlug: $inputFilter->getValue(ShortUrlInputFilter::CUSTOM_SLUG),
|
||||
@@ -87,7 +80,6 @@ final readonly class ShortUrlCreation implements TitleResolutionModelInterface
|
||||
return new self(
|
||||
longUrl: $this->longUrl,
|
||||
shortUrlMode: $this->shortUrlMode,
|
||||
deviceLongUrls: $this->deviceLongUrls,
|
||||
validSince: $this->validSince,
|
||||
validUntil: $this->validUntil,
|
||||
customSlug: $this->customSlug,
|
||||
|
||||
@@ -6,7 +6,6 @@ namespace Shlinkio\Shlink\Core\ShortUrl\Model;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\TitleResolutionModelInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
|
||||
@@ -19,14 +18,10 @@ final readonly class ShortUrlEdition implements TitleResolutionModelInterface
|
||||
{
|
||||
/**
|
||||
* @param string[] $tags
|
||||
* @param DeviceLongUrlPair[] $deviceLongUrls
|
||||
* @param DeviceType[] $devicesToRemove
|
||||
*/
|
||||
private function __construct(
|
||||
private bool $longUrlPropWasProvided = false,
|
||||
public ?string $longUrl = null,
|
||||
public array $deviceLongUrls = [],
|
||||
public array $devicesToRemove = [],
|
||||
private bool $validSincePropWasProvided = false,
|
||||
public ?Chronos $validSince = null,
|
||||
private bool $validUntilPropWasProvided = false,
|
||||
@@ -55,15 +50,9 @@ final readonly class ShortUrlEdition implements TitleResolutionModelInterface
|
||||
throw ValidationException::fromInputFilter($inputFilter);
|
||||
}
|
||||
|
||||
[$deviceLongUrls, $devicesToRemove] = DeviceLongUrlPair::fromMapToChangeSet(
|
||||
$inputFilter->getValue(ShortUrlInputFilter::DEVICE_LONG_URLS) ?? [],
|
||||
);
|
||||
|
||||
return new self(
|
||||
longUrlPropWasProvided: array_key_exists(ShortUrlInputFilter::LONG_URL, $data),
|
||||
longUrl: $inputFilter->getValue(ShortUrlInputFilter::LONG_URL),
|
||||
deviceLongUrls: $deviceLongUrls,
|
||||
devicesToRemove: $devicesToRemove,
|
||||
validSincePropWasProvided: array_key_exists(ShortUrlInputFilter::VALID_SINCE, $data),
|
||||
validSince: normalizeOptionalDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE)),
|
||||
validUntilPropWasProvided: array_key_exists(ShortUrlInputFilter::VALID_UNTIL, $data),
|
||||
@@ -86,8 +75,6 @@ final readonly class ShortUrlEdition implements TitleResolutionModelInterface
|
||||
return new self(
|
||||
longUrlPropWasProvided: $this->longUrlPropWasProvided,
|
||||
longUrl: $this->longUrl,
|
||||
deviceLongUrls: $this->deviceLongUrls,
|
||||
devicesToRemove: $this->devicesToRemove,
|
||||
validSincePropWasProvided: $this->validSincePropWasProvided,
|
||||
validSince: $this->validSince,
|
||||
validUntilPropWasProvided: $this->validUntilPropWasProvided,
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core\ShortUrl\Model\Validation;
|
||||
|
||||
use Laminas\Validator\AbstractValidator;
|
||||
use Laminas\Validator\ValidatorInterface;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
|
||||
use function array_keys;
|
||||
use function array_values;
|
||||
use function is_array;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\contains;
|
||||
use function Shlinkio\Shlink\Core\ArrayUtils\every;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
class DeviceLongUrlsValidator extends AbstractValidator
|
||||
{
|
||||
private const NOT_ARRAY = 'NOT_ARRAY';
|
||||
private const INVALID_DEVICE = 'INVALID_DEVICE';
|
||||
private const INVALID_LONG_URL = 'INVALID_LONG_URL';
|
||||
|
||||
protected array $messageTemplates = [
|
||||
self::NOT_ARRAY => 'Provided value is not an array.',
|
||||
self::INVALID_DEVICE => 'You have provided at least one invalid device identifier.',
|
||||
self::INVALID_LONG_URL => 'At least one of the long URLs are invalid.',
|
||||
];
|
||||
|
||||
public function __construct(private readonly ValidatorInterface $longUrlValidators)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function isValid(mixed $value): bool
|
||||
{
|
||||
if (! is_array($value)) {
|
||||
$this->error(self::NOT_ARRAY);
|
||||
return false;
|
||||
}
|
||||
|
||||
$validValues = enumValues(DeviceType::class);
|
||||
$keys = array_keys($value);
|
||||
if (! every($keys, static fn ($key) => contains($key, $validValues))) {
|
||||
$this->error(self::INVALID_DEVICE);
|
||||
return false;
|
||||
}
|
||||
|
||||
$longUrls = array_values($value);
|
||||
$result = every($longUrls, $this->longUrlValidators->isValid(...));
|
||||
if (! $result) {
|
||||
$this->error(self::INVALID_LONG_URL);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,6 @@ class ShortUrlInputFilter extends InputFilter
|
||||
|
||||
// Fields for creation and edition
|
||||
public const LONG_URL = 'longUrl';
|
||||
public const DEVICE_LONG_URLS = 'deviceLongUrls';
|
||||
public const VALID_SINCE = 'validSince';
|
||||
public const VALID_UNTIL = 'validUntil';
|
||||
public const MAX_VISITS = 'maxVisits';
|
||||
@@ -97,12 +96,6 @@ class ShortUrlInputFilter extends InputFilter
|
||||
$longUrlInput->getValidatorChain()->merge($this->longUrlValidators());
|
||||
$this->add($longUrlInput);
|
||||
|
||||
$deviceLongUrlsInput = InputFactory::basic(self::DEVICE_LONG_URLS);
|
||||
$deviceLongUrlsInput->getValidatorChain()->attach(
|
||||
new DeviceLongUrlsValidator($this->longUrlValidators(allowNull: ! $requireLongUrl)),
|
||||
);
|
||||
$this->add($deviceLongUrlsInput);
|
||||
|
||||
$validSince = InputFactory::basic(self::VALID_SINCE);
|
||||
$validSince->getValidatorChain()->attach(new Validator\Date(['format' => DateTimeInterface::ATOM]));
|
||||
$this->add($validSince);
|
||||
|
||||
@@ -27,7 +27,6 @@ class ShortUrlDataTransformer implements DataTransformerInterface
|
||||
'shortCode' => $shortUrl->getShortCode(),
|
||||
'shortUrl' => $this->stringifier->stringify($shortUrl),
|
||||
'longUrl' => $shortUrl->getLongUrl(),
|
||||
'deviceLongUrls' => $shortUrl->deviceLongUrls(),
|
||||
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
|
||||
'tags' => array_map(static fn (Tag $tag) => $tag->__toString(), $shortUrl->getTags()->toArray()),
|
||||
'meta' => $this->buildMeta($shortUrl),
|
||||
|
||||
Reference in New Issue
Block a user