feat(admin): compact UTM percentages and shared notes
deploy-preland / deploy zoomstage.online (push) Successful in 1m1s

This commit is contained in:
Panfilov Konstantin committed 2026-10-25 22:59:37 +03:00
1 parent 4fc4fd8162
commit f2097d4800
10 files changed
+240 -16

No files matched your search

+46
View File
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace PrelandAdmin;
final class Notes
{
private const MARKERS=['indpic','theme','degr','reg','banch','tp','cons','focus'];
public function __construct(private readonly Store $store) {}
public function all(): array {
return $this->store->locked('utm-notes',fn()=>$this->load());
}
public function save(array $input): array {
if(count($input)!==3||array_diff(array_keys($input),['marker','text','version'])||!is_string($input['marker']??null)||!in_array($input['marker'],self::MARKERS,true)||!is_string($input['text']??null)||!is_string($input['version']??null))throw new Problem('Передайте маркер UTM, текст и версию заметки.');
if(!self::validText($input['text']))throw new Problem('Заметка должна содержать корректный UTF-8, не более 10000 символов, без управляющих символов кроме перевода строки и табуляции.');
return $this->store->locked('utm-notes',function() use($input){
$notes=$this->load();$marker=$input['marker'];
if(!hash_equals($notes[$marker]['version'],$input['version']))throw new Problem('Заметка уже изменена. Загрузите актуальную версию перед сохранением.',409);
$note=['text'=>$input['text'],'version'=>bin2hex(random_bytes(32)),'saved_at'=>gmdate(DATE_ATOM)];
$notes[$marker]=$note;$this->store->write('utm-notes.json',$notes);return $note;
});
}
private function load(): array {
$path=$this->store->path('utm-notes.json');clearstatcache(true,$path);
if(is_link($path)||file_exists($path)&&(!is_file($path)||filesize($path)>1048576))throw new Problem('Повреждено приватное состояние заметок.',503);
if(!file_exists($path)){
$notes=[];foreach(self::MARKERS as $marker)$notes[$marker]=['text'=>'','version'=>hash('sha256','utm-notes:'.$marker.':empty'),'saved_at'=>null];return $notes;
}
$notes=$this->store->read('utm-notes.json',null);
if(!is_array($notes)||count($notes)!==8||array_diff(self::MARKERS,array_keys($notes)))throw new Problem('Повреждено приватное состояние заметок.',503);
$ordered=[];
foreach(self::MARKERS as $marker){
$note=$notes[$marker];
if(!is_array($note)||count($note)!==3||array_diff(array_keys($note),['text','version','saved_at'])||!is_string($note['text']??null)||!self::validText($note['text'])||!is_string($note['version']??null)||!preg_match('/^[a-f0-9]{64}$/D',$note['version'])||!array_key_exists('saved_at',$note)||($note['saved_at']!==null&&(!is_string($note['saved_at'])||!preg_match('/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+00:00$/D',$note['saved_at']))))throw new Problem('Повреждено приватное состояние заметок.',503);
if($note['saved_at']===null&&($note['text']!==''||$note['version']!==hash('sha256','utm-notes:'.$marker.':empty')))throw new Problem('Повреждено приватное состояние заметок.',503);
$ordered[$marker]=$note;
}
return $ordered;
}
private static function validText(string $text): bool {
return strlen($text)<=40000&&preg_match('//u',$text)===1&&preg_match('/[\x{0000}-\x{0008}\x{000B}\x{000C}\x{000E}-\x{001F}\x{007F}-\x{009F}]/u',$text)===0&&preg_match_all('/./us',$text)<=10000;
}
}