8000 Create endpoint to set redirect rules for a short URL by acelaya · Pull Request #2033 · shlinkio/shlink · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Create endpoint to set redirect rules for a short URL #2033

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

Merged
Merged
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"ext-pdo": "*",
"akrabat/ip-address-middleware": "^2.1",
"cakephp/chronos": "^3.0.2",
"doctrine/dbal": "^4.0",
"doctrine/migrations": "^3.6",
"doctrine/orm": "^3.0",
"endroid/qr-code": "^5.0",
Expand Down
2 changes: 1 addition & 1 deletion config/autoload/mercure.local.php.dist
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ declare(strict_types=1);
return [

'mercure' => [
'public_hub_url' => 'http://localhost:8001',
'public_hub_url' => 'http://localhost:8002',
'internal_hub_url' => 'http://shlink_mercure_proxy',
'jwt_secret' => 'mercure_jwt_key_long_enough_to_avoid_error',
],
Expand Down
1 change: 1 addition & 0 deletions config/autoload/routes.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@

//Redirect rules
Action\RedirectRule\ListRedirectRulesAction::getRouteDef([$dropDomainMiddleware]),
Action\RedirectRule\SetRedirectRulesAction::getRouteDef([$dropDomainMiddleware]),

// Short URLs
Action\ShortUrl\CreateShortUrlAction::getRouteDef([
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ services:
container_name: shlink_mercure_proxy
image: nginx:1.25-alpine
ports:
- "8001:80"
- "8002:80"
volumes:
- ./:/home/shlink/www
- ./data/infra/mercure_proxy_vhost.conf:/etc/nginx/conf.d/default.conf
Expand Down
6 changes: 1 addition & 5 deletions docs/swagger/definitions/ShortUrlRedirectRule.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@
"type": "array",
"items": {
"type": "object",
"required": ["name", "type", "matchKey", "matchValue"],
"required": ["type", "matchKey", "matchValue"],
"properties": {
"name": {
"type": "string",
"description": "Unique condition name"
},
"type": {
"type": "string",
"enum": ["device", "language", "query"],
Expand Down
159 changes: 159 additions & 0 deletions docs/swagger/paths/v3_short-urls_{shortCode}_redirect-rules.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,164 @@
}
}
}
},

"post": {
"operationId": "setShortUrlRedirectRules",
"tags": [
"Redirect rules"
],
"summary": "Set short URL redirect rules",
"description": "Overwrites redirect rules for a short URL with the ones provided here.",
"parameters": [
{
"$ref": "../parameters/version.json"
},
{
"$ref": "../parameters/shortCode.json"
},
{
"$ref": "../parameters/domain.json"
}
],
"security": [
{
"ApiKey": []
}
],
"requestBody": {
"description": "Request body.",
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object", 9E7A
"properties": {
"redirectRules": {
"type": "array",
"items": {
"$ref": "../definitions/ShortUrlRedirectRule.json"
}
}
}
}
}
}
},
"responses": {
"200": {
"description": "The list of rules",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["defaultLongUrl", "redirectRules"],
"properties": {
"defaultLongUrl": {
"type": "string"
},
"redirectRules": {
"type": "array",
"items": {
"$ref": "../definitions/ShortUrlRedirectRule.json"
}
}
}
},
"example": {
"defaultLongUrl": "https://example.com",
"redirectRules": [
{
"longUrl": "https://example.com/android-en-us",
"priority": 1,
"conditions": [
{
"type": "device",
"matchValue": "android",
"matchKey": null
},
{
"type": "language",
"matchValue": "en-US",
"matchKey": null
}
]
},
{
"longUrl": "https://example.com/fr",
"priority": 2,
"conditions": [
{
"type": "language",
"matchValue": "fr",
"matchKey": null
}
]
},
{
"longUrl": "https://example.com/query-foo-bar-hello-world",
"priority": 3,
"conditions": [
{
"type": "query",
"matchKey": "foo",
"matchValue": "bar"
},
{
"type": "query",
"matchKey": "hello",
"matchValue": "world"
}
]
}
]
}
}
}
},
"404": {
"description": "No URL was found for provided short code.",
"content": {
"application/problem+json": {
"schema": {
"allOf": [
{
"$ref": "../definitions/Error.json"
},
{
"type": "object",
"required": ["shortCode"],
"properties": {
"shortCode": {
"type": "string",
"description": "The short code with which we tried to find the short URL"
},
"domain": {
"type": "string",
"description": "The domain with which we tried to find the short URL"
}
}
}
]
},
"examples": {
"Short URL not found": {
"$ref": "../examples/short-url-not-found-v3.json"
}
}
}
}
},
"default": {
"description": "Unexpected error.",
"content": {
"application/problem+json": {
"schema": {
"$ref": "../definitions/Error.json"
}
}
}
}
}
}
}
10 changes: 10 additions & 0 deletions module/Core/src/RedirectRule/Entity/RedirectCondition.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectConditionType;
use Shlinkio\Shlink\Core\RedirectRule\Model\Validation\RedirectRulesInputFilter;

