fix(admin): align UTM notes and block obsolete chb publication
deploy-preland / deploy zoomstage.online (push) Successful in 54s

This commit is contained in:
Panfilov Konstantin committed 2026-10-25 23:16:35 +03:00
1 parent f2097d4800
commit 84f70b1c71
14 files changed
+93 -29

No files matched your search

+6 -1
View File
@@ -9,6 +9,10 @@ final class Config
return $value;
}
public static function encode(\stdClass $value): string { return json_encode($value, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR) . "\n"; }
/** Старый raw читается без миграции, но повторная публикация chb запрещена. */
public static function assertWritable(\stdClass $config): void {
if(($config->utm??null) instanceof \stdClass&&property_exists($config->utm,'chb'))throw new Problem('Устаревший ключ utm.chb нельзя записывать. Явно удалите его перед сохранением; восстановление такого архива запрещено.');
}
public static function patch(\stdClass $config, array $changes): \stdClass {
if (!$changes || count($changes)>500) throw new Problem('Выберите изменённые поля (не более 500).');
$copy = self::decode(self::encode($config));
@@ -31,7 +35,7 @@ final class Config
}
unset($ref);
}
return $copy;
self::assertWritable($copy);return $copy;
}
private static function adaptChange(\stdClass $config,array $change): array {
$path=$change['path']??[];if(($path[0]??null)!=='utm'||!isset($path[1]))return $change;
@@ -51,6 +55,7 @@ final class Config
$change['path']=$path;return $change;
}
public static function validate(\stdClass $config, callable $setExists,?array $catalog=null): array {
self::assertWritable($config);
if (isset($config->host)) {
$host = $config->host;
if (!is_string($host) || !preg_match('~^(?:https://)?[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?(?::[0-9]{1,5})?$~iD',$host) || str_contains($host,'..')) throw new Problem('Адрес сайта должен быть HTTPS-хостом без пути.');
+6
View File
@@ -32,6 +32,12 @@ final class Remote
ksort($hashes);return hash('sha256',json_encode($hashes,JSON_THROW_ON_ERROR));
}
public static function execute(array $p): array {
// Этот обработчик исполняется отдельно по SSH, без автозагрузки Config.
// Отказ до любых приватных каталогов, recovery, резервов и temp-файлов.
if(($p['op']??null)==='write'&&is_string($p['content']??null)){
$candidate=json_decode($p['content']);
if($candidate instanceof \stdClass&&($candidate->utm??null) instanceof \stdClass&&property_exists($candidate->utm,'chb'))return ['ok'=>false,'error'=>'Устаревший ключ utm.chb нельзя записывать. Явно удалите его перед сохранением; восстановление такого архива запрещено.'];
}
$root=$p['root'];
if (!is_string($root)||!str_starts_with($root,'/')||str_contains($root,'..')||str_contains($root,'\\')||realpath($root)!==$root||is_link($root)) return ['ok'=>false,'error'=>'Небезопасный корневой путь.'];
$private=$p['local_private']??dirname($root).'/.preland-admin-'.substr(hash('sha256',$root),0,16);
+1
View File
@@ -78,6 +78,7 @@ final class Transport
public function test(): array {return $this->call(['op'=>'test']);}
public function readConfig(): array {$r=$this->call(['op'=>'read']);return ['config'=>Config::decode($r['content']),'version'=>$r['version'],'content'=>$r['content']];}
public function writeConfig(string $version,string $content): array {
Config::assertWritable(Config::decode($content));
$before=$this->call(['op'=>'read']);if(!hash_equals($version,$before['version']))throw new Conflict();
$this->store->backup($this->target['id'],$before['content']);
return $this->call(['op'=>'write','version'=>$version,'content'=>$content]);