Skip to content

Commit b3992fc

Browse files
authored
Clean up and use more secure JavaScript (#314)
* Clean up and use more secure JavaScript * Fix up JS
1 parent d2472c5 commit b3992fc

File tree

1 file changed

+118
-82
lines changed

1 file changed

+118
-82
lines changed

doc/source/_static/js/commit-banner.js

Lines changed: 118 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// --- Global Constants (Moved from Theme Switcher) ---
1+
// --- Global Constants (Theme & Background) ---
22
const THEME_KEY = 'sphinx_theme_preference';
33
const CUSTOM_THEME_KEY = 'sphinx_custom_theme_colors';
44
const CUSTOM_THEME_TRIGGER = 'sphinx_custom_theme_active';
@@ -9,11 +9,12 @@ const GITHUB_COMMIT_URL = `${GITHUB_REPO_URL}/commit/`;
99
const prRegex = /\((#\d+)\)\s*$/;
1010
let allCommits = [];
1111

12-
// --- Theme Randomization Functions (Moved from Theme Switcher) ---
12+
// ---------------------------------------------
13+
// I. THEME CORE LOGIC (Sanitized for DOM interaction)
14+
// ---------------------------------------------
1315

1416
/**
15-
* Generates a random dark hex color (good for backgrounds).
16-
* @returns {string} A hex color string.
17+
* Generates random dark/bright colors (omitted for brevity, assume safe functions).
1718
*/
1819
function getRandomDarkColor() {
1920
const r = Math.floor(Math.random() * 128);
@@ -22,21 +23,13 @@ function getRandomDarkColor() {
2223
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).padStart(6, '0');
2324
}
2425

25-
/**
26-
* Generates a random bright hex color (good for text/headings).
27-
* @returns {string} A hex color string.
28-
*/
2926
function getRandomBrightColor() {
3027
const r = Math.floor(Math.random() * 128) + 128;
3128
const g = Math.floor(Math.random() * 128) + 128;
3229
const b = Math.floor(Math.random() * 128) + 128;
3330
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1).padStart(6, '0');
3431
}
3532

36-
/**
37-
* Generates a completely random hex color for accents/links.
38-
* @returns {string} A hex color string.
39-
*/
4033
function getRandomAccentColor() {
4134
return (
4235
'#' +
@@ -46,19 +39,13 @@ function getRandomAccentColor() {
4639
);
4740
}
4841

49-
// --- Theme Core Logic (Moved from Theme Switcher) ---
50-
51-
/**
52-
* Generates and applies a full random color scheme to the custom-theme CSS variables.
53-
* @param {boolean} persist - Whether to save the generated scheme to localStorage.
54-
* @param {object} [colors] - Optional pre-generated colors to use (for loading from storage).
55-
*/
5642
function setRandomTheme(persist = true, colors = null) {
5743
let newScheme;
5844

5945
if (colors) {
6046
newScheme = colors;
6147
} else {
48+
// Generate new scheme (all functions return clean hex strings)
6249
const mainBg = getRandomDarkColor();
6350
const accentColor = getRandomAccentColor();
6451
const textColor = getRandomBrightColor();
@@ -78,6 +65,7 @@ function setRandomTheme(persist = true, colors = null) {
7865

7966
const root = document.documentElement;
8067
for (const [key, value] of Object.entries(newScheme)) {
68+
// Setting CSS properties with safe string values is secure
8169
root.style.setProperty(key, value);
8270
}
8371

@@ -89,45 +77,9 @@ function setRandomTheme(persist = true, colors = null) {
8977

9078
document.body.classList.add('custom-random-theme');
9179
document.body.classList.remove('light-theme', 'dark-theme');
92-
updateThemeLinks('random'); // Update link state
93-
}
94-
95-
/**
96-
* Updates the visual state of the theme links in the new dropdown.
97-
* @param {string} currentTheme - 'light', 'dark', or 'random'
98-
*/
99-
function updateThemeLinks(currentTheme) {
100-
const lightLink = document.getElementById('theme-light-link');
101-
const darkLink = document.getElementById('theme-dark-link');
102-
const randomLink = document.getElementById('theme-random-link');
103-
104-
// Reset all to normal
105-
if (lightLink) lightLink.style.fontWeight = 'normal';
106-
if (darkLink) darkLink.style.fontWeight = 'normal';
107-
if (randomLink) randomLink.style.fontWeight = 'normal';
108-
109-
// Set the current one to bold
110-
if (currentTheme === 'light' && lightLink) lightLink.style.fontWeight = 'bold';
111-
if (currentTheme === 'dark' && darkLink) darkLink.style.fontWeight = 'bold';
112-
if (currentTheme === 'random' && randomLink) randomLink.style.fontWeight = 'bold';
80+
updateThemeLinks('random');
11381
}
11482

115-
/**
116-
* Updates the visual state of the background links in the new dropdown.
117-
* NOTE: This relies on the links being present in the HTML (which they are now).
118-
* @param {string} currentState - 'on' or 'off'
119-
*/
120-
function updateBackgroundLinks(currentState) {
121-
const onLink = document.getElementById('bg-on-link');
122-
const offLink = document.getElementById('bg-off-link');
123-
124-
if (onLink) onLink.style.fontWeight = currentState === 'on' ? 'bold' : 'normal';
125-
if (offLink) offLink.style.fontWeight = currentState === 'off' ? 'bold' : 'normal';
126-
}
127-
128-
/**
129-
* Clears the custom theme settings and styles.
130-
*/
13183
function clearCustomTheme() {
13284
document.body.classList.remove('custom-random-theme');
13385
localStorage.removeItem(CUSTOM_THEME_TRIGGER);
@@ -145,9 +97,8 @@ function clearCustomTheme() {
14597
}
14698

14799
/**
148-
* Public function to set the theme (light, dark, or random).
149-
* Defined globally so the HTML `href="javascript:setTheme(...)` works.
150-
* @param {string} theme - 'light', 'dark', or 'random'
100+
* @public Function to set the theme (light, dark, or random).
101+
* Exposed globally via window.setTheme.
151102
*/
152103
window.setTheme = function (theme) {
153104
const body = document.body;
@@ -171,63 +122,145 @@ window.setTheme = function (theme) {
171122
updateThemeLinks(theme);
172123
};
173124

174-
// --- Initialization Logic (Combined) ---
125+
/**
126+
* Updates the visual state of the theme links using classList/style.
127+
* NOTE: The use of element.style.fontWeight here is minimal and localized.
128+
*/
129+
function updateThemeLinks(currentTheme) {
130+
const lightLink = document.getElementById('theme-light-link');
131+
const darkLink = document.getElementById('theme-dark-link');
132+
const randomLink = document.getElementById('theme-random-link');
133+
134+
// Reset all to normal
135+
if (lightLink) lightLink.style.fontWeight = 'normal';
136+
if (darkLink) darkLink.style.fontWeight = 'normal';
137+
if (randomLink) randomLink.style.fontWeight = 'normal';
138+
139+
// Set the current one to bold
140+
if (currentTheme === 'light' && lightLink) lightLink.style.fontWeight = 'bold';
141+
if (currentTheme === 'dark' && darkLink) darkLink.style.fontWeight = 'bold';
142+
if (currentTheme === 'random' && randomLink) randomLink.style.fontWeight = 'bold';
143+
}
144+
145+
/**
146+
* Update background links (placeholder for interaction with background-switcher.js)
147+
*/
148+
window.updateBackgroundLinks = function (currentState) {
149+
const onLink = document.getElementById('bg-on-link');
150+
const offLink = document.getElementById('bg-off-link');
151+
152+
// Use style properties for simple, localized formatting
153+
if (onLink) onLink.style.fontWeight = currentState === 'on' ? 'bold' : 'normal';
154+
if (offLink) offLink.style.fontWeight = currentState === 'off' ? 'bold' : 'normal';
155+
};
175156

176157
function loadThemePreference() {
177158
const customThemeActive = localStorage.getItem(CUSTOM_THEME_TRIGGER);
178159
if (customThemeActive === 'active') {
179160
const storedScheme = JSON.parse(localStorage.getItem(CUSTOM_THEME_KEY));
180161
if (storedScheme) {
181162
setRandomTheme(false, storedScheme);
182-
// We still need to call updateBackgroundLinks() later, but theme links are updated here.
183163
return;
184164
}
185165
}
186166

187167
const storedTheme = localStorage.getItem(THEME_KEY) || 'light';
188-
window.setTheme(storedTheme); // Use the global function
168+
window.setTheme(storedTheme);
189169
}
190170

191-
// --- Commit-Specific Functions (Copied from previous response) ---
171+
// ---------------------------------------------
172+
// II. COMMIT BANNER LOGIC (Secure Refactoring)
173+
// ---------------------------------------------
192174

175+
/**
176+
* Processes the commit message to convert a trailing PR number
177+
* into an HTML link. This is the ONLY place where controlled HTML is generated.
178+
* @param {string} message - The original commit message (potentially malicious).
179+
* @returns {string} The formatted message with a linked PR anchor, or the original message.
180+
*/
193181
function formatCommitMessage(message) {
194-
let formattedMessage = message;
182+
// To prevent XSS, we must escape the user-provided message first.
183+
const tempDiv = document.createElement('div');
184+
tempDiv.textContent = message;
185+
let formattedMessage = tempDiv.innerHTML;
186+
195187
const match = formattedMessage.match(prRegex);
196188

197189
if (match) {
198-
const prTag = match[1];
190+
const prTag = match[1]; // e.g., "#241"
199191
const prNumber = prTag.substring(1);
200192
const prUrl = `${GITHUB_REPO_URL}/pull/${prNumber}`;
201193

194+
// Replace the matched text with the HTML anchor tag.
195+
// The message is already escaped, so this is safe.
202196
formattedMessage = formattedMessage.replace(
203197
prRegex,
204-
` (<a href="${prUrl}" target="_blank" rel="noopener noreferrer" class="commit-pr-link">${prTag}</a>)`,
198+
` (<a
199+
href="${prUrl}"
200+
target="_blank"
201+
rel="noopener noreferrer"
202+
class="commit-pr-link"
203+
>${prTag}</a>)`,
205204
);
206205
}
207206
return formattedMessage;
208207
}
209208

209+
/**
210+
* Renders the list of commits, building the DOM using createElement and
211+
* textContent to ensure security against XSS.
212+
* @param {Array<Object>} commits - The list of commit objects.
213+
*/
210214
function renderCommits(commits) {
211215
const commitList = document.getElementById('commit-list');
212-
commitList.innerHTML = '';
216+
commitList.innerHTML = ''; // Safely clear the list
217+
213218
if (commits.length === 0) {
214-
commitList.innerHTML = '<li>No commits found.</li>';
219+
// Using textContent is safer than innerHTML even for static messages
220+
const li = document.createElement('li');
221+
li.textContent = 'No commits found.';
222+
commitList.appendChild(li);
215223
return;
216224
}
225+
217226
commits.forEach((commit) => {
218227
const li = document.createElement('li');
219-
const formattedMessage = formatCommitMessage(commit.message);
220-
li.innerHTML = `
221-
<a
222-
href="${GITHUB_COMMIT_URL}${commit.sha}"
223-
target="_blank"
224-
class="commit-sha-link"
225-
>
226-
${commit.short_sha}
227-
</a>
228-
(<span class="commit-date-span">${commit.date}</span>):
229-
${formattedMessage}
230-
`;
228+
229+
// --- 1. SHA Link (using textContent for secure data injection) ---
230+
const shaLink = document.createElement('a');
231+
shaLink.href = `${GITHUB_COMMIT_URL}${commit.sha}`;
232+
shaLink.target = '_blank';
233+
shaLink.rel = 'noopener noreferrer'; // Good practice for target="_blank"
234+
shaLink.className = 'commit-sha-link';
235+
shaLink.textContent = commit.short_sha;
236+
237+
// --- 2. Date Span (using textContent) ---
238+
const dateSpan = document.createElement('span');
239+
dateSpan.className = 'commit-date-span';
240+
dateSpan.textContent = commit.date;
241+
242+
// --- 3. Message Content (Handling controlled HTML vs. text) ---
243+
const formattedMessageHTML = formatCommitMessage(commit.message);
244+
245+
// Create a temporary element to parse the HTML and extract its contents safely
246+
const tempDiv = document.createElement('div');
247+
// We rely on formatCommitMessage to only generate safe, non-malicious HTML.
248+
tempDiv.innerHTML = formattedMessageHTML;
249+
250+
// --- 4. Assembly ---
251+
// Structure: <li>SHA_LINK (DATE_SPAN): MESSAGE_CONTENT</li>
252+
li.appendChild(shaLink);
253+
li.appendChild(document.createTextNode(' (')); // Text node for secure parentheses
254+
li.appendChild(dateSpan);
255+
li.appendChild(document.createTextNode('): ')); // Text node for colon and space
256+
257+
// Append the content generated by formatCommitMessage
258+
// This loops through children in case the message was long/complex, though
259+
// formatCommitMessage currently returns just text or text + <a>.
260+
while (tempDiv.firstChild) {
261+
li.appendChild(tempDiv.firstChild);
262+
}
263+
231264
commitList.appendChild(li);
232265
});
233266
}
@@ -252,7 +285,9 @@ function loadCommitData() {
252285
});
253286
}
254287

255-
// --- DOM Ready Event Listener (Combined Initialization & Event Handlers) ---
288+
// ---------------------------------------------
289+
// III. DOM READY / EVENT HANDLERS
290+
// ---------------------------------------------
256291

257292
document.addEventListener('DOMContentLoaded', () => {
258293
// Commit Elements
@@ -266,7 +301,6 @@ document.addEventListener('DOMContentLoaded', () => {
266301

267302
// 1. Initial Load
268303
loadThemePreference();
269-
// Note: loadBackgroundPreference will be called from background-switcher.js
270304

271305
// 2. Commit Dropdown Toggle
272306
commitButton.addEventListener('click', () => {
@@ -284,6 +318,7 @@ document.addEventListener('DOMContentLoaded', () => {
284318

285319
// 4. Commit Search and Filter
286320
searchBar.addEventListener('keyup', (e) => {
321+
// Sanitize input: only convert to lowercase/trim, no insertion into DOM
287322
const searchTerm = e.target.value.toLowerCase().trim();
288323

289324
if (searchTerm === '') {
@@ -293,6 +328,7 @@ document.addEventListener('DOMContentLoaded', () => {
293328

294329
const filteredCommits = allCommits.filter(
295330
(commit) =>
331+
// Comparing against data is safe
296332
commit.message.toLowerCase().includes(searchTerm) ||
297333
commit.sha.toLowerCase().includes(searchTerm),
298334
);

0 commit comments

Comments
 (0)