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; } }