8000 Support redirect rules based on Short URL age threshold by vaslv · Pull Request #2430 · shlinkio/shlink · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Support redirect rules based on Short URL age threshold #2430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions module/CLI/src/RedirectRule/RedirectRuleHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Doctrine\Common\Collections\ArrayCollection;
use Shlinkio\Shlink\Core\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Model\AgeMatch;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\Entity\RedirectCondition;
use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule;
Expand Down Expand Up @@ -116,6 +117,10 @@ private function addRule(ShortUrl $shortUrl, StyleInterface $io, array $currentR
),
RedirectConditionType::GEOLOCATION_CITY_NAME => RedirectCondition::forGeolocationCityName(
$this->askMandatory('City name to match?', $io),
),
RedirectConditionType::AGE => RedirectCondition::forAge(
AgeMatch::from($io->choice('Age direction?', enumValues(AgeMatch::class))),
$this->askMandatory('Age threshold in seconds?', $io),
)
};

Expand Down
5 changes: 5 additions & 0 deletions module/CLI/test/RedirectRule/RedirectRuleHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\CLI\RedirectRule\RedirectRuleHandler;
use Shlinkio\Shlink\CLI\RedirectRule\RedirectRuleHandlerAction;
use Shlinkio\Shlink\Core\Model\AgeMatch;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\Entity\RedirectCondition;
use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule;
Expand Down Expand Up @@ -119,6 +120,7 @@ public function newRulesCanBeAdded(
'IP address, CIDR block or wildcard-pattern (1.2.*.*)' => '1.2.3.4',
'Country code to match?' => 'FR',
'City name to match?' => 'Los angeles',
'Age threshold in seconds?' => '86400',
default => '',
},
);
Expand All @@ -130,6 +132,8 @@ function (string $message) use (&$callIndex, $type): string {
return $type->value;
} elseif ($message === 'Device to match?') {
return DeviceType::ANDROID->value;
} elseif ($message === 'Age direction?') {
return AgeMatch::OLDER->value;
}

// First we select remove action to trigger code branch, then save to finish execution
Expand Down Expand Up @@ -175,6 +179,7 @@ public static function provideDeviceConditions(): iterable
RedirectConditionType::GEOLOCATION_CITY_NAME,
[RedirectCondition::forGeolocationCityName('Los angeles')],
];
yield 'link age older' => [RedirectConditionType::AGE, [RedirectCondition::forAge(AgeMatch::OLDER, '86400')]];
}

#[Test]
Expand Down
9 changes: 9 additions & 0 deletions module/Core/src/Model/AgeMatch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

namespace Shlinkio\Shlink\Core\Model;

enum AgeMatch: string
{
case OLDER = 'older';
case YOUNGER = 'younger';
}
32 changes: 31 additions & 1 deletion module/Core/src/RedirectRule/Entity/RedirectCondition.php
8000
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

namespace Shlinkio\Shlink\Core\RedirectRule\Entity;

use Cake\Chronos\Chronos;
use JsonSerializable;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Core\Model\AgeMatch;
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\ShortUrl\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Util\IpAddressUtils;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkRedirectCondition;

Expand Down Expand Up @@ -64,6 +67,11 @@ public static function forGeolocationCityName(string $cityName): self
return new self(RedirectConditionType::GEOLOCATION_CITY_NAME, $cityName);
}

public static function forAge(AgeMatch $ageMatch, string $ageSeconds): self
{
return new self(RedirectConditionType::AGE, $ageSeconds, $ageMatch->value);
}

public static function fromRawData(array $rawData): self
{
$type = RedirectConditionType::from($rawData[RedirectRulesInputFilter::CONDITION_TYPE]);
Expand All @@ -87,13 +95,14 @@ public static function fromImport(ImportedShlinkRedirectCondition $cond): self|n
RedirectConditionType::IP_ADDRESS => self::forIpAddress($cond->matchValue),
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => self::forGeolocationCountryCode($cond->matchValue),
RedirectConditionType::GEOLOCATION_CITY_NAME => self::forGeolocationCityName($cond->matchValue),
RedirectConditionType::AGE => self::forAge(AgeMatch::from($cond->matchKey), $cond->matchValue),
};
}

/**
* Tells if this condition matches provided request
*/
public function matchesRequest(ServerRequestInterface $request): bool
public function matchesRequest(ServerRequestInterface $request, ShortUrl|null $shortUrl = null): bool
{
return match ($this->type) {
RedirectConditionType::QUERY_PARAM => $this->matchesQueryParam($request),
Expand All @@ -102,6 +111,7 @@ public function matchesRequest(ServerRequestInterface $request): bool
RedirectConditionType::IP_ADDRESS => $this->matchesRemoteIpAddress($request),
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => $this->matchesGeolocationCountryCode($request),
RedirectConditionType::GEOLOCATION_CITY_NAME => $this->matchesGeolocationCityName($request),
RedirectConditionType::AGE => $this->matchesAge($shortUrl),
};
}

