crash_page.go RawHistoryFolder

1package webutil
2
3import (
4	"fmt"
5	"html"
6	"net/url"
7	"strings"
8)
9
10// SanitizeCrashPageOriginalURI validates and sanitizes a URI for display on a crash page.
11// Only http, https, and dumb schemes are allowed. Non-allowlisted schemes such as
12// javascript:, data:, and vbscript: are intentionally rejected to prevent XSS when
13// the URI is rendered into crash page HTML.
14func SanitizeCrashPageOriginalURI(originalURI string) string {
15	originalURI = strings.TrimSpace(originalURI)
16	if originalURI == "" {
17		return ""
18	}
19	parsed, err := url.Parse(originalURI)
20	if err != nil {
21		return ""
22	}
23	switch strings.ToLower(parsed.Scheme) {
24	case "http", "https":
25		if parsed.Host == "" {
26			return ""
27		}
28		return parsed.String()
29	case "dumb":
30		if parsed.Host == "" && parsed.Opaque == "" {
31			return ""
32		}
33		return parsed.String()
34	default: // Security: reject all non-allowlisted schemes (javascript:, data:, vbscript:, etc.)
35		return ""
36	}
37}
38
39// BuildCrashPageHTML returns a self-contained HTML page for renderer crash recovery.
40func BuildCrashPageHTML(originalURI string) string {
41	escapedURI := html.EscapeString(originalURI)
42	return fmt.Sprintf(`<!DOCTYPE html>
43<html lang="en">
44<head>
45    <meta charset="utf-8">
46    <meta name="viewport" content="width=device-width, initial-scale=1">
47    <title>Renderer crashed</title>
48    <style>
49        :root { color-scheme: dark; font-family: "IBM Plex Sans", "Segoe UI", sans-serif; }
50        body {
51            margin: 0;
52            min-height: 100vh;
53            display: flex;
54            align-items: center;
55            justify-content: center;
56            background: radial-gradient(circle at top, #253447, #101622 55%%);
57            color: #f2f6fa;
58            padding: 24px;
59        }
60        .card {
61            width: min(640px, 100%%);
62            background: rgba(10, 16, 26, 0.86);
63            border: 1px solid rgba(144, 173, 205, 0.35);
64            border-radius: 16px;
65            box-shadow: 0 24px 64px rgba(0, 0, 0, 0.45);
66            padding: 28px;
67        }
68        .url {
69            margin: 16px 0 20px;
70            padding: 12px;
71            border-radius: 10px;
72            background: rgba(26, 38, 56, 0.85);
73            border: 1px solid rgba(139, 167, 194, 0.28);
74            font-family: "IBM Plex Mono", "Fira Code", monospace;
75            overflow-wrap: anywhere;
76        }
77        .actions { display: flex; gap: 12px; flex-wrap: wrap; }
78        button {
79            border: 0;
80            border-radius: 10px;
81            padding: 10px 16px;
82            cursor: pointer;
83            font-size: 0.95rem;
84            font-weight: 600;
85        }
86        .primary { background: #4dd0e1; color: #061018; }
87        .secondary { background: #233346; color: #d6e5f5; }
88    </style>
89</head>
90<body>
91    <div class="card">
92        <h1>Renderer process ended</h1>
93        <p>The current page was interrupted. You can reload it to continue browsing.</p>
94        <div class="url">%s</div>
95        <div class="actions">
96            <button class="primary" id="reload-btn" data-target="%s">Reload page</button>
97            <button class="secondary" id="stay-btn">Stay on this page</button>
98        </div>
99    </div>
100    <script>
101        const reloadButton = document.getElementById('reload-btn');
102        const targetUrl = (reloadButton.getAttribute('data-target') || '').trim();
103        reloadButton.addEventListener('click', function() {
104            if (targetUrl) {
105                window.location.href = targetUrl;
106                return;
107            }
108            window.location.reload();
109        });
110        document.getElementById('stay-btn').addEventListener('click', function() {
111            this.disabled = true;
112            this.textContent = 'Staying on page';
113        });
114    </script>
115</body>
116</html>`, escapedURI, escapedURI)
117}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close