stripNulls($value); if ($value === null || $value === '') { return $value; } $decoded = $value; for ($i = 0; $i < 2; $i++) { $tmp = base64_decode($decoded, true); if ($tmp === false) { break; } $decoded = $this->stripNulls($tmp); } return $decoded === null ? null : $this->ensureUtf8($decoded); } /** * Подготавливает строку к сохранению в VARBINARY: CP1251 → двойной base64 → HEX 0x... */ public function set($model, string $key, $value, array $attributes) { $value = $this->stripNulls($value); if ($value === null) { return $value; // сохраняем NULL как есть } if ($value === '') { // Пустая бинарная строка для VARBINARY в MSSQL return DB::raw('0x'); } if ($this->isBase64($value)) { $inner = base64_decode($value, true); if ($inner !== false && $this->isBase64($inner)) { $encoded = $value; // Уже двойной base64 } else { $encoded = base64_encode($value); } } else { $prepared = function_exists('iconv') ? @iconv('UTF-8', 'CP1251//IGNORE', $value) : $value; if ($prepared === false) { $prepared = $value; } $encoded = base64_encode(base64_encode($prepared)); } // MSSQL VARBINARY требует явного бинарного литерала return DB::raw('0x' . bin2hex($encoded)); } /** * Удаляет завершающие нулевые байты (часто встречаются в char(n)). */ private function stripNulls(?string $value): ?string { return $value === null ? null : rtrim($value, "\0"); } /** * Лояльная проверка, что строка похожа на base64. */ private function isBase64(string $value): bool { if ($value === '') { return false; } if (preg_match('/^[A-Za-z0-9+\/\r\n]+=*$/', $value) !== 1) { return false; } return (strlen($value) % 4 === 0); } /** * Гарантирует корректный UTF-8, пытаясь конвертацию из CP1251/ISO-8859-1 * и удаляя управляющие символы. */ private function ensureUtf8(string $value): string { if ($value === '') { return ''; } if (mb_check_encoding($value, 'UTF-8')) { return $this->removeControlChars($value); } $candidates = []; if (function_exists('iconv')) { $candidates[] = fn () => @iconv('CP1251', 'UTF-8//IGNORE', $value); } if (function_exists('mb_convert_encoding')) { $candidates[] = fn () => @mb_convert_encoding($value, 'UTF-8', 'CP1251'); $candidates[] = fn () => @mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1'); } $candidates[] = fn () => utf8_encode($value); foreach ($candidates as $candidate) { $converted = $candidate(); if (is_string($converted) && mb_check_encoding($converted, 'UTF-8')) { return $this->removeControlChars($converted); } } if (function_exists('iconv')) { $sanitized = @iconv('UTF-8', 'UTF-8//IGNORE', $value); if (is_string($sanitized) && mb_check_encoding($sanitized, 'UTF-8')) { return $this->removeControlChars($sanitized); } } return ''; } /** * Удаляет управляющие ASCII-символы (0x00–0x1F, 0x7F). */ private function removeControlChars(string $value): string { $clean = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value); return is_string($clean) ? $clean : ''; } }