Fix: Decode geographic location as UTF-8 across client and server

Geographic location (city, country) from the geo APIs is UTF-8, but the client
and several server paths converted or stored it in the system ANSI code page,
turning umlauts ("ö"/"ä") into "?" or GBK mojibake ("B枚blingen"). Keep the
value UTF-8 end to end: the client caches it as UTF-8 and re-queries once after
an upgrade (keyed by __DATE__, the app's version convention), while the server
stores and displays it as UTF-8 in the online list, the activity log, the
client-info export, JSON/email payloads and search.

Also add a confirmation prompt before opening a remote chat from the toolbar,
with German, English and Traditional-Chinese translations.

Co-Authored-By: deepseek-v4-pro
This commit is contained in:
yuanyuanxianganddeepseek-v4-pro committed 2026-10-10 13:58:38 +02:00
1 parent 1a9312d2d1
commit d15cd77a28
11 files changed
+201 -45

No files matched your search

+22 -6
View File
@@ -378,13 +378,29 @@ LOGIN_INFOR GetLoginInfo(DWORD dwSpeed, CONNECT_ADDRESS& conn, const std::string
std::string pubIP = cfg.GetStr("settings", "public_ip", ""); std::string pubIP = cfg.GetStr("settings", "public_ip", "");
auto ip_time = cfg.GetInt("settings", "ip_time"); auto ip_time = cfg.GetInt("settings", "ip_time");
bool timeout = ip_time <= 0 || (time(0) - ip_time > 7 * 86400); bool timeout = ip_time <= 0 || (time(0) - ip_time > 7 * 86400);
// 升级后强制重新查询一次:旧版缓存的 location 是 GBK/CP1252,或已把 ö/ä/ß 等替换成 '?';
// 新版 GetGeoLocation 改为 UTF-8 上报(见 common/location.h),需重新拉取才能修正。
// 版本号即编译日期 __DATE__(见 common/commands.h 的 DLL_VERSION):编译期宏,内存 DLL
// 也能取到,且每次编译都会变,天然标识“升级”,无需手动递增,也不依赖 exe 版本资源。
bool cacheVerChanged = (cfg.GetStr("settings", "loc_cache_ver", "") != __DATE__);
IPConverter cvt; IPConverter cvt;
if (loc.empty() || pubIP.empty() || timeout) { if (loc.empty() || pubIP.empty() || timeout || cacheVerChanged) {
pubIP = cvt.getPublicIP(); std::string newPubIP = pubIP;
loc = cvt.GetGeoLocation(pubIP); // 升级重查 location 时复用已缓存的公网 IP;仅当 IP 缺失或 7 天过期才重新探测,
cfg.SetStr("settings", "location", loc); // 否则离线/无外网机器每次重连都会轮询 5 个公网 IP 源、空等数秒到十几秒。
cfg.SetStr("settings", "public_ip", pubIP); if (pubIP.empty() || timeout) {
cfg.SetInt("settings", "ip_time", time(0)); newPubIP = cvt.getPublicIP();
}
std::string newLoc = cvt.GetGeoLocation(newPubIP);
// 仅查询成功才覆盖缓存并推进版本号;失败保留旧值,下次登录仍会重试。
if (!newLoc.empty() && !newPubIP.empty()) {
pubIP = newPubIP;
loc = newLoc;
cfg.SetStr("settings", "location", loc);
cfg.SetStr("settings", "public_ip", pubIP);
cfg.SetInt("settings", "ip_time", time(0));
cfg.SetStr("settings", "loc_cache_ver", __DATE__);
}
} }
LoginInfor.AddReserved(loc.c_str()); LoginInfor.AddReserved(loc.c_str());
LoginInfor.AddReserved(pubIP.c_str()); LoginInfor.AddReserved(pubIP.c_str());
+4 -2
View File
@@ -328,8 +328,10 @@ private:
} }
} }
std::string city = Utf8ToAnsi(json[cityField].asString()); // 地理位置 API 返回 UTF-8;不再转 CP_ACP——那会把 ö/ä/ß 等变音符替换成 '?'。
std::string country = Utf8ToAnsi(json[countryField].asString()); // GetGeoLocation 现统一返回 UTF-8,消费方按需用 NormalizeLocationToUtf8 / SafeUtf8ToAnsi 转。
std::string city = json[cityField].asString();
std::string country = json[countryField].asString();
if (!city.empty() && !country.empty()) return city + ", " + country; if (!city.empty() && !country.empty()) return city + ", " + country;
if (!city.empty()) return city; if (!city.empty()) return city;
+152 -28
View File
@@ -261,6 +261,47 @@ UINT GetProcessListEncoding(context* ctx) {
return 936; return 936;
} }
// 判断字符串是否为合法 UTF-8(MB_ERR_INVALID_CHARS 严格校验,避免把 GBK 误判成 UTF-8)。
static bool IsValidUtf8Str(const char* s)
{
if (!s || !s[0]) return false;
return MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, s, -1, NULL, 0) > 0;
}
// 把按 cp 编码的字符串转成 UTF-8(cp == CP_UTF8 时原样返回)。
static CString LocationToUtf8(const CString& str, UINT cp)
{
if (str.IsEmpty() || cp == CP_UTF8) return str;
int wlen = MultiByteToWideChar(cp, 0, str, -1, NULL, 0);
if (wlen <= 1) return str;
std::wstring w(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, str, -1, &w[0], wlen);
int u8len = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, NULL, 0, NULL, NULL);
if (u8len <= 1) return str;
std::string u8(u8len - 1, '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), -1, &u8[0], u8len, NULL, NULL);
return CString(u8.c_str());
}
// 兜底规整地理位置为 UTF-8:合法 UTF-8(LNX/MAC 自报、迁移后的缓存)原样保留,
// 否则视为 GBK/936(Windows 自报、本地库 QQWry/Ip2Region、老缓存)转 UTF-8。
static CString NormalizeLocationToUtf8(const CString& str)
{
if (str.IsEmpty() || IsValidUtf8Str(str.GetString())) return str;
return LocationToUtf8(str, 936);
}
// 把按 cp 编码的字符串转成宽字符串(cp == CP_UTF8 用于地理位置,CP_ACP 用于 GBK/ASCII 部分)。
static CStringW AnsiToWide(const char* s, UINT cp = CP_ACP)
{
if (!s || !s[0]) return L"";
int wlen = MultiByteToWideChar(cp, 0, s, -1, NULL, 0);
if (wlen <= 1) return L"";
std::wstring w(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, s, -1, &w[0], wlen);
return CStringW(w.c_str());
}
// 授权日志频率控制:首次必须记录,状态变化必须记录,相同状态每小时记录一次 // 授权日志频率控制:首次必须记录,状态变化必须记录,相同状态每小时记录一次
static bool ShouldLogAuth(const std::string& sn, int success) { static bool ShouldLogAuth(const std::string& sn, int success) {
struct AuthLogState { struct AuthLogState {
@@ -1638,7 +1679,7 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
v[RES_CLIENT_ID] = std::to_string(id); v[RES_CLIENT_ID] = std::to_string(id);
} }
bool modify = false, needConvert = true; bool modify = false;
// 新客户端 V2 ID 算法首次上线时,把老 ID 下的元数据迁过来。 // 新客户端 V2 ID 算法首次上线时,把老 ID 下的元数据迁过来。
if (TryMigrateClientMetadata(id, strPCName, path)) { if (TryMigrateClientMetadata(id, strPCName, path)) {
@@ -1647,20 +1688,21 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
// 优先级:① 客户端刚发来的有效地理位置(非空非"?") // 优先级:① 客户端刚发来的有效地理位置(非空非"?")
// ② 服务端缓存(排除历史遗留的"?"无效值) // ② 服务端缓存(排除历史遗留的"?"无效值)
// ③ 服务端按连接 IP 自行查询 // ③ 服务端按连接 IP 自行查询
// 存储统一为 UTF-8:列表用宽字符显示、HostJson/邮件导出直接透传,
// 不再 SafeUtf8ToAnsi 转 GBK(GBK 无法编码 ö/ä/ß 等变音符,会替换成 '?')。
CString clientLoc = v[RES_CLIENT_LOC].c_str(); CString clientLoc = v[RES_CLIENT_LOC].c_str();
CString cachedLoc = m_ClientMap->GetClientMapData(id, MAP_LOCATION); CString cachedLoc = m_ClientMap->GetClientMapData(id, MAP_LOCATION);
CString loc; CString loc;
if (!clientLoc.IsEmpty() && clientLoc != "?") { if (!clientLoc.IsEmpty() && clientLoc != "?") {
loc = clientLoc; // 客户端自报:GetGeoLocation 已改为返回 UTF-8(见 common/location.h),
// 新客户端各类型都上报 UTF-8;老版本/旧缓存的客户端可能仍是 GBK/936。统一按 UTF-8 规整。
loc = NormalizeLocationToUtf8(clientLoc);
} else if (!cachedLoc.IsEmpty() && cachedLoc != "?") { } else if (!cachedLoc.IsEmpty() && cachedLoc != "?") {
loc = cachedLoc; loc = NormalizeLocationToUtf8(cachedLoc);
} else { } else {
loc = m_IPConverter->GetGeoLocation(data[ONLINELIST_IP].GetString()).c_str(); std::string geo = m_IPConverter->GetGeoLocation(data[ONLINELIST_IP].GetString());
needConvert = !m_HasLocDB; loc = NormalizeLocationToUtf8(CString(geo.c_str()));
} }
// TODO: Remove SafeUtf8ToAnsi after migrating to UTF-8
if (needConvert)
loc = SafeUtf8ToAnsi(loc.GetString()).c_str();
bool flag = strIP == "127.0.0.1" && !v[RES_CLIENT_PUBIP].empty(); bool flag = strIP == "127.0.0.1" && !v[RES_CLIENT_PUBIP].empty();
data[ONLINELIST_IP] = flag ? v[RES_CLIENT_PUBIP].c_str() : strIP; data[ONLINELIST_IP] = flag ? v[RES_CLIENT_PUBIP].c_str() : strIP;
data[ONLINELIST_LOCATION] = loc; data[ONLINELIST_LOCATION] = loc;
@@ -1734,7 +1776,13 @@ VOID CMy2015RemoteDlg::AddList(CString strIP, CString strAddr, CString strPCName
} }
std::string tip = flag ? " (" + v[RES_CLIENT_PUBIP] + ") " : ""; std::string tip = flag ? " (" + v[RES_CLIENT_PUBIP] + ") " : "";
ShowMessage(_TR("操作成功"), strIP + tip.c_str() + " " + _L(_T("主机上线")) + "[" + loc + "][" + groupName.c_str() + "]"); // 地理位置 loc 是 UTF-8,其余是 GBK/ASCII;拼成宽字符串走 ShowMessageW,
// 否则 UTF-8 会在 ANSI 日志列表里被按 GBK 显示成乱码(Böblingen → B枚blingen)。
CString type = _TR("操作成功");
CString prefix = strIP + tip.c_str() + " " + _L(_T("主机上线")) + "[";
CString suffix = "][" + CString(groupName.c_str()) + "]";
ShowMessageW(AnsiToWide(type.GetString()),
AnsiToWide(prefix.GetString(), CP_ACP) + AnsiToWide(loc.GetString(), CP_UTF8) + AnsiToWide(suffix.GetString(), CP_ACP));
CharMsg *title = new CharMsg(_TR("主机上线")); CharMsg *title = new CharMsg(_TR("主机上线"));
CharMsg *text = new CharMsg(strIP + CString(tip.c_str()) + _T(" ") + _L(_T("主机上线")) + _T(" [") + loc + _T("]")); CharMsg *text = new CharMsg(strIP + CString(tip.c_str()) + _T(" ") + _L(_T("主机上线")) + _T(" [") + loc + _T("]"));
@@ -1881,6 +1929,61 @@ VOID CMy2015RemoteDlg::ShowMessage(CString strType, CString strMsg)
m_StatusBar.SetPaneText(0,strStatusMsg); //在状态条上显示文字 m_StatusBar.SetPaneText(0,strStatusMsg); //在状态条上显示文字
} }
// 宽字符版 ShowMessage:消息里嵌入了 UTF-8 地理位置时,SetItemText 会把 UTF-8 按 GBK
// 显示成乱码(Böblingen → B枚blingen)。这里把消息列经 LVM_SETITEMTEXTW 直接写宽字符串。
// 类型/时间列与线程安全镜像仍保持 GBK,与其它条目及 McpServer get_audit_log 的 ToUtf8(msg,936) 一致。
VOID CMy2015RemoteDlg::ShowMessageW(const CStringW& strTypeW, const CStringW& strMsgW)
{
AUTO_TICK(200, "");
CTime Timer = CTime::GetCurrentTime();
CString strTime = Timer.Format("%Y-%m-%d %H:%M:%S");
int count = m_CList_Message.GetItemCount();
if (count >= MAX_MESSAGE_COUNT) {
m_CList_Message.DeleteItem(count - 1);
}
// 宽 → GBK 仅用于 InsertItem(类型列)、颜色判定与线程安全镜像;ö 等变音符会退化为 '?',
// 但 UI 消息列用的是下面的宽字符写,不受影响。
CString strType(strTypeW);
CString strMsg(strMsgW);
m_CList_Message.InsertItem(0, strType);
m_CList_Message.SetItemText(0, 1, strTime);
LVITEMW lv = {};
lv.mask = LVIF_TEXT;
lv.iItem = 0;
lv.iSubItem = 2;
lv.pszText = const_cast<LPWSTR>(strMsgW.GetString());
::SendMessageW(m_CList_Message.GetSafeHwnd(), LVM_SETITEMTEXTW, 0, (LPARAM)&lv);
m_CList_Message.SetItemData(0, (DWORD_PTR)MsgColorLevel(strType, strMsg));
// 同步线程安全镜像(GBK,与其它条目一致)
{
EnterCriticalSection(&m_cs);
LogMessage entry;
entry.type = strType.GetString();
entry.time = strTime.GetString();
entry.msg = strMsg.GetString();
m_MessageLog.insert(m_MessageLog.begin(), entry);
if ((int)m_MessageLog.size() > MAX_MESSAGE_COUNT)
m_MessageLog.pop_back();
LeaveCriticalSection(&m_cs);
}
CString strStatusMsg;
EnterCriticalSection(&m_cs);
int m_iCount = m_CList_Online.GetItemCount();
int totalCount = m_HostList.size();
LeaveCriticalSection(&m_cs);
strStatusMsg.FormatL("有%d个主机在线", m_iCount);
strStatusMsg += CString("[Total: ") + std::to_string(totalCount).c_str() + "]";
if (m_StatusBar.GetSafeHwnd())
m_StatusBar.SetPaneText(0, strStatusMsg);
}
// 试用版 IP 段触发:OnAccept 发现入站连接对端是公网 IP(已透过 Proxy Protocol v2 解出真实 IP)。 // 试用版 IP 段触发:OnAccept 发现入站连接对端是公网 IP(已透过 Proxy Protocol v2 解出真实 IP)。
// 与 OnTrialRttAbuse 共用 IOCPServer::s_TrialAbuseWarned latch,本函数每进程最多调一次。 // 与 OnTrialRttAbuse 共用 IOCPServer::s_TrialAbuseWarned latch,本函数每进程最多调一次。
LRESULT CMy2015RemoteDlg::OnTrialWanIpAbuse(WPARAM wParam, LPARAM lParam) LRESULT CMy2015RemoteDlg::OnTrialWanIpAbuse(WPARAM wParam, LPARAM lParam)
@@ -4259,6 +4362,10 @@ void CMy2015RemoteDlg::OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult)
text = ctx->GetClientData(slot); text = ctx->GetClientData(slot);
if (text.IsEmpty()) text = "?"; if (text.IsEmpty()) text = "?";
} }
// 地理位置列已统一按 UTF-8 存储(见 AddList);A 版回调只能填 CP_ACP 文本,转回 CP_ACP。
if (slot == ONLINELIST_LOCATION) {
text = Utf8ToAnsi(std::string(text.GetString())).c_str();
}
lstrcpyn(pItem->pszText, text, pItem->cchTextMax); lstrcpyn(pItem->pszText, text, pItem->cchTextMax);
} }
} }
@@ -4325,7 +4432,8 @@ void CMy2015RemoteDlg::OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult)
} }
if (wtext.empty()) wtext = L"?"; if (wtext.empty()) wtext = L"?";
} else { } else {
// 其它列:CString 仍是 ANSI(CP_ACP),按 CP_ACP 转宽供控件显示 // 其它列:CString 仍是 ANSI(CP_ACP),按 CP_ACP 转宽供控件显示;
// 地理位置列已统一按 UTF-8 存储(见 AddList),按 UTF-8 转宽,避免把 UTF-8 字节当 GBK。
CString text; CString text;
if (nCol == ONLINELIST_COMPUTER_NAME) { if (nCol == ONLINELIST_COMPUTER_NAME) {
CString note = m_ClientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE); CString note = m_ClientMap->GetClientMapData(ctx->GetClientID(), MAP_NOTE);
@@ -4334,10 +4442,11 @@ void CMy2015RemoteDlg::OnGetDispInfoW(NMHDR* pNMHDR, LRESULT* pResult)
text = ctx->GetClientData(nCol); text = ctx->GetClientData(nCol);
if (text.IsEmpty()) text = "?"; if (text.IsEmpty()) text = "?";
} }
int wlen = MultiByteToWideChar(CP_ACP, 0, text, -1, NULL, 0); UINT cp = (nCol == ONLINELIST_LOCATION) ? CP_UTF8 : CP_ACP;
int wlen = MultiByteToWideChar(cp, 0, text, -1, NULL, 0);
if (wlen > 0) { if (wlen > 0) {
wtext.resize(wlen - 1); wtext.resize(wlen - 1);
MultiByteToWideChar(CP_ACP, 0, text, -1, &wtext[0], wlen); MultiByteToWideChar(cp, 0, text, -1, &wtext[0], wlen);
} }
} }
@@ -4550,6 +4659,10 @@ void CMy2015RemoteDlg::OnOnlineMessage()
void CMy2015RemoteDlg::OnChat() void CMy2015RemoteDlg::OnChat()
{ {
// 二次确认:避免误点工具栏“远程交谈”就立刻向被控端发起双向聊天
if (IDYES != MessageBoxL(_T("确定开始远程交谈吗?"), _T("提示"), MB_ICONQUESTION | MB_YESNO))
return;
BYTE bToken = COMMAND_CHAT; //向被控端发送开始远程交谈(双向聊天) BYTE bToken = COMMAND_CHAT; //向被控端发送开始远程交谈(双向聊天)
SendSelectedCommand(&bToken, sizeof(BYTE)); SendSelectedCommand(&bToken, sizeof(BYTE));
} }
@@ -5509,7 +5622,7 @@ int CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, const
if (outExpired) *outExpired = true; // 签名有效但已过期 if (outExpired) *outExpired = true; // 签名有效但已过期
if (ctx != nullptr) { if (ctx != nullptr) {
std::string ip = ctx->GetClientData(ONLINELIST_IP); std::string ip = ctx->GetClientData(ONLINELIST_IP);
std::string location = m_IPConverter ? m_IPConverter->GetGeoLocation(ip) : ""; std::string location = m_IPConverter ? SafeUtf8ToAnsi(m_IPConverter->GetGeoLocation(ip)) : "";
std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME); std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
UpdateLicenseActivity(sn, passcode, hmacStr, ip, location, machineName); UpdateLicenseActivity(sn, passcode, hmacStr, ip, location, machineName);
} else { } else {
@@ -5527,7 +5640,7 @@ int CMy2015RemoteDlg::AuthorizeClient(context* ctx, const std::string& sn, const
// 授权成功时更新 license 活跃信息 // 授权成功时更新 license 活跃信息
if (ctx != nullptr) { if (ctx != nullptr) {
std::string ip = ctx->GetClientData(ONLINELIST_IP); std::string ip = ctx->GetClientData(ONLINELIST_IP);
std::string location = m_IPConverter ? m_IPConverter->GetGeoLocation(ip) : ""; std::string location = m_IPConverter ? SafeUtf8ToAnsi(m_IPConverter->GetGeoLocation(ip)) : "";
std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME); std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
UpdateLicenseActivity(sn, passcode, hmacStr, ip, location, machineName); UpdateLicenseActivity(sn, passcode, hmacStr, ip, location, machineName);
} else { } else {
@@ -5576,7 +5689,7 @@ int CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, con
if (outExpired) *outExpired = true; // 签名有效但已过期 if (outExpired) *outExpired = true; // 签名有效但已过期
if (ctx != nullptr) { if (ctx != nullptr) {
std::string ip = ctx->GetClientData(ONLINELIST_IP); std::string ip = ctx->GetClientData(ONLINELIST_IP);
std::string location = m_IPConverter ? m_IPConverter->GetGeoLocation(ip) : ""; std::string location = m_IPConverter ? SafeUtf8ToAnsi(m_IPConverter->GetGeoLocation(ip)) : "";
std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME); std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
UpdateLicenseActivity(sn, passcode, hmacV2, ip, location, machineName); UpdateLicenseActivity(sn, passcode, hmacV2, ip, location, machineName);
} else { } else {
@@ -5594,7 +5707,7 @@ int CMy2015RemoteDlg::AuthorizeClientV2(context* ctx, const std::string& sn, con
// 授权成功时更新 license 活跃信息 // 授权成功时更新 license 活跃信息
if (ctx != nullptr) { if (ctx != nullptr) {
std::string ip = ctx->GetClientData(ONLINELIST_IP); std::string ip = ctx->GetClientData(ONLINELIST_IP);
std::string location = m_IPConverter ? m_IPConverter->GetGeoLocation(ip) : ""; std::string location = m_IPConverter ? SafeUtf8ToAnsi(m_IPConverter->GetGeoLocation(ip)) : "";
std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME); std::string machineName = ctx->GetClientData(ONLINELIST_COMPUTER_NAME);
UpdateLicenseActivity(sn, passcode, hmacV2, ip, location, machineName); UpdateLicenseActivity(sn, passcode, hmacV2, ip, location, machineName);
} else { } else {
@@ -12660,36 +12773,47 @@ LRESULT CMy2015RemoteDlg::OnSplitterReleased(WPARAM, LPARAM)
void CMy2015RemoteDlg::OnCopyClientInfo() void CMy2015RemoteDlg::OnCopyClientInfo()
{ {
CString csv; // 用宽字符拼 CSV 后以 CF_UNICODETEXT 写入剪贴板,逐列按源编码转宽,保证无损。
// 地理位置/程序路径已统一按 UTF-8 存储(见 AddList),活动窗口编码随客户端能力位;
// 其余列仍是 CP_ACP(GBK)。此前直接 dump 原始字节,UTF-8 列会被当 GBK 造成乱码。
std::wstring csv;
EnterCriticalSection(&m_cs); EnterCriticalSection(&m_cs);
int nItem = m_CList_Online.GetNextItem(-1, LVNI_SELECTED); int nItem = m_CList_Online.GetNextItem(-1, LVNI_SELECTED);
while (nItem != -1) { while (nItem != -1) {
context* ctx = GetContextByListIndex(nItem); context* ctx = GetContextByListIndex(nItem);
if (ctx) { if (ctx) {
CString line; std::wstring line;
for (int col = 1; col < g_Column_Count_Online; ++col) { for (int col = 1; col < g_Column_Count_Online; ++col) {
if (col > 1) line += _T(","); if (col > 1) line += L",";
line += ctx->GetClientData(col - 1); int slot = col - 1;
CString text = ctx->GetClientData(slot);
UINT cp = CP_ACP;
if (slot == ONLINELIST_LOCATION || slot == ONLINELIST_PATH)
cp = CP_UTF8;
else if (slot == ONLINELIST_LOGINTIME)
cp = GetClientEncoding(ctx);
int wlen = MultiByteToWideChar(cp, 0, text, -1, NULL, 0);
if (wlen > 1) {
std::wstring w(wlen - 1, L'\0');
MultiByteToWideChar(cp, 0, text, -1, &w[0], wlen);
line += w;
}
} }
csv += line + _T("\r\n"); csv += line + L"\r\n";
} }
nItem = m_CList_Online.GetNextItem(nItem, LVNI_SELECTED); nItem = m_CList_Online.GetNextItem(nItem, LVNI_SELECTED);
} }
LeaveCriticalSection(&m_cs); LeaveCriticalSection(&m_cs);
if (csv.IsEmpty()) return; if (csv.empty()) return;
if (!OpenClipboard()) return; if (!OpenClipboard()) return;
EmptyClipboard(); EmptyClipboard();
int len = (csv.GetLength() + 1) * sizeof(TCHAR); int len = (int)((csv.size() + 1) * sizeof(wchar_t));
HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len); HGLOBAL hMem = GlobalAlloc(GMEM_MOVEABLE, len);
if (hMem) { if (hMem) {
memcpy(GlobalLock(hMem), (LPCTSTR)csv, len); memcpy(GlobalLock(hMem), csv.c_str(), len);
GlobalUnlock(hMem); GlobalUnlock(hMem);
#ifdef UNICODE
SetClipboardData(CF_UNICODETEXT, hMem); SetClipboardData(CF_UNICODETEXT, hMem);
#else
SetClipboardData(CF_TEXT, hMem);
#endif
} }
CloseClipboard(); CloseClipboard();
} }
+3
View File
@@ -223,6 +223,9 @@ public:
VOID AddList(CString strIP, CString strAddr, CString strPCName, CString strOS, CString strCPU, CString strVideo, CString strPing, VOID AddList(CString strIP, CString strAddr, CString strPCName, CString strOS, CString strCPU, CString strVideo, CString strPing,
CString ver, CString startTime, std::vector<std::string>& v, CONTEXT_OBJECT* ContextObject); CString ver, CString startTime, std::vector<std::string>& v, CONTEXT_OBJECT* ContextObject);
VOID ShowMessage(CString strType, CString strMsg); VOID ShowMessage(CString strType, CString strMsg);
// 消息中含 UTF-8(如地理位置)时使用:消息列经 LVM_SETITEMTEXTW 写宽字符串,
// 避免 UTF-8 在 ANSI 日志列表里被按 GBK 显示成乱码(Böblingen → B枚blingen)。
VOID ShowMessageW(const CStringW& strType, const CStringW& strMsg);
VOID CreatStatusBar(); VOID CreatStatusBar();
VOID CreateToolBar(); VOID CreateToolBar();
VOID CreateNotifyBar(); VOID CreateNotifyBar();
+7 -7
View File
@@ -94,10 +94,10 @@ static CString GetFieldValue(const ClientValue& val, GroupField field)
} }
} }
// 客户端自报的程序路径是 UTF-8(见 client/LoginServer.cpp 的 RES_FILE_PATH), // 程序路径与地理位置均以 UTF-8 存储(见 AddList / client 的 RES_FILE_PATH、RES_CLIENT_LOC),
// 这里用 LVM_SETITEMTEXTW 直接写宽字符串,绕开 ANSI→CP_ACP 回转, // 这里用 LVM_SETITEMTEXTW 直接写宽字符串,绕开 ANSI→CP_ACP 回转,
// 即使服务端运行在非中文 ACP(如德语)上,中文路径也能正常显示(与窗口/进程管理同法)。 // 即使服务端运行在非中文 ACP(如德语)上,中文/带变音符的文本也能正常显示(与窗口/进程管理同法)。
static void SetProgramPathText(CListCtrl& list, int iItem, int iSubItem, const char* sz) static void SetUtf8ItemText(CListCtrl& list, int iItem, int iSubItem, const char* sz)
{ {
std::wstring w; std::wstring w;
if (sz && sz[0]) { if (sz && sz[0]) {
@@ -316,12 +316,12 @@ void CClientListDlg::DisplayClients()
m_ClientList.SetItemText(nItem, COL_ID, strID); m_ClientList.SetItemText(nItem, COL_ID, strID);
m_ClientList.SetItemText(nItem, COL_NOTE, val.Note); m_ClientList.SetItemText(nItem, COL_NOTE, val.Note);
m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName)); m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName));
m_ClientList.SetItemText(nItem, COL_LOCATION, val.Location); SetUtf8ItemText(m_ClientList, nItem, COL_LOCATION, val.Location);
m_ClientList.SetItemText(nItem, COL_IP, val.IP); m_ClientList.SetItemText(nItem, COL_IP, val.IP);
m_ClientList.SetItemText(nItem, COL_OS, val.OsName); m_ClientList.SetItemText(nItem, COL_OS, val.OsName);
m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime); m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime);
m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime); m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime);
SetProgramPathText(m_ClientList, nItem, COL_PROGRAM_PATH, val.ProgramPath); SetUtf8ItemText(m_ClientList, nItem, COL_PROGRAM_PATH, val.ProgramPath);
m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel); m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel);
m_ClientList.SetItemText(nItem, COL_AUTH, strAuth); m_ClientList.SetItemText(nItem, COL_AUTH, strAuth);
m_ClientList.SetItemData(nItem, (DWORD_PTR)key); m_ClientList.SetItemData(nItem, (DWORD_PTR)key);
@@ -368,12 +368,12 @@ void CClientListDlg::DisplayClients()
m_ClientList.SetItemText(nItem, COL_ID, strID); m_ClientList.SetItemText(nItem, COL_ID, strID);
m_ClientList.SetItemText(nItem, COL_NOTE, val.Note); m_ClientList.SetItemText(nItem, COL_NOTE, val.Note);
m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName)); m_ClientList.SetItemText(nItem, COL_COMPUTER_NAME, CString(val.ComputerName));
m_ClientList.SetItemText(nItem, COL_LOCATION, val.Location); SetUtf8ItemText(m_ClientList, nItem, COL_LOCATION, val.Location);
m_ClientList.SetItemText(nItem, COL_IP, val.IP); m_ClientList.SetItemText(nItem, COL_IP, val.IP);
m_ClientList.SetItemText(nItem, COL_OS, val.OsName); m_ClientList.SetItemText(nItem, COL_OS, val.OsName);
m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime); m_ClientList.SetItemText(nItem, COL_INSTALL_TIME, val.InstallTime);
m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime); m_ClientList.SetItemText(nItem, COL_LAST_LOGIN, val.LastLoginTime);
SetProgramPathText(m_ClientList, nItem, COL_PROGRAM_PATH, val.ProgramPath); SetUtf8ItemText(m_ClientList, nItem, COL_PROGRAM_PATH, val.ProgramPath);
m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel); m_ClientList.SetItemText(nItem, COL_LEVEL, strLevel);
m_ClientList.SetItemText(nItem, COL_AUTH, strAuth); m_ClientList.SetItemText(nItem, COL_AUTH, strAuth);
m_ClientList.SetItemData(nItem, (DWORD_PTR)key); m_ClientList.SetItemData(nItem, (DWORD_PTR)key);
+2 -1
View File
@@ -69,8 +69,9 @@ Json::Value BuildHostJson(context* ctx, _ClientList* clientMap) {
CString os = ctx->GetClientData(ONLINELIST_OS); CString os = ctx->GetClientData(ONLINELIST_OS);
device["os"] = AnsiToUtf8(os); device["os"] = AnsiToUtf8(os);
// 地理位置已统一按 UTF-8 存储(见 AddList),直接透传,不再 CP_ACP→UTF-8 二次转换。
CString location = ctx->GetClientData(ONLINELIST_LOCATION); CString location = ctx->GetClientData(ONLINELIST_LOCATION);
device["location"] = AnsiToUtf8(location); device["location"] = std::string(location.GetString());
CString rtt = ctx->GetClientData(ONLINELIST_PING); CString rtt = ctx->GetClientData(ONLINELIST_PING);
device["rtt"] = AnsiToUtf8(rtt); device["rtt"] = AnsiToUtf8(rtt);
+2 -1
View File
@@ -205,7 +205,8 @@ void NotifyManager::BuildHostOnlineEmail(context* ctx, const std::string& matche
// Get host info // Get host info
std::string computerName = CT2A(ctx->GetClientData(ONLINELIST_COMPUTER_NAME), CP_UTF8); std::string computerName = CT2A(ctx->GetClientData(ONLINELIST_COMPUTER_NAME), CP_UTF8);
std::string ip = CT2A(ctx->GetClientData(ONLINELIST_IP), CP_UTF8); std::string ip = CT2A(ctx->GetClientData(ONLINELIST_IP), CP_UTF8);
std::string location = CT2A(ctx->GetClientData(ONLINELIST_LOCATION), CP_UTF8); // 地理位置已统一按 UTF-8 存储(见 AddList),直接透传,不再 CP_ACP→UTF-8 二次转换。
std::string location = ctx->GetClientData(ONLINELIST_LOCATION).GetString();
std::string os = CT2A(ctx->GetClientData(ONLINELIST_OS), CP_UTF8); std::string os = CT2A(ctx->GetClientData(ONLINELIST_OS), CP_UTF8);
std::string version = CT2A(ctx->GetClientData(ONLINELIST_VERSION), CP_UTF8); std::string version = CT2A(ctx->GetClientData(ONLINELIST_VERSION), CP_UTF8);
+6
View File
@@ -277,6 +277,12 @@ void CSearchBarDlg::DoSearch()
colText = ctx->GetClientData(col); colText = ctx->GetClientData(col);
} }
// 地理位置列已统一按 UTF-8 存储(见 AddList),而搜索输入是 CP_ACP;
// 转回 CP_ACP 再比对,否则按中文位置搜索会因编码不一致而失配。
if (col == ONLINELIST_LOCATION) {
colText = CString(Utf8ToAnsi(std::string(colText.GetString())).c_str());
}
colText.MakeLower(); colText.MakeLower();
if (colText.Find(searchText) >= 0) { if (colText.Find(searchText) >= 0) {
matched = true; matched = true;
+1
View File
@@ -306,6 +306,7 @@ Web
; ============================================ ; ============================================
发送消息=Nachricht senden 发送消息=Nachricht senden
结束交谈=Chat beenden 结束交谈=Chat beenden
确定开始远程交谈吗?=Remote-Chat starten?
锁定屏幕\n屏蔽功能键=Bildschirm sperren\nTasten blockieren 锁定屏幕\n屏蔽功能键=Bildschirm sperren\nTasten blockieren
解除锁定=Entsperren 解除锁定=Entsperren
+1
View File
@@ -309,6 +309,7 @@ Web
; ============================================ ; ============================================
发送消息=Send Msg 发送消息=Send Msg
结束交谈=End Chat 结束交谈=End Chat
确定开始远程交谈吗?=Start remote chat?
锁定屏幕\n屏蔽功能键=Lock Screen\nBlock Keys 锁定屏幕\n屏蔽功能键=Lock Screen\nBlock Keys
解除锁定=Unlock 解除锁定=Unlock
+1
View File
@@ -309,6 +309,7 @@ Web
; ============================================ ; ============================================
发送消息=傳送訊息 发送消息=傳送訊息
结束交谈=結束交談 结束交谈=結束交談
确定开始远程交谈吗?=確定開始遠程交談嗎?
锁定屏幕\n屏蔽功能键=鎖定螢幕\n屏蔽功能鍵 锁定屏幕\n屏蔽功能键=鎖定螢幕\n屏蔽功能鍵
解除锁定=解除鎖定 解除锁定=解除鎖定