1- // --- Global Constants (Moved from Theme Switcher ) ---
1+ // --- Global Constants (Theme & Background ) ---
22const THEME_KEY = 'sphinx_theme_preference' ;
33const CUSTOM_THEME_KEY = 'sphinx_custom_theme_colors' ;
44const CUSTOM_THEME_TRIGGER = 'sphinx_custom_theme_active' ;
@@ -9,11 +9,12 @@ const GITHUB_COMMIT_URL = `${GITHUB_REPO_URL}/commit/`;
99const prRegex = / \( ( # \d + ) \) \s * $ / ;
1010let 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 */
1819function 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- */
2926function 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- */
4033function 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- */
5642function 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- */
13183function 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 */
152103window . 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
176157function 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+ */
193181function 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+ */
210214function 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
257292document . 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