feat(admin): add runtime overview and independent saved UTM link builder
deploy-preland / deploy zoomstage.online (push) Successful in 57s
deploy-preland / deploy zoomstage.online (push) Successful in 57s
This commit is contained in:
1 parent
48c71f3300
commit
2cfa2e2d2e
10 files changed
+421
-8
No files matched your search
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace PrelandAdmin;
|
||||
|
||||
/** Независимый выбор меток: читает реестр целей, пишет только приватное состояние ссылки. */
|
||||
final class LinkBuilder
|
||||
{
|
||||
private const KEYS = ['indpic', 'theme', 'degr', 'reg', 'banch', 'tp', 'cons', 'focus'];
|
||||
private const ENUMS = ['theme', 'reg', 'banch', 'focus'];
|
||||
|
||||
public function __construct(private readonly Store $store) {}
|
||||
|
||||
public function get(string $targetId): array
|
||||
{
|
||||
$this->assertId($targetId);
|
||||
return $this->store->locked('link-builder-' . $targetId, function () use ($targetId): array {
|
||||
$origin = $this->origin($this->store->target($targetId));
|
||||
$name = $this->filename($targetId);
|
||||
$saved = $this->store->read($name, null);
|
||||
if ($saved === null) return ['selection'=>null, 'url'=>null, 'saved_at'=>null];
|
||||
if (!is_array($saved) || !is_array($saved['selection'] ?? null)
|
||||
|| !is_string($saved['url'] ?? null) || !is_string($saved['saved_at'] ?? null)) {
|
||||
throw new Problem('Повреждено сохранённое состояние конструктора ссылки.', 503);
|
||||
}
|
||||
$selection = $this->selection($saved['selection']);
|
||||
$query = parse_url($saved['url'], PHP_URL_QUERY);
|
||||
if (!is_string($query)) throw new Problem('Повреждена сохранённая ссылка.', 503);
|
||||
parse_str($query, $parameters);
|
||||
if (array_keys($parameters) !== self::KEYS) throw new Problem('Повреждена сохранённая ссылка.', 503);
|
||||
foreach ($selection as $key=>$value) {
|
||||
$digit = $value === 'rnd' ? '0' : $value;
|
||||
if (!is_string($parameters[$key]) || !preg_match('/^[a-f0-9]{4}' . $digit . '[a-f0-9]{12}$/D', $parameters[$key])) {
|
||||
throw new Problem('Повреждена сохранённая ссылка.', 503);
|
||||
}
|
||||
}
|
||||
// Переименование цели меняет только origin, сохранённый шум не генерируется заново.
|
||||
$url = $origin . '/?' . http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
|
||||
if ($saved['url'] !== $url) {
|
||||
$saved['url'] = $url;
|
||||
$this->store->write($name, $saved);
|
||||
}
|
||||
return ['selection'=>$selection, 'url'=>$url, 'saved_at'=>$saved['saved_at']];
|
||||
});
|
||||
}
|
||||
|
||||
public function save(string $targetId, array $selection): array
|
||||
{
|
||||
$this->assertId($targetId);
|
||||
$selection = $this->selection($selection);
|
||||
return $this->store->locked('link-builder-' . $targetId, function () use ($targetId, $selection): array {
|
||||
$origin = $this->origin($this->store->target($targetId));
|
||||
$parameters = [];
|
||||
foreach ($selection as $key=>$value) {
|
||||
$parameters[$key] = bin2hex(random_bytes(2)) . ($value === 'rnd' ? '0' : $value) . bin2hex(random_bytes(6));
|
||||
}
|
||||
$saved = [
|
||||
'selection'=>$selection,
|
||||
'url'=>$origin . '/?' . http_build_query($parameters, '', '&', PHP_QUERY_RFC3986),
|
||||
'saved_at'=>gmdate(DATE_ATOM),
|
||||
];
|
||||
$this->store->write($this->filename($targetId), $saved);
|
||||
return $saved;
|
||||
});
|
||||
}
|
||||
|
||||
private function selection(array $input): array
|
||||
{
|
||||
if (count($input) !== count(self::KEYS) || array_diff(array_keys($input), self::KEYS)) {
|
||||
throw new Problem('Передайте ровно восемь параметров конструктора ссылки.');
|
||||
}
|
||||
$selection = [];
|
||||
foreach (self::KEYS as $key) {
|
||||
$value = $input[$key] ?? null;
|
||||
if (!is_string($value) || ($value !== 'rnd' && !preg_match('/^[1-9]$/D', $value))
|
||||
|| (in_array($key, self::ENUMS, true) && !in_array($value, ['1', '2', 'rnd'], true))) {
|
||||
throw new Problem("Недопустимое значение параметра {$key} конструктора ссылки.");
|
||||
}
|
||||
$selection[$key] = $value;
|
||||
}
|
||||
return $selection;
|
||||
}
|
||||
|
||||
private function origin(array $target): string
|
||||
{
|
||||
$domain = $target['domain'] ?? null;
|
||||
if (!is_string($domain) || strlen($domain)>253 || !preg_match('/^[a-z0-9.-]+$/iD', $domain)
|
||||
|| str_starts_with($domain, '.') || str_ends_with($domain, '.')
|
||||
|| filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) === false) {
|
||||
throw new Problem('В подключении указан небезопасный домен для HTTPS-ссылки.');
|
||||
}
|
||||
return 'https://' . strtolower($domain);
|
||||
}
|
||||
|
||||
private function assertId(string $targetId): void
|
||||
{
|
||||
if (!preg_match('/^[a-zA-Z0-9_-]{1,64}$/D', $targetId)) throw new Problem('Некорректный ID подключения.');
|
||||
}
|
||||
|
||||
private function filename(string $targetId): string
|
||||
{
|
||||
return 'link-builder-' . $targetId . '.json';
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user