Expand Down Expand Up @@ -169,6 +179,21 @@ private function matchesGeolocationCityName(ServerRequestInterface $request): bo
return strcasecmp($geolocation->city, $this->matchValue) === 0;
}

private function matchesAge(ShortUrl $shortUrl): bool
{
$ageSeconds = Chronos::now()->diffInSeconds($shortUrl->dateCreated());

if ($this->matchKey === AgeMatch::OLDER->value && $ageSeconds > $this->matchValue){
return true;
}

if ($this->matchKey === AgeMatch::YOUNGER->value && $ageSeconds < $this->matchValue){
return true;
}

return false;
}

public function jsonSerialize(): array
{
return [
Expand All @@ -191,6 +216,11 @@ public function toHumanFriendly(): string
RedirectConditionType::IP_ADDRESS => sprintf('IP address matches %s', $this->matchValue),
RedirectConditionType::GEOLOCATION_COUNTRY_CODE => sprintf('country code is %s', $this->matchValue),
RedirectConditionType::GEOLOCATION_CITY_NAME => sprintf('city name is %s', $this->matchValue),
RedirectConditionType::AGE => sprintf(
'link age %s %s seconds',
$this->matchKey,
$this->matchValue,
),
};
}
}
4 changes: 2 additions & 2 deletions module/Core/src/RedirectRule/Entity/ShortUrlRedirectRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ public function withPriority(int $newPriority): self
/**
* Tells if this condition matches provided request
*/
public function matchesRequest(ServerRequestInterface $request): bool
public function matchesRequest(ServerRequestInterface $request, ShortUrl|null $shortUrl = null): bool
{
return $this->conditions->count() > 0 && every(
$this->conditions,
static fn (RedirectCondition $condition) => $condition->matchesRequest($request),
static fn (RedirectCondition $condition) => $condition->matchesRequest($request, $shortUrl),
);
}

Expand Down
2 changes: 2 additions & 0 deletions module/Core/src/RedirectRule/Model/RedirectConditionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ enum RedirectConditionType: string
case IP_ADDRESS = 'ip-address';
case GEOLOCATION_COUNTRY_CODE = 'geolocation-country-code';
case GEOLOCATION_CITY_NAME = 'geolocation-city-name';
case AGE = 'age';

/**
* Tells if a value is valid for the condition type
Expand Down Expand Up @@ -45,6 +46,7 @@ public function isValid(string $value): bool
'TO', 'TT', 'TN', 'TR', 'TM', 'TC', 'TV', 'UG', 'UA', 'AE', 'GB', 'US', 'UM', 'UY', 'UZ', 'VU',
'VE', 'VN', 'VG', 'VI', 'WF', 'EH', 'YE', 'ZM', 'ZW',
]),
RedirectConditionType::AGE => (int)$value > 0,
default => true,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function resolveLongUrl(ShortUrl $shortUrl, ServerRequestInterface $reque
$rules = $this->ruleService->rulesForShortUrl($shortUrl);
foreach ($rules as $rule) {
// Return the long URL for the first rule found that matches
if ($rule->matchesRequest($request)) {
if ($rule->matchesRequest($request, $shortUrl)) {
return $rule->longUrl;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Model\AgeMatch;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\Entity\RedirectCondition;
use Shlinkio\Shlink\Core\RedirectRule\Entity\ShortUrlRedirectRule;
Expand Down Expand Up @@ -63,6 +64,7 @@ public function conditionsCanBeMapped(callable $callback, array $expectedResult)
RedirectCondition::forQueryParam('foo', 'bar'),
RedirectCondition::forDevice(DeviceType::ANDROID),
RedirectCondition::forIpAddress('1.2.3.*'),
RedirectCondition::forAge(AgeMatch::YOUNGER, '3600'),
]);
$rule = $this->createRule($conditions);

Expand Down Expand Up @@ -94,12 +96,18 @@ public static function provideConditionMappingCallbacks(): iterable
'matchKey' => null,
'matchValue' => '1.2.3.*',
],
[
'type' => RedirectConditionType::AGE->value,
'matchKey' => AgeMatch::YOUNGER->value,
'matchValue' => '3600',
],
]];
yield 'human-friendly conditions' => [fn (RedirectCondition $cond) => $cond->toHumanFriendly(), [
'en-UK language is accepted',
'query string contains foo=bar',
sprintf('device is %s', DeviceType::ANDROID->value),
'IP address matches 1.2.3.*',
sprintf('link age %s 3600 seconds', AgeMatch::YOUNGER->value),
]];
}

Expand Down
0