use function Shlinkio\Shlink\Core\acceptLanguageToLocales;
use function Shlinkio\Shlink\Core\ArrayUtils\some;
Expand Down Expand Up @@ -39,6 +40,15 @@ public static function forDevice(DeviceType $device): self
return new self(RedirectConditionType::DEVICE, $device->value);
}

public static function fromRawData(array $rawData): self
{
$type = RedirectConditionType::from($rawData[RedirectRulesInputFilter::CONDITION_TYPE]);
$value = $rawData[RedirectRulesInputFilter::CONDITION_MATCH_VALUE];
$key = $rawData[RedirectRulesInputFilter::CONDITION_MATCH_KEY] ?? null;

return new self($type, $value, $key);
}

/**
* Tells if this condition matches provided request
*/
Expand Down
5 changes: 5 additions & 0 deletions module/Core/src/RedirectRule/Entity/ShortUrlRedirectRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public function matchesRequest(ServerRequestInterface $request): bool
);
}

public function clearConditions(): void
{
$this->conditions->clear();
}

public function jsonSerialize(): array
{
return [
Expand Down
34 changes: 34 additions & 0 deletions module/Core/src/RedirectRule/Model/RedirectRulesData.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\Core\RedirectRule\Model;

use Laminas\InputFilter\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\RedirectRule\Model\Validation\RedirectRulesInputFilter;

use function array_values;

readonly class RedirectRulesData
{
private function __construct(public array $rules)
{
}

public static function fromRawData(array $rawData): self
{
try {
$inputFilter = RedirectRulesInputFilter::initialize($rawData);
if (! $inputFilter->isValid()) {
throw ValidationException::fromInputFilter($inputFilter);
}

return new self(array_values($inputFilter->getValue(RedirectRulesInputFilter::REDIRECT_RULES)));
} catch (InvalidArgumentException) {
throw ValidationException::fromArray(
[RedirectRulesInputFilter::REDIRECT_RULES => RedirectRulesInputFilter::REDIRECT_RULES],
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\Core\RedirectRule\Model\Validation;

use Laminas\InputFilter\CollectionInputFilter;
use Laminas\InputFilter\InputFilter;
use Laminas\Validator\Callback;
use Laminas\Validator\InArray;
use Shlinkio\Shlink\Common\Validation\InputFactory;
use Shlinkio\Shlink\Core\Model\DeviceType;
use Shlinkio\Shlink\Core\RedirectRule\Model\RedirectConditionType;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;

use function Shlinkio\Shlink\Core\ArrayUtils\contains;
use function Shlinkio\Shlink\Core\enumValues;

class RedirectRulesInputFilter extends InputFilter
{
public const REDIRECT_RULES = 'redirectRules';

public const RULE_LONG_URL = 'longUrl';
public const RULE_CONDITIONS = 'conditions';

public const CONDITION_TYPE = 'type';
public const CONDITION_MATCH_VALUE = 'matchValue';
public const CONDITION_MATCH_KEY = 'matchKey';

private function __construct()
{
}

public static function initialize(array $rawData): self
{
$redirectRulesInputFilter = new CollectionInputFilter();
$redirectRulesInputFilter->setInputFilter(self::createRedirectRuleInputFilter());

$instance = new self();
$instance->add($redirectRulesInputFilter, self::REDIRECT_RULES);

$instance->setData($rawData);
return $instance;
}

private static function createRedirectRuleInputFilter(): InputFilter
{
$redirectRuleInputFilter = new InputFilter();

$longUrl = InputFactory::basic(self::RULE_LONG_URL, required: true);
$longUrl->getValidatorChain()->merge(ShortUrlInputFilter::longUrlValidators());
$redirectRuleInputFilter->add($longUrl);

$conditionsInputFilter = new CollectionInputFilter();
$conditionsInputFilter->setInputFilter(self::createRedirectConditionInputFilter())
->setIsRequired(true);
$redirectRuleInputFilter->add($conditionsInputFilter, self::RULE_CONDITIONS);

return $redirectRuleInputFilter;
}

private static function createRedirectConditionInputFilter(): InputFilter
{
$redirectConditionInputFilter = new InputFilter();

$type = InputFactory::basic(self::CONDITION_TYPE, required: true);
$type->getValidatorChain()->attach(new InArray([
'haystack' => enumValues(RedirectConditionType::class),
'strict' => InArray::COMPARE_STRICT,
]));
$redirectConditionInputFilter->add($type);

$value = InputFactory::basic(self::CONDITION_MATCH_VALUE, required: true);
$value->getValidatorChain()->attach(new Callback(function (string $value, array $context) {
if ($context[self::CONDITION_TYPE] === RedirectConditionType::DEVICE->value) {
return contains($value, enumValues(DeviceType::class));
}

return true;
}));
$redirectConditionInputFilter->add($value);

$redirectConditionInputFilter->add(
InputFactory::basic(self::CONDITION_MATCH_KEY, required: true)->setAllowEmpty(true),
);

return $redirectConditionInputFilter;
}
}
Loading
0