fix(ui): validate and clamp dialog input

Tighten input handling across several dialogs so bad/edge input can't
produce a doomed request or a confusing display:

- app_security: passphrase strength meter now factors character-class
  diversity — an all-one-class string downgrades one tier so "aaaaaaaa"
  no longer scores as high as a mixed one.
- block_info: clamp the height field to the chain tip and hide/deny
  "Next" at the tip (was only yielding a raw RPC error).
- address_label: trim surrounding whitespace before saving; a
  whitespace-only label clears it (mirrors clearing the icon).
- send_tab: re-clamp the amount when a Max send has its fee bumped in the
  confirm popup (kept the total within budget) and NUL-terminate the
  strncpy'd address/memo when a payment URI overwrites the form.
- settings_page: reject an empty/whitespace-only lite wallet path before
  dispatching an unusable open/restore request.
- peers_tab: guard ExtractIP against an empty address.
- transaction_details: disable "View in explorer" when the configured
  explorer URL is empty/whitespace so we never open a garbage link.
- app: seed-backup "Skip" now requires a second, deliberate click with a
  fund-loss warning before it dismisses.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 15:08:59 -05:00
parent 41f5548593
commit 658c0f355b
8 changed files with 118 additions and 14 deletions

View File

@@ -221,7 +221,19 @@ public:
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(saveLabel, ImVec2(saveW, 0), buttonFont)) {
// Apply changes
s_app->setAddressLabel(s_address, s_label);
// Trim surrounding whitespace; a whitespace-only label clears it
// (mirroring how an empty icon selection clears the icon).
std::string trimmedLabel(s_label);
{
std::size_t b = trimmedLabel.find_first_not_of(" \t\r\n");
if (b == std::string::npos) {
trimmedLabel.clear();
} else {
std::size_t e = trimmedLabel.find_last_not_of(" \t\r\n");
trimmedLabel = trimmedLabel.substr(b, e - b + 1);
}
}
s_app->setAddressLabel(s_address, trimmedLabel);
if (s_selectedIcon >= 0)
s_app->setAddressIcon(s_address, material::project_icons::walletIconName(s_selectedIcon));
else

View File

@@ -115,6 +115,11 @@ void BlockInfoDialog::render(App* app)
ImGui::SetNextItemWidth(heightInput.width);
ImGui::InputInt("##Height", &s_height);
if (s_height < 1) s_height = 1;
// Clamp to the chain tip so navigation/typing can't request a height
// above the tip (which would only yield a raw RPC error).
if (state.sync.blocks > 0 && s_height > state.sync.blocks) {
s_height = state.sync.blocks;
}
ImGui::SameLine();
@@ -287,7 +292,8 @@ void BlockInfoDialog::render(App* app)
if (ImGui::IsItemHovered()) {
material::Tooltip("%s", TR("block_click_next"));
}
if (ImGui::IsItemClicked()) {
if (ImGui::IsItemClicked() &&
(state.sync.blocks <= 0 || s_height < state.sync.blocks)) {
s_height++;
s_has_data = false;
}
@@ -308,7 +314,10 @@ void BlockInfoDialog::render(App* app)
ImGui::SameLine();
}
if (!s_next_hash.empty()) {
// Only offer "Next" when below the chain tip (the tip block has no
// nextblockhash, so this stays hidden there).
if (!s_next_hash.empty() &&
(state.sync.blocks <= 0 || s_height < state.sync.blocks)) {
if (material::StyledButton(TR("block_nav_next"), ImVec2(0,0), S.resolveFont(closeBtn.font))) {
s_height++;
s_has_data = false;

View File

@@ -37,6 +37,7 @@ static int s_selected_banned_idx = -1;
// Helper: Extract IP without port
static std::string ExtractIP(const std::string& addr)
{
if (addr.empty()) return addr;
std::string ip = addr;
if (ip[0] == '[') {
auto pos = ip.rfind("]:");

View File

@@ -725,7 +725,17 @@ void RenderSendConfirmPopup(App* app) {
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_network_fee"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
double feeBefore = s_fee;
RenderFeeTierSelector("##confirm");
// If Max was selected, a fee bump here would push total over the available
// balance (s_amount was clamped to available - old fee). Re-clamp so the send
// stays within budget.
if (s_send_max && s_fee != feeBefore) {
double avail = GetAvailableBalance(app);
double maxAmount = avail - s_fee;
if (maxAmount < 0) maxAmount = 0;
s_amount = maxAmount;
}
// Recalculate total after potential fee change
total = s_amount + s_fee;
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
@@ -1129,9 +1139,16 @@ void RenderSendTab(App* app)
// Handle pending payment from URI
if (app->hasPendingPayment()) {
// A URI arriving mid-compose would silently overwrite the user's in-progress form.
// Capture an undo snapshot first so "Undo" can restore what they were typing.
if (FormHasData()) {
SaveFormSnapshot();
}
strncpy(s_to_address, app->getPendingToAddress().c_str(), sizeof(s_to_address) - 1);
s_to_address[sizeof(s_to_address) - 1] = '\0';
s_amount = app->getPendingAmount();
strncpy(s_memo, app->getPendingMemo().c_str(), sizeof(s_memo) - 1);
s_memo[sizeof(s_memo) - 1] = '\0';
app->clearPendingPayment();
}

View File

@@ -179,10 +179,15 @@ void TransactionDetailsDialog::render(App* app)
float start_x = (ImGui::GetWindowWidth() - total_width) / 2.0f;
ImGui::SetCursorPosX(start_x);
// Guard against an empty/whitespace explorer URL so we never open a garbage link.
std::string explorerBase = app->settings()->getTxExplorerUrl();
bool explorerValid = explorerBase.find_first_not_of(" \t\r\n") != std::string::npos;
if (!explorerValid) ImGui::BeginDisabled();
if (material::StyledButton(TR("tx_view_explorer"), ImVec2(button_width, 0), S.resolveFont(bottomBtn.font))) {
std::string url = app->settings()->getTxExplorerUrl() + tx.txid;
std::string url = explorerBase + tx.txid;
util::Platform::openUrl(url);
}
if (!explorerValid) ImGui::EndDisabled();
ImGui::SameLine();