Add scheme failsafe to all inventory URL link buttons

If a stored URL is missing http:// or https:// the browser treats it as relative
and appends it to the app URL. Guard in three places:

- inventory-catalog-lookup.js syncLinkButton: ensureAbsoluteUrl() prepends https://
- inventory-label-scan.js syncLink: same guard for scan-filled URL fields
- Details.cshtml SafeUrl() Razor helper on SpecPageUrl, SdsUrl, TdsUrl links

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-22 17:38:09 -04:00
parent baec0b33f7
commit 14f220347b
3 changed files with 20 additions and 5 deletions
@@ -5,6 +5,11 @@
ViewData["PageIcon"] = "bi-box-seam"; ViewData["PageIcon"] = "bi-box-seam";
ViewData["PageHelpTitle"] = "Inventory Item"; ViewData["PageHelpTitle"] = "Inventory Item";
ViewData["PageHelpContent"] = "Full detail for this inventory item. Stock Information shows current quantity and reorder thresholds &mdash; a Low Stock banner appears when quantity is at or below the Reorder Point. Pricing shows Unit Cost (what you paid), Average Cost (weighted average across purchases), and Total Stock Value. Use the Actions panel to edit, view jobs using this powder, or delete the item."; ViewData["PageHelpContent"] = "Full detail for this inventory item. Stock Information shows current quantity and reorder thresholds &mdash; a Low Stock banner appears when quantity is at or below the Reorder Point. Pricing shows Unit Cost (what you paid), Average Cost (weighted average across purchases), and Total Stock Value. Use the Actions panel to edit, view jobs using this powder, or delete the item.";
string SafeUrl(string? url) =>
string.IsNullOrEmpty(url) ? "#"
: (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
? url : "https://" + url;
} }
@section Styles { @section Styles {
@@ -184,7 +189,7 @@
<div class="col-12"> <div class="col-12">
<label class="text-muted small mb-1">Product URL</label> <label class="text-muted small mb-1">Product URL</label>
<p class="mb-0"> <p class="mb-0">
<a href="@Model.SpecPageUrl" target="_blank" class="text-decoration-none"> <a href="@SafeUrl(Model.SpecPageUrl)" target="_blank" class="text-decoration-none">
<i class="bi bi-box-arrow-up-right me-1"></i>View on Manufacturer's Web Site <i class="bi bi-box-arrow-up-right me-1"></i>View on Manufacturer's Web Site
</a> </a>
</p> </p>
@@ -197,13 +202,13 @@
<div class="d-flex gap-2 flex-wrap"> <div class="d-flex gap-2 flex-wrap">
@if (!string.IsNullOrEmpty(Model.SdsUrl)) @if (!string.IsNullOrEmpty(Model.SdsUrl))
{ {
<a href="@Model.SdsUrl" target="_blank" class="btn btn-sm btn-outline-secondary"> <a href="@SafeUrl(Model.SdsUrl)" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-file-earmark-pdf me-1"></i>Safety Data Sheet <i class="bi bi-file-earmark-pdf me-1"></i>Safety Data Sheet
</a> </a>
} }
@if (!string.IsNullOrEmpty(Model.TdsUrl)) @if (!string.IsNullOrEmpty(Model.TdsUrl))
{ {
<a href="@Model.TdsUrl" target="_blank" class="btn btn-sm btn-outline-secondary"> <a href="@SafeUrl(Model.TdsUrl)" target="_blank" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-file-earmark-text me-1"></i>Technical Data Sheet <i class="bi bi-file-earmark-text me-1"></i>Technical Data Sheet
</a> </a>
} }
@@ -413,10 +413,15 @@
// ── Helpers ─────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────
function ensureAbsoluteUrl(url) {
if (!url) return url;
return /^https?:\/\//i.test(url) ? url : 'https://' + url;
}
function syncLinkButton(inputId, linkId, url) { function syncLinkButton(inputId, linkId, url) {
const link = document.getElementById(linkId); const link = document.getElementById(linkId);
if (!link) return; if (!link) return;
if (url) { link.href = url; link.classList.remove('d-none'); } if (url) { link.href = ensureAbsoluteUrl(url); link.classList.remove('d-none'); }
else { link.classList.add('d-none'); } else { link.classList.add('d-none'); }
} }
@@ -550,10 +550,15 @@
}); });
} }
function ensureAbsoluteUrl(url) {
if (!url) return url;
return /^https?:\/\//i.test(url) ? url : 'https://' + url;
}
function syncLink(inputId, linkId, url) { function syncLink(inputId, linkId, url) {
const link = document.getElementById(linkId); const link = document.getElementById(linkId);
if (!link) return; if (!link) return;
if (url) { link.href = url; link.classList.remove('d-none'); } if (url) { link.href = ensureAbsoluteUrl(url); link.classList.remove('d-none'); }
else { link.classList.add('d-none'); } else { link.classList.add('d-none'); }
} }