From 2f39aff2fe84eaaaa317b5078ce4a4e0d3fca1a5 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Sun, 22 Dec 2024 12:42:06 +0100 Subject: [PATCH] Implement logic to import redirect rules from other Shlink instances --- composer.json | 28 ++++++------- module/Core/config/dependencies.config.php | 1 + .../src/Importer/ImportedLinksProcessor.php | 3 ++ .../Core/src/Importer/ShortUrlImporting.php | 42 ++++++++++++++++++- .../RedirectRule/Entity/RedirectCondition.php | 18 ++++++++ .../ShortUrlRedirectRuleService.php | 8 ++-- .../ShortUrlRedirectRuleServiceInterface.php | 2 + module/Core/src/ShortUrl/Entity/ShortUrl.php | 1 - .../Importer/ImportedLinksProcessorTest.php | 6 ++- 9 files changed, 87 insertions(+), 22 deletions(-) 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);