diff --git a/composer.json b/composer.json
index ca6cab07..15ba3b70 100644
--- a/composer.json
+++ b/composer.json
@@ -26,15 +26,15 @@
"donatj/phpuseragentparser": "^1.10",
"endroid/qr-code": "^6.0",
"friendsofphp/proxy-manager-lts": "^1.0",
- "geoip2/geoip2": "^3.0",
+ "geoip2/geoip2": "^3.1",
"guzzlehttp/guzzle": "^7.9",
"hidehalo/nanoid-php": "^2.0",
"jaybizzle/crawler-detect": "^1.3",
- "laminas/laminas-config-aggregator": "^1.15",
+ "laminas/laminas-config-aggregator": "^1.17",
"laminas/laminas-diactoros": "^3.5",
- "laminas/laminas-inputfilter": "^2.30",
- "laminas/laminas-servicemanager": "^3.22",
- "laminas/laminas-stdlib": "^3.19",
+ "laminas/laminas-inputfilter": "^2.31",
+ "laminas/laminas-servicemanager": "^3.23",
+ "laminas/laminas-stdlib": "^3.20",
"matomo/matomo-php-tracker": "^3.3",
"mezzio/mezzio": "^3.20",
"mezzio/mezzio-fastroute": "^3.12",
@@ -46,7 +46,7 @@
"shlinkio/shlink-common": "^6.6",
"shlinkio/shlink-config": "^3.4",
"shlinkio/shlink-event-dispatcher": "^4.1",
- "shlinkio/shlink-importer": "^5.3.2",
+ "shlinkio/shlink-importer": "dev-main#6c305ee as 5.5",
"shlinkio/shlink-installer": "dev-develop#3675f6d as 9.4",
"shlinkio/shlink-ip-geolocation": "^4.2",
"shlinkio/shlink-json": "^1.1",
@@ -54,14 +54,14 @@
"spiral/roadrunner-cli": "^2.6",
"spiral/roadrunner-http": "^3.5",
"spiral/roadrunner-jobs": "^4.5",
- "symfony/console": "^7.1",
- "symfony/filesystem": "^7.1",
- "symfony/lock": "^7.1",
- "symfony/process": "^7.1",
- "symfony/string": "^7.1"
+ "symfony/console": "^7.2",
+ "symfony/filesystem": "^7.2",
+ "symfony/lock": "^7.2",
+ "symfony/process": "^7.2",
+ "symfony/string": "^7.2"
},
"require-dev": {
- "devizzent/cebe-php-openapi": "^1.1.1",
+ "devizzent/cebe-php-openapi": "^1.1.2",
"devster/ubench": "^2.1",
"phpstan/phpstan": "^2.0",
"phpstan/phpstan-doctrine": "^2.0",
@@ -69,11 +69,11 @@
"phpstan/phpstan-symfony": "^2.0",
"phpunit/php-code-coverage": "^11.0",
"phpunit/phpcov": "^10.0",
- "phpunit/phpunit": "^11.4",
+ "phpunit/phpunit": "^11.5",
"roave/security-advisories": "dev-master",
"shlinkio/php-coding-standard": "~2.4.0",
"shlinkio/shlink-test-utils": "^4.2",
- "symfony/var-dumper": "^7.1",
+ "symfony/var-dumper": "^7.2",
"veewee/composer-run-parallel": "^1.4"
},
"conflict": {
diff --git a/module/Core/config/dependencies.config.php b/module/Core/config/dependencies.config.php
index adc9ae2a..eda556e9 100644
--- a/module/Core/config/dependencies.config.php
+++ b/module/Core/config/dependencies.config.php
@@ -262,6 +262,7 @@ return [
ShortUrl\Resolver\PersistenceShortUrlRelationResolver::class,
ShortUrl\Helper\ShortCodeUniquenessHelper::class,
Util\DoctrineBatchHelper::class,
+ RedirectRule\ShortUrlRedirectRuleService::class,
],
Crawling\CrawlingHelper::class => [ShortUrl\Repository\CrawlableShortCodesQuery::class],
diff --git a/module/Core/src/Importer/ImportedLinksProcessor.php b/module/Core/src/Importer/ImportedLinksProcessor.php
index e8434d4f..af4ce917 100644
--- a/module/Core/src/Importer/ImportedLinksProcessor.php
+++ b/module/Core/src/Importer/ImportedLinksProcessor.php
@@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\Core\Importer;
use Doctrine\ORM\EntityManagerInterface;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
+use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortCodeUniquenessHelperInterface;
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository;
@@ -32,6 +33,7 @@ readonly class ImportedLinksProcessor implements ImportedLinksProcessorInterface
private ShortUrlRelationResolverInterface $relationResolver,
private ShortCodeUniquenessHelperInterface $shortCodeHelper,
private DoctrineBatchHelperInterface $batchHelper,
+ private ShortUrlRedirectRuleServiceInterface $redirectRuleService,
) {
}
@@ -80,6 +82,7 @@ readonly class ImportedLinksProcessor implements ImportedLinksProcessorInterface
continue;
}
+ $shortUrlImporting->importRedirectRules($importedUrl->redirectRules, $this->em, $this->redirectRuleService);
$resultMessage = $shortUrlImporting->importVisits(
$this->batchHelper->wrapIterable($importedUrl->visits, 100),
$this->em,
diff --git a/module/Core/src/Importer/ShortUrlImporting.php b/module/Core/src/Importer/ShortUrlImporting.php
index ad812e8c..cc534fc2 100644
--- a/module/Core/src/Importer/ShortUrlImporting.php
+++ b/module/Core/src/Importer/ShortUrlImporting.php
@@ -4,11 +4,17 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Importer;
+use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
+use Shlinkio\Shlink\Core\RedirectRule\Entity\RedirectCondition;
+use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule;
+use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
+use Shlinkio\Shlink\Importer\Model\ImportedShlinkRedirectRule;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkVisit;
+use function Shlinkio\Shlink\Core\ArrayUtils\map;
use function Shlinkio\Shlink\Core\normalizeDate;
use function sprintf;
@@ -20,12 +26,12 @@ final readonly class ShortUrlImporting
public static function fromExistingShortUrl(ShortUrl $shortUrl): self
{
- return new self($shortUrl, false);
+ return new self($shortUrl, isNew: false);
}
public static function fromNewShortUrl(ShortUrl $shortUrl): self
{
- return new self($shortUrl, true);
+ return new self($shortUrl, isNew: true);
}
/**
@@ -55,6 +61,38 @@ final readonly class ShortUrlImporting
: sprintf('Skipped. Imported %s visits', $importedVisits);
}
+ /**
+ * @param ImportedShlinkRedirectRule[] $rules
+ */
+ public function importRedirectRules(
+ array $rules,
+ EntityManagerInterface $em,
+ ShortUrlRedirectRuleServiceInterface $redirectRuleService,
+ ): void {
+ $shortUrl = $this->resolveShortUrl($em);
+ $redirectRules = map(
+ $rules,
+ function (ImportedShlinkRedirectRule $rule, int|string|float $index) use ($shortUrl): ShortUrlRedirectRule {
+ $conditions = new ArrayCollection();
+ foreach ($rule->conditions as $cond) {
+ $redirectCondition = RedirectCondition::fromImport($cond);
+ if ($redirectCondition !== null) {
+ $conditions->add($redirectCondition);
+ }
+ }
+
+ return new ShortUrlRedirectRule(
+ shortUrl: $shortUrl,
+ priority: ((int) $index) + 1,
+ longUrl:$rule->longUrl,
+ conditions: $conditions,
+ );
+ },
+ );
+
+ $redirectRuleService->saveRulesForShortUrl($shortUrl, $redirectRules);
+ }
+
private function resolveShortUrl(EntityManagerInterface $em): ShortUrl
{
// If wrapped ShortUrl has no ID, avoid trying to query the EM, as it would fail in Postgres.
diff --git a/module/Core/src/RedirectRule/Entity/RedirectCondition.php b/module/Core/src/RedirectRule/Entity/RedirectCondition.php
index cf1e134b..602d07b7 100644
--- a/module/Core/src/RedirectRule/Entity/RedirectCondition.php
+++ b/module/Core/src/RedirectRule/Entity/RedirectCondition.php
@@ -9,6 +9,7 @@ use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectConditionType;
use Shlinkio\Shlink\Core\RedirectRule\Model\Validation\RedirectRulesInputFilter;
use Shlinkio\Shlink\Core\Util\IpAddressUtils;
+use Shlinkio\Shlink\Importer\Model\ImportedShlinkRedirectCondition;
use function Shlinkio\Shlink\Core\acceptLanguageToLocales;
use function Shlinkio\Shlink\Core\ArrayUtils\some;
@@ -72,6 +73,23 @@ class RedirectCondition extends AbstractEntity implements JsonSerializable
return new self($type, $value, $key);
}
+ public static function fromImport(ImportedShlinkRedirectCondition $cond): self|null
+ {
+ $type = RedirectConditionType::tryFrom($cond->type);
+ if ($type === null) {
+ return null;
+ }
+
+ return match ($type) {
+ RedirectConditionType::QUERY_PARAM => self::forQueryParam($cond->matchKey ?? '', $cond->matchValue),
+ RedirectConditionType::LANGUAGE => self::forLanguage($cond->matchValue),
+ RedirectConditionType::DEVICE => self::forDevice(DeviceType::from($cond->matchValue)),
+ RedirectConditionType::IP_ADDRESS => self::forIpAddress($cond->matchValue),
+ RedirectConditionType::GEOLOCATION_COUNTRY_CODE => self::forGeolocationCountryCode($cond->matchValue),
+ RedirectConditionType::GEOLOCATION_CITY_NAME => self::forGeolocationCityName($cond->matchValue),
+ };
+ }
+
/**
* Tells if this condition matches provided request
*/
diff --git a/module/Core/src/RedirectRule/ShortUrlRedirectRuleService.php b/module/Core/src/RedirectRule/ShortUrlRedirectRuleService.php
index 01ba0a8f..dac0dc61 100644
--- a/module/Core/src/RedirectRule/ShortUrlRedirectRuleService.php
+++ b/module/Core/src/RedirectRule/ShortUrlRedirectRuleService.php
@@ -20,7 +20,7 @@ readonly class ShortUrlRedirectRuleService implements ShortUrlRedirectRuleServic
}
/**
- * @return ShortUrlRedirectRule[]
+ * @inheritDoc
*/
public function rulesForShortUrl(ShortUrl $shortUrl): array
{
@@ -31,7 +31,7 @@ readonly class ShortUrlRedirectRuleService implements ShortUrlRedirectRuleServic
}
/**
- * @return ShortUrlRedirectRule[]
+ * @inheritDoc
*/
public function setRulesForShortUrl(ShortUrl $shortUrl, RedirectRulesData $data): array
{
@@ -55,7 +55,7 @@ readonly class ShortUrlRedirectRuleService implements ShortUrlRedirectRuleServic
}
/**
- * @param ShortUrlRedirectRule[] $rules
+ * @inheritDoc
*/
public function saveRulesForShortUrl(ShortUrl $shortUrl, array $rules): void
{
@@ -74,7 +74,7 @@ readonly class ShortUrlRedirectRuleService implements ShortUrlRedirectRuleServic
/**
* @param ShortUrlRedirectRule[] $rules
*/
- public function doSetRulesForShortUrl(ShortUrl $shortUrl, array $rules): void
+ private function doSetRulesForShortUrl(ShortUrl $shortUrl, array $rules): void
{
$this->em->wrapInTransaction(function () use ($shortUrl, $rules): void {
// First, delete existing rules for the short URL
diff --git a/module/Core/src/RedirectRule/ShortUrlRedirectRuleServiceInterface.php b/module/Core/src/RedirectRule/ShortUrlRedirectRuleServiceInterface.php
index 186be87e..e05c6ab8 100644
--- a/module/Core/src/RedirectRule/ShortUrlRedirectRuleServiceInterface.php
+++ b/module/Core/src/RedirectRule/ShortUrlRedirectRuleServiceInterface.php
@@ -14,11 +14,13 @@ interface ShortUrlRedirectRuleServiceInterface
public function rulesForShortUrl(ShortUrl $shortUrl): array;
/**
+ * Resolve a set of redirect rules and attach them to a short URL, replacing any already existing rules.
* @return ShortUrlRedirectRule[]
*/
public function setRulesForShortUrl(ShortUrl $shortUrl, RedirectRulesData $data): array;
/**
+ * Save provided set of rules for a short URL, replacing any already existing rules.
* @param ShortUrlRedirectRule[] $rules
*/
public function saveRulesForShortUrl(ShortUrl $shortUrl, array $rules): void;
diff --git a/module/Core/src/ShortUrl/Entity/ShortUrl.php b/module/Core/src/ShortUrl/Entity/ShortUrl.php
index b7fb6c56..086a47bb 100644
--- a/module/Core/src/ShortUrl/Entity/ShortUrl.php
+++ b/module/Core/src/ShortUrl/Entity/ShortUrl.php
@@ -76,7 +76,6 @@ class ShortUrl extends AbstractEntity
/**
* @param non-empty-string $longUrl
- * @internal
*/
public static function withLongUrl(string $longUrl): self
{
diff --git a/module/Core/test/Importer/ImportedLinksProcessorTest.php b/module/Core/test/Importer/ImportedLinksProcessorTest.php
index 36265aa3..8fb63e68 100644
--- a/module/Core/test/Importer/ImportedLinksProcessorTest.php
+++ b/module/Core/test/Importer/ImportedLinksProcessorTest.php
@@ -14,6 +14,7 @@ use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Shlinkio\Shlink\Core\Importer\ImportedLinksProcessor;
+use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleServiceInterface;
use Shlinkio\Shlink\Core\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortCodeUniquenessHelperInterface;
use Shlinkio\Shlink\Core\ShortUrl\Repository\ShortUrlRepository;
@@ -44,13 +45,15 @@ class ImportedLinksProcessorTest extends TestCase
private MockObject & ShortCodeUniquenessHelperInterface $shortCodeHelper;
private MockObject & ShortUrlRepository $repo;
private MockObject & StyleInterface $io;
+ private MockObject & ShortUrlRedirectRuleServiceInterface $redirectRuleService;
protected function setUp(): void
{
$this->em = $this->createMock(EntityManagerInterface::class);
$this->repo = $this->createMock(ShortUrlRepository::class);
-
$this->shortCodeHelper = $this->createMock(ShortCodeUniquenessHelperInterface::class);
+ $this->redirectRuleService = $this->createMock(ShortUrlRedirectRuleServiceInterface::class);
+
$batchHelper = $this->createMock(DoctrineBatchHelperInterface::class);
$batchHelper->method('wrapIterable')->willReturnArgument(0);
@@ -59,6 +62,7 @@ class ImportedLinksProcessorTest extends TestCase
new SimpleShortUrlRelationResolver(),
$this->shortCodeHelper,
$batchHelper,
+ $this->redirectRuleService,
);
$this->io = $this->createMock(StyleInterface::class);