mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-21 16:38:37 -06:00
Remove device long URLs support
This commit is contained in:
parent
4ad3dc0bc7
commit
36749658da
@ -16,6 +16,9 @@
|
||||
If you want to keep previous behavior, pass `QR_CODE_FOR_DISABLED_SHORT_URLS=false` or the equivalent configuration option.
|
||||
* Long URL title resolution is now enabled by default. You can still disable it by passing `AUTO_RESOLVE_TITLES=false` or the equivalent configuration option.
|
||||
* Shlink no longer allows to opt-in for long URL verification. Long URLs are unconditionally considered correct during short URL creation/edition.
|
||||
* Device long URLs have been migrated to the new Dynamic rule-based redirects system.
|
||||
All existing short URLs which were using device long URLs will be automatically migrated and continue working as expected, but the API surface has changed.
|
||||
If you use shlink-web-client and rely on this feature when creating/updating short URLs, **DO NOT UPDATE YET**. Support for dynamic rule-based redirects will be added to shlink-web-client soon, in v4.1.0
|
||||
|
||||
### Changes in REST API
|
||||
|
||||
@ -34,6 +37,7 @@
|
||||
* Endpoints previously returning props like `"visitsCount": {number}` no longer do it. There should be an alternative `"visitsSummary": {}` object with the amount nested on it.
|
||||
* It is no longer possible to order the short URLs list with `orderBy=visitsCount-ASC`/`orderBy=visitsCount-DESC`. Use `orderBy=visits-ASC`/`orderBy=visits-DESC` instead.
|
||||
* It is no longer possible to get tags with stats using `GET /tags?withStats=true`. Use `GET /tags/stats` endpoint instead.
|
||||
* The `deviceLongUrls` are ignored when calling `POST /short-urls` or `PATCH /short-urls/{shortCode}`. These should now be configured as dynamic rule-based redirects via `POST /short-urls/{shortCode}/redirect-rules`.
|
||||
|
||||
### Changes in Docker image
|
||||
|
||||
|
@ -111,9 +111,6 @@
|
||||
"type": "string",
|
||||
"description": "The original long URL."
|
||||
},
|
||||
"deviceLongUrls": {
|
||||
"$ref": "#/components/schemas/DeviceLongUrls"
|
||||
},
|
||||
"dateCreated": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
@ -150,11 +147,6 @@
|
||||
"shortCode": "12C18",
|
||||
"shortUrl": "https://s.test/12C18",
|
||||
"longUrl": "https://store.steampowered.com",
|
||||
"deviceLongUrls": {
|
||||
"android": "https://store.steampowered.com/android",
|
||||
"ios": "https://store.steampowered.com/ios",
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsSummary": {
|
||||
"total": 328,
|
||||
@ -218,24 +210,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeviceLongUrls": {
|
||||
"type": "object",
|
||||
"required": ["android", "ios", "desktop"],
|
||||
"properties": {
|
||||
"android": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
|
||||
"type": "string"
|
||||
},
|
||||
"ios": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
|
||||
"type": "string"
|
||||
},
|
||||
"desktop": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Visit": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"android": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running Android",
|
||||
"type": ["string"]
|
||||
},
|
||||
"ios": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a device running iOS",
|
||||
"type": ["string"]
|
||||
},
|
||||
"desktop": {
|
||||
"description": "The long URL to redirect to when the short URL is visited from a desktop browser",
|
||||
"type": ["string"]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"type": "object",
|
||||
"allOf": [{
|
||||
"$ref": "./DeviceLongUrls.json"
|
||||
}],
|
||||
"properties": {
|
||||
"android": {
|
||||
"type": ["null"]
|
||||
},
|
||||
"ios": {
|
||||
"type": ["null"]
|
||||
},
|
||||
"desktop": {
|
||||
"type": ["null"]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"type": "object",
|
||||
"required": ["android", "ios", "desktop"],
|
||||
"allOf": [{
|
||||
"$ref": "./DeviceLongUrlsEdit.json"
|
||||
}]
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
"shortCode",
|
||||
"shortUrl",
|
||||
"longUrl",
|
||||
"deviceLongUrls",
|
||||
"dateCreated",
|
||||
"visitsSummary",
|
||||
"tags",
|
||||
@ -27,9 +26,6 @@
|
||||
"type": "string",
|
||||
"description": "The original long URL."
|
||||
},
|
||||
"deviceLongUrls": {
|
||||
"$ref": "./DeviceLongUrlsResp.json"
|
||||
},
|
||||
"dateCreated": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
|
@ -5,9 +5,6 @@
|
||||
"description": "The long URL this short URL will redirect to",
|
||||
"type": "string"
|
||||
},
|
||||
"deviceLongUrls": {
|
||||
"$ref": "./DeviceLongUrlsEdit.json"
|
||||
},
|
||||
"validSince": {
|
||||
"description": "The date (in ISO-8601 format) from which this short code will be valid",
|
||||
"type": ["string", "null"]
|
||||
|
@ -163,11 +163,6 @@
|
||||
"shortCode": "12C18",
|
||||
"shortUrl": "https://s.test/12C18",
|
||||
"longUrl": "https://store.steampowered.com",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsSummary": {
|
||||
"total": 328,
|
||||
@ -191,11 +186,6 @@
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://s.test/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": "https://shlink.io/ios",
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsSummary": {
|
||||
"total": 1029,
|
||||
@ -218,11 +208,6 @@
|
||||
"shortCode": "123bA",
|
||||
"shortUrl": "https://example.com/123bA",
|
||||
"longUrl": "https://www.google.com",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2015-10-01T20:34:16+02:00",
|
||||
"visitsSummary": {
|
||||
"total": 25,
|
||||
@ -296,9 +281,6 @@
|
||||
"type": "object",
|
||||
"required": ["longUrl"],
|
||||
"properties": {
|
||||
"deviceLongUrls": {
|
||||
"$ref": "../definitions/DeviceLongUrls.json"
|
||||
},
|
||||
"customSlug": {
|
||||
"description": "A unique custom slug to be used instead of the generated short code",
|
||||
"type": "string"
|
||||
@ -338,11 +320,6 @@
|
||||
"shortCode": "12C18",
|
||||
"shortUrl": "https://s.test/12C18",
|
||||
"longUrl": "https://store.steampowered.com",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
"visitsSummary": {
|
||||
"total": 0,
|
||||
|
@ -53,11 +53,6 @@
|
||||
},
|
||||
"example": {
|
||||
"longUrl": "https://github.com/shlinkio/shlink",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"shortUrl": "https://s.test/abc123",
|
||||
"shortCode": "abc123",
|
||||
"dateCreated": "2016-08-21T20:34:16+02:00",
|
||||
|
@ -34,11 +34,6 @@
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://s.test/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"deviceLongUrls": {
|
||||
"android": null,
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsSummary": {
|
||||
"total": 1029,
|
||||
@ -155,11 +150,6 @@
|
||||
"shortCode": "12Kb3",
|
||||
"shortUrl": "https://s.test/12Kb3",
|
||||
"longUrl": "https://shlink.io",
|
||||
"deviceLongUrls": {
|
||||
"android": "https://shlink.io/android",
|
||||
"ios": null,
|
||||
"desktop": null
|
||||
},
|
||||
"dateCreated": "2016-05-01T20:34:16+02:00",
|
||||
"visitsSummary": {
|
||||
"total": 1029,
|
||||
|
@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\Core;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\Builder\ClassMetadataBuilder;
|
||||
use Doctrine\ORM\Mapping\Builder\FieldBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
|
||||
return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable(determineTableName('device_long_urls', $emConfig));
|
||||
|
||||
$builder->createField('id', Types::BIGINT)
|
||||
->columnName('id')
|
||||
->makePrimaryKey()
|
||||
->generatedValue('IDENTITY')
|
||||
->option('unsigned', true)
|
||||
->build();
|
||||
|
||||
(new FieldBuilder($builder, [
|
||||
'fieldName' => 'deviceType',
|
||||
'type' => Types::STRING,
|
||||
'enumType' => DeviceType::class,
|
||||
]))->columnName('device_type')
|
||||
->length(255)
|
||||
->build();
|
||||
|
||||
fieldWithUtf8Charset($builder->createField('longUrl', Types::TEXT), $emConfig)
|
||||
->columnName('long_url')
|
||||
->length(2048)
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('shortUrl', ShortUrl\Entity\ShortUrl::class)
|
||||
->addJoinColumn('short_url_id', 'id', nullable: false, onDelete: 'CASCADE')
|
||||
->build();
|
||||
};
|
@ -67,13 +67,6 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('deviceLongUrls', ShortUrl\Entity\DeviceLongUrl::class)
|
||||
->mappedBy('shortUrl')
|
||||
->cascadePersist()
|
||||
->orphanRemoval()
|
||||
->setIndexBy('deviceType')
|
||||
->build();
|
||||
|
||||
$builder->createManyToMany('tags', Tag\Entity\Tag::class)
|
||||
->setJoinTable(determineTableName('short_urls_in_tags', $emConfig))
|
||||
->addInverseJoinColumn('tag_id', 'id', onDelete: 'CASCADE')
|
||||
|
@ -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),
|
||||
|
@ -51,7 +51,6 @@ class PublishingUpdatesGeneratorTest extends TestCase
|
||||
'shortCode' => $shortUrl->getShortCode(),
|
||||
'shortUrl' => 'http:/' . $shortUrl->getShortCode(),
|
||||
'longUrl' => 'https://longUrl',
|
||||
'deviceLongUrls' => $shortUrl->deviceLongUrls(),
|
||||
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
@ -125,7 +124,6 @@ class PublishingUpdatesGeneratorTest extends TestCase
|
||||
'shortCode' => $shortUrl->getShortCode(),
|
||||
'shortUrl' => 'http:/' . $shortUrl->getShortCode(),
|
||||
'longUrl' => 'https://longUrl',
|
||||
'deviceLongUrls' => $shortUrl->deviceLongUrls(),
|
||||
'dateCreated' => $shortUrl->getDateCreated()->toAtomString(),
|
||||
'tags' => [],
|
||||
'meta' => [
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace RedirectRule\Entity;
|
||||
namespace ShlinkioTest\Shlink\Core\RedirectRule\Entity;
|
||||
|
||||
use Laminas\Diactoros\ServerRequestFactory;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace RedirectRule;
|
||||
namespace ShlinkioTest\Shlink\Core\RedirectRule;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
@ -41,10 +41,6 @@ class ShortUrlRedirectionResolverTest extends TestCase
|
||||
): void {
|
||||
$shortUrl = ShortUrl::create(ShortUrlCreation::fromRawData([
|
||||
'longUrl' => 'https://example.com/foo/bar',
|
||||
'deviceLongUrls' => [
|
||||
DeviceType::ANDROID->value => 'https://example.com/android',
|
||||
DeviceType::IOS->value => 'https://example.com/ios',
|
||||
],
|
||||
]));
|
||||
|
||||
$repo = $this->createMock(EntityRepository::class);
|
||||
@ -75,12 +71,16 @@ class ShortUrlRedirectionResolverTest extends TestCase
|
||||
'https://example.com/foo/bar',
|
||||
];
|
||||
yield 'desktop user agent' => [$request(DESKTOP_USER_AGENT), null, 'https://example.com/foo/bar'];
|
||||
yield 'android user agent' => [
|
||||
yield 'matching android device' => [
|
||||
$request(ANDROID_USER_AGENT),
|
||||
RedirectCondition::forQueryParam('foo', 'bar'), // This condition won't match
|
||||
'https://example.com/android',
|
||||
RedirectCondition::forDevice(DeviceType::ANDROID),
|
||||
'https://example.com/from-rule',
|
||||
];
|
||||
yield 'matching ios device' => [
|
||||
$request(IOS_USER_AGENT),
|
||||
RedirectCondition::forDevice(DeviceType::IOS),
|
||||
'https://example.com/from-rule',
|
||||
];
|
||||
yield 'ios user agent' => [$request(IOS_USER_AGENT), null, 'https://example.com/ios'];
|
||||
yield 'matching language' => [
|
||||
$request()->withHeader('Accept-Language', 'es-ES'),
|
||||
RedirectCondition::forLanguage('es-ES'),
|
||||
|
@ -10,11 +10,9 @@ use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\Attributes\TestWith;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
|
||||
@ -113,48 +111,6 @@ class ShortUrlTest extends TestCase
|
||||
self::assertEquals($expectedShortCodeLength, strlen($shortCode));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function deviceLongUrlsAreUpdated(): void
|
||||
{
|
||||
$shortUrl = ShortUrl::withLongUrl('https://foo');
|
||||
|
||||
$shortUrl->update(ShortUrlEdition::fromRawData([
|
||||
ShortUrlInputFilter::DEVICE_LONG_URLS => [
|
||||
DeviceType::ANDROID->value => 'https://android',
|
||||
DeviceType::IOS->value => 'https://ios',
|
||||
],
|
||||
]));
|
||||
self::assertEquals([
|
||||
DeviceType::ANDROID->value => 'https://android',
|
||||
DeviceType::IOS->value => 'https://ios',
|
||||
DeviceType::DESKTOP->value => null,
|
||||
], $shortUrl->deviceLongUrls());
|
||||
|
||||
$shortUrl->update(ShortUrlEdition::fromRawData([
|
||||
ShortUrlInputFilter::DEVICE_LONG_URLS => [
|
||||
DeviceType::ANDROID->value => null,
|
||||
DeviceType::DESKTOP->value => 'https://desktop',
|
||||
],
|
||||
]));
|
||||
self::assertEquals([
|
||||
DeviceType::ANDROID->value => null,
|
||||
DeviceType::IOS->value => 'https://ios',
|
||||
DeviceType::DESKTOP->value => 'https://desktop',
|
||||
], $shortUrl->deviceLongUrls());
|
||||
|
||||
$shortUrl->update(ShortUrlEdition::fromRawData([
|
||||
ShortUrlInputFilter::DEVICE_LONG_URLS => [
|
||||
DeviceType::ANDROID->value => null,
|
||||
DeviceType::IOS->value => null,
|
||||
],
|
||||
]));
|
||||
self::assertEquals([
|
||||
DeviceType::ANDROID->value => null,
|
||||
DeviceType::IOS->value => null,
|
||||
DeviceType::DESKTOP->value => 'https://desktop',
|
||||
], $shortUrl->deviceLongUrls());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function generatesLowercaseOnlyShortCodesInLooseMode(): void
|
||||
{
|
||||
|
@ -9,7 +9,6 @@ use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Exception\ValidationException;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlMode;
|
||||
@ -79,43 +78,6 @@ class ShortUrlCreationTest extends TestCase
|
||||
yield [[
|
||||
ShortUrlInputFilter::LONG_URL => 'missing_schema',
|
||||
]];
|
||||
yield [[
|
||||
ShortUrlInputFilter::LONG_URL => 'https://foo',
|
||||
ShortUrlInputFilter::DEVICE_LONG_URLS => [
|
||||
'invalid' => 'https://shlink.io',
|
||||
],
|
||||
]];
|
||||
yield [[
|
||||
ShortUrlInputFilter::LONG_URL => 'https://foo',
|
||||
ShortUrlInputFilter::DEVICE_LONG_URLS => [
|
||||
DeviceType::DESKTOP->value => '',
|
||||
],
|
||||
]];
|
||||
yield [[
|
||||
ShortUrlInputFilter::LONG_URL => 'https://foo',
|
||||
ShortUrlInputFilter::DEVICE_LONG_URLS => [
|
||||
DeviceType::DESKTOP->value => null,
|
||||
],
|
||||
]];
|
||||
yield [[
|
||||
ShortUrlInputFilter::LONG_URL => 'https://foo',
|
||||
ShortUrlInputFilter::DEVICE_LONG_URLS => [
|
||||
DeviceType::IOS->value => ' ',
|
||||
],
|
||||
]];
|
||||
yield [[
|
||||
ShortUrlInputFilter::LONG_URL => 'https://foo',
|
||||
ShortUrlInputFilter::DEVICE_LONG_URLS => [
|
||||
DeviceType::ANDROID->value => 'missing_schema',
|
||||
],
|
||||
]];
|
||||
yield [[
|
||||
ShortUrlInputFilter::LONG_URL => 'https://foo',
|
||||
ShortUrlInputFilter::DEVICE_LONG_URLS => [
|
||||
DeviceType::IOS->value => 'https://bar',
|
||||
DeviceType::ANDROID->value => [],
|
||||
],
|
||||
]];
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideCustomSlugs')]
|
||||
|
@ -1,59 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\ShortUrl\Model;
|
||||
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\DeviceLongUrlPair;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
|
||||
class ShortUrlEditionTest extends TestCase
|
||||
{
|
||||
#[Test, DataProvider('provideDeviceLongUrls')]
|
||||
public function expectedDeviceLongUrlsAreResolved(
|
||||
?array $deviceLongUrls,
|
||||
array $expectedDeviceLongUrls,
|
||||
array $expectedDevicesToRemove,
|
||||
): void {
|
||||
$edition = ShortUrlEdition::fromRawData([ShortUrlInputFilter::DEVICE_LONG_URLS => $deviceLongUrls]);
|
||||
|
||||
self::assertEquals($expectedDeviceLongUrls, $edition->deviceLongUrls);
|
||||
self::assertEquals($expectedDevicesToRemove, $edition->devicesToRemove);
|
||||
}
|
||||
|
||||
public static function provideDeviceLongUrls(): iterable
|
||||
{
|
||||
yield 'null' => [null, [], []];
|
||||
yield 'empty' => [[], [], []];
|
||||
yield 'only new urls' => [[
|
||||
DeviceType::DESKTOP->value => 'https://foo',
|
||||
DeviceType::IOS->value => 'https://bar',
|
||||
], [
|
||||
DeviceType::DESKTOP->value => DeviceLongUrlPair::fromRawTypeAndLongUrl(
|
||||
DeviceType::DESKTOP->value,
|
||||
'https://foo',
|
||||
),
|
||||
DeviceType::IOS->value => DeviceLongUrlPair::fromRawTypeAndLongUrl(DeviceType::IOS->value, 'https://bar'),
|
||||
], []];
|
||||
yield 'only urls to remove' => [[
|
||||
DeviceType::ANDROID->value => null,
|
||||
DeviceType::IOS->value => null,
|
||||
], [], [DeviceType::ANDROID, DeviceType::IOS]];
|
||||
yield 'both' => [[
|
||||
DeviceType::DESKTOP->value => 'https://bar',
|
||||
DeviceType::IOS->value => 'https://foo',
|
||||
DeviceType::ANDROID->value => null,
|
||||
], [
|
||||
DeviceType::DESKTOP->value => DeviceLongUrlPair::fromRawTypeAndLongUrl(
|
||||
DeviceType::DESKTOP->value,
|
||||
'https://bar',
|
||||
),
|
||||
DeviceType::IOS->value => DeviceLongUrlPair::fromRawTypeAndLongUrl(DeviceType::IOS->value, 'https://foo'),
|
||||
], [DeviceType::ANDROID]];
|
||||
}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ShlinkioTest\Shlink\Core\ShortUrl\Model\Validation;
|
||||
|
||||
use Laminas\Validator\NotEmpty;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\DeviceLongUrlsValidator;
|
||||
use stdClass;
|
||||
|
||||
class DeviceLongUrlsValidatorTest extends TestCase
|
||||
{
|
||||
private DeviceLongUrlsValidator $validator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->validator = new DeviceLongUrlsValidator(new NotEmpty());
|
||||
}
|
||||
|
||||
#[Test, DataProvider('provideNonArrayValues')]
|
||||
public function nonArrayValuesAreNotValid(mixed $invalidValue): void
|
||||
{
|
||||
self::assertFalse($this->validator->isValid($invalidValue));
|
||||
self::assertEquals(['NOT_ARRAY' => 'Provided value is not an array.'], $this->validator->getMessages());
|
||||
}
|
||||
|
||||
public static function provideNonArrayValues(): iterable
|
||||
{
|
||||
yield 'int' => [0];
|
||||
yield 'float' => [100.45];
|
||||
yield 'string' => ['foo'];
|
||||
yield 'boolean' => [true];
|
||||
yield 'object' => [new stdClass()];
|
||||
yield 'null' => [null];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function unrecognizedKeysAreNotValid(): void
|
||||
{
|
||||
self::assertFalse($this->validator->isValid(['foo' => 'bar']));
|
||||
self::assertEquals(
|
||||
['INVALID_DEVICE' => 'You have provided at least one invalid device identifier.'],
|
||||
$this->validator->getMessages(),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function everyUrlMustMatchLongUrlValidator(): void
|
||||
{
|
||||
self::assertFalse($this->validator->isValid([DeviceType::ANDROID->value => '']));
|
||||
self::assertEquals(
|
||||
['INVALID_LONG_URL' => 'At least one of the long URLs are invalid.'],
|
||||
$this->validator->getMessages(),
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function validValuesResultInValidResult(): void
|
||||
{
|
||||
self::assertTrue($this->validator->isValid([
|
||||
DeviceType::ANDROID->value => 'foo',
|
||||
DeviceType::IOS->value => 'bar',
|
||||
DeviceType::DESKTOP->value => 'baz',
|
||||
]));
|
||||
}
|
||||
}
|
@ -12,7 +12,6 @@ use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
|
||||
use PHPUnit\Framework\MockObject\Rule\InvokedCount;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlTitleResolutionHelperInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||
@ -22,9 +21,6 @@ use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlService;
|
||||
use Shlinkio\Shlink\Rest\Entity\ApiKey;
|
||||
|
||||
use function array_fill_keys;
|
||||
use function Shlinkio\Shlink\Core\enumValues;
|
||||
|
||||
class ShortUrlServiceTest extends TestCase
|
||||
{
|
||||
private ShortUrlService $service;
|
||||
@ -73,21 +69,11 @@ class ShortUrlServiceTest extends TestCase
|
||||
$apiKey,
|
||||
);
|
||||
|
||||
$resolveDeviceLongUrls = function () use ($shortUrlEdit): array {
|
||||
$result = array_fill_keys(enumValues(DeviceType::class), null);
|
||||
foreach ($shortUrlEdit->deviceLongUrls ?? [] as $longUrl) {
|
||||
$result[$longUrl->deviceType->value] = $longUrl->longUrl;
|
||||
}
|
||||
|
||||
return $result;
|
||||
};
|
||||
|
||||
self::assertSame($shortUrl, $result);
|
||||
self::assertEquals($shortUrlEdit->validSince, $shortUrl->getValidSince());
|
||||
self::assertEquals($shortUrlEdit->validUntil, $shortUrl->getValidUntil());
|
||||
self::assertEquals($shortUrlEdit->maxVisits, $shortUrl->getMaxVisits());
|
||||
self::assertEquals($shortUrlEdit->longUrl ?? $originalLongUrl, $shortUrl->getLongUrl());
|
||||
self::assertEquals($resolveDeviceLongUrls(), $shortUrl->deviceLongUrls());
|
||||
}
|
||||
|
||||
public static function provideShortUrlEdits(): iterable
|
||||
@ -102,11 +88,5 @@ class ShortUrlServiceTest extends TestCase
|
||||
'maxVisits' => 10,
|
||||
'longUrl' => 'https://modifiedLongUrl',
|
||||
]), ApiKey::create()];
|
||||
yield 'device redirects' => [new InvokedCount(0), ShortUrlEdition::fromRawData([
|
||||
'deviceLongUrls' => [
|
||||
DeviceType::IOS->value => 'https://iosLongUrl',
|
||||
DeviceType::ANDROID->value => 'https://androidLongUrl',
|
||||
],
|
||||
]), null];
|
||||
}
|
||||
}
|
||||
|
@ -250,18 +250,6 @@ class CreateShortUrlTest extends ApiTestCase
|
||||
yield 'empty long url v3' => [['longUrl' => ' '], '3', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'missing url schema v2' => [['longUrl' => 'foo.com'], '2', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'missing url schema v3' => [['longUrl' => 'foo.com'], '3', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'empty device long url v2' => [[
|
||||
'longUrl' => 'foo',
|
||||
'deviceLongUrls' => [
|
||||
'android' => null,
|
||||
],
|
||||
], '2', 'https://shlink.io/api/error/invalid-data'];
|
||||
yield 'empty device long url v3' => [[
|
||||
'longUrl' => 'foo',
|
||||
'deviceLongUrls' => [
|
||||
'ios' => ' ',
|
||||
],
|
||||
], '3', 'https://shlink.io/api/error/invalid-data'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
@ -313,22 +301,6 @@ class CreateShortUrlTest extends ApiTestCase
|
||||
self::assertEquals('http://s.test/🦣🦣🦣', $payload['shortUrl']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function canCreateShortUrlsWithDeviceLongUrls(): void
|
||||
{
|
||||
[$statusCode, $payload] = $this->createShortUrl([
|
||||
'longUrl' => 'https://github.com/shlinkio/shlink/issues/1557',
|
||||
'deviceLongUrls' => [
|
||||
'ios' => 'https://github.com/shlinkio/shlink/ios',
|
||||
'android' => 'https://github.com/shlinkio/shlink/android',
|
||||
],
|
||||
]);
|
||||
|
||||
self::assertEquals(self::STATUS_OK, $statusCode);
|
||||
self::assertEquals('https://github.com/shlinkio/shlink/ios', $payload['deviceLongUrls']['ios'] ?? null);
|
||||
self::assertEquals('https://github.com/shlinkio/shlink/android', $payload['deviceLongUrls']['android'] ?? null);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function titleIsIgnoredIfLongUrlTimesOut(): void
|
||||
{
|
||||
|
@ -153,27 +153,4 @@ class EditShortUrlTest extends ApiTestCase
|
||||
];
|
||||
yield 'no domain' => [null, 'https://shlink.io/documentation/'];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function deviceLongUrlsCanBeEdited(): void
|
||||
{
|
||||
$shortCode = 'def456';
|
||||
$url = new Uri(sprintf('/short-urls/%s', $shortCode));
|
||||
$editResp = $this->callApiWithKey(self::METHOD_PATCH, (string) $url, [RequestOptions::JSON => [
|
||||
'deviceLongUrls' => [
|
||||
'android' => null, // This one will get removed
|
||||
'ios' => 'https://blog.alejandrocelaya.com/ios/edited', // This one will be edited
|
||||
'desktop' => 'https://blog.alejandrocelaya.com/desktop', // This one is new and will be created
|
||||
],
|
||||
]]);
|
||||
$deviceLongUrls = $this->getJsonResponsePayload($editResp)['deviceLongUrls'] ?? [];
|
||||
|
||||
self::assertEquals(self::STATUS_OK, $editResp->getStatusCode());
|
||||
self::assertArrayHasKey('ios', $deviceLongUrls);
|
||||
self::assertEquals('https://blog.alejandrocelaya.com/ios/edited', $deviceLongUrls['ios']);
|
||||
self::assertArrayHasKey('desktop', $deviceLongUrls);
|
||||
self::assertEquals('https://blog.alejandrocelaya.com/desktop', $deviceLongUrls['desktop']);
|
||||
self::assertArrayHasKey('android', $deviceLongUrls);
|
||||
self::assertNull($deviceLongUrls['android']);
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ use Cake\Chronos\Chronos;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\TestUtils\ApiTest\ApiTestCase;
|
||||
|
||||
use function count;
|
||||
@ -163,123 +162,109 @@ class ListShortUrlsTest extends ApiTestCase
|
||||
|
||||
public static function provideFilteredLists(): iterable
|
||||
{
|
||||
$withDeviceLongUrls = static fn (array $shortUrl, ?array $longUrls = null) => [
|
||||
...$shortUrl,
|
||||
'deviceLongUrls' => $longUrls ?? [
|
||||
DeviceType::ANDROID->value => null,
|
||||
DeviceType::IOS->value => null,
|
||||
DeviceType::DESKTOP->value => null,
|
||||
],
|
||||
];
|
||||
$shortUrlMeta = $withDeviceLongUrls(self::SHORT_URL_META, [
|
||||
DeviceType::ANDROID->value => 'https://blog.alejandrocelaya.com/android',
|
||||
DeviceType::IOS->value => 'https://blog.alejandrocelaya.com/ios',
|
||||
DeviceType::DESKTOP->value => null,
|
||||
]);
|
||||
|
||||
yield [[], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
$withDeviceLongUrls(self::SHORT_URL_DOCS),
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
self::SHORT_URL_DOCS,
|
||||
], 'valid_api_key'];
|
||||
yield [['excludePastValidUntil' => 'true'], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['excludeMaxVisitsReached' => 'true'], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_DOCS),
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_DOCS,
|
||||
], 'valid_api_key'];
|
||||
yield [['orderBy' => 'shortCode'], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_DOCS),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_DOCS,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
], 'valid_api_key'];
|
||||
yield [['orderBy' => 'shortCode-DESC'], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_DOCS),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
self::SHORT_URL_DOCS,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['orderBy' => 'title-DESC'], [
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
|
||||
$withDeviceLongUrls(self::SHORT_URL_DOCS),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_DOCS,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['startDate' => Chronos::parse('2018-12-01')->toAtomString()], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
|
||||
$shortUrlMeta,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_META,
|
||||
], 'valid_api_key'];
|
||||
yield [['endDate' => Chronos::parse('2018-12-01')->toAtomString()], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN),
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
$withDeviceLongUrls(self::SHORT_URL_DOCS),
|
||||
self::SHORT_URL_CUSTOM_SLUG_AND_DOMAIN,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
self::SHORT_URL_DOCS,
|
||||
], 'valid_api_key'];
|
||||
yield [['tags' => ['foo']], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['tags' => ['bar']], [
|
||||
$shortUrlMeta,
|
||||
self::SHORT_URL_META,
|
||||
], 'valid_api_key'];
|
||||
yield [['tags' => ['foo', 'bar']], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['tags' => ['foo', 'bar'], 'tagsMode' => 'any'], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['tags' => ['foo', 'bar'], 'tagsMode' => 'all'], [
|
||||
$shortUrlMeta,
|
||||
self::SHORT_URL_META,
|
||||
], 'valid_api_key'];
|
||||
yield [['tags' => ['foo', 'bar', 'baz']], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['tags' => ['foo', 'bar', 'baz'], 'tagsMode' => 'all'], [], 'valid_api_key'];
|
||||
yield [['tags' => ['foo'], 'endDate' => Chronos::parse('2018-12-01')->toAtomString()], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['searchTerm' => 'alejandro'], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
$shortUrlMeta,
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
self::SHORT_URL_META,
|
||||
], 'valid_api_key'];
|
||||
yield [['searchTerm' => 'cool'], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'valid_api_key'];
|
||||
yield [['searchTerm' => 'example.com'], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
], 'valid_api_key'];
|
||||
yield [[], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_SLUG),
|
||||
$shortUrlMeta,
|
||||
$withDeviceLongUrls(self::SHORT_URL_SHLINK_WITH_TITLE),
|
||||
self::SHORT_URL_CUSTOM_SLUG,
|
||||
self::SHORT_URL_META,
|
||||
self::SHORT_URL_SHLINK_WITH_TITLE,
|
||||
], 'author_api_key'];
|
||||
yield [[], [
|
||||
$withDeviceLongUrls(self::SHORT_URL_CUSTOM_DOMAIN),
|
||||
self::SHORT_URL_CUSTOM_DOMAIN,
|
||||
], 'domain_api_key'];
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Entity\RedirectCondition;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
@ -33,6 +34,12 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
|
||||
$helloQueryCondition = RedirectCondition::forQueryParam('hello', 'world');
|
||||
$manager->persist($helloQueryCondition);
|
||||
|
||||
$androidCondition = RedirectCondition::forDevice(DeviceType::ANDROID);
|
||||
$manager->persist($androidCondition);
|
||||
|
||||
$iosCondition = RedirectCondition::forDevice(DeviceType::IOS);
|
||||
$manager->persist($iosCondition);
|
||||
|
||||
$englishAndFooQueryRule = new ShortUrlRedirectRule(
|
||||
$defShortUrl,
|
||||
1,
|
||||
@ -57,6 +64,22 @@ class ShortUrlRedirectRulesFixture extends AbstractFixture implements DependentF
|
||||
);
|
||||
$manager->persist($onlyEnglishRule);
|
||||
|
||||
$androidRule = new ShortUrlRedirectRule(
|
||||
$defShortUrl,
|
||||
4,
|
||||
'https://blog.alejandrocelaya.com/android',
|
||||
new ArrayCollection([$androidCondition]),
|
||||
);
|
||||
$manager->persist($androidRule);
|
||||
|
||||
$iosRule = new ShortUrlRedirectRule(
|
||||
$defShortUrl,
|
||||
5,
|
||||
'https://blog.alejandrocelaya.com/ios',
|
||||
new ArrayCollection([$iosCondition]),
|
||||
);
|
||||
$manager->persist($iosRule);
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use ReflectionObject;
|
||||
use Shlinkio\Shlink\Core\Model\DeviceType;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Resolver\PersistenceShortUrlRelationResolver;
|
||||
@ -49,10 +48,6 @@ class ShortUrlsFixture extends AbstractFixture implements DependentFixtureInterf
|
||||
'apiKey' => $authorApiKey,
|
||||
'longUrl' =>
|
||||
'https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/',
|
||||
'deviceLongUrls' => [
|
||||
DeviceType::ANDROID->value => 'https://blog.alejandrocelaya.com/android',
|
||||
DeviceType::IOS->value => 'https://blog.alejandrocelaya.com/ios',
|
||||
],
|
||||
'tags' => ['foo', 'bar'],
|
||||
]), $relationResolver), '2019-01-01 00:00:10');
|
||||
$manager->persist($defShortUrl);
|
||||
|
Loading…
Reference in New Issue
Block a user