66let radarChartInstance = null ;
77let barChartInstance = null ;
88
9+ // HTML 跳脫輔助函數,防範 XSS
10+ function escapeHTML ( str ) {
11+ if ( str === null || str === undefined ) return '' ;
12+ const text = String ( str ) ;
13+ const map = {
14+ '&' : '&' ,
15+ '<' : '<' ,
16+ '>' : '>' ,
17+ '"' : '"' ,
18+ "'" : '''
19+ } ;
20+ return text . replace ( / [ & < > " ' ] / g, function ( m ) { return map [ m ] ; } ) ;
21+ }
22+
923document . addEventListener ( 'DOMContentLoaded' , ( ) => {
1024 loadGradesData ( ) ;
1125 setupFileImport ( ) ;
@@ -377,13 +391,13 @@ function generateScoreCards(subjects) {
377391 card . className = 'score-card' ;
378392 card . innerHTML = `
379393 <div class="score-header">
380- <span class="subject-name">${ subject . SubjectName } </span>
381- <span class="score-badge ${ scoreClass } ">${ subject . ScoreDisplay ?? scoreValue } </span>
394+ <span class="subject-name">${ escapeHTML ( subject . SubjectName ) } </span>
395+ <span class="score-badge ${ scoreClass } ">${ escapeHTML ( subject . ScoreDisplay ?? scoreValue ) } </span>
382396 </div>
383397 <div class="score-details">
384398 <div class="score-row">
385399 <span class="score-label">班級平均</span>
386- <span class="score-value">${ subject . ClassAVGScoreDisplay ?? classAvgValue . toFixed ( 2 ) } </span>
400+ <span class="score-value">${ escapeHTML ( subject . ClassAVGScoreDisplay ?? classAvgValue . toFixed ( 2 ) ) } </span>
387401 </div>
388402 <div class="score-row">
389403 <span class="score-label">與班平均差距</span>
@@ -626,7 +640,7 @@ function generateStandardsTable(subjects, standards) {
626640
627641 const row = document . createElement ( 'tr' ) ;
628642 row . innerHTML = `
629- <td>${ shortenName ( subject . SubjectName ) } </td>
643+ <td>${ escapeHTML ( shortenName ( subject . SubjectName ) ) } </td>
630644 <td class="top-mark">${ std [ "頂標" ] . toFixed ( 2 ) } </td>
631645 <td class="front-mark">${ std [ "前標" ] . toFixed ( 2 ) } </td>
632646 <td class="avg-mark">${ std [ "均標" ] . toFixed ( 2 ) } </td>
@@ -682,7 +696,7 @@ function generateDistributionCards(subjects, standards) {
682696 const card = document . createElement ( 'div' ) ;
683697 card . className = 'distribution-card' ;
684698 card . innerHTML = `
685- <h4>${ subject . SubjectName } </h4>
699+ <h4>${ escapeHTML ( subject . SubjectName ) } </h4>
686700 <div class="distribution-bars">
687701 ${ ranges . map ( r => {
688702 const percentage = r . count === 0 ? 0 : ( r . count / total ) * 100 ;
@@ -838,12 +852,18 @@ function setupSyncFeature() {
838852 const handleLogin = async ( ) => {
839853 const username = usernameInput . value . trim ( ) ;
840854 const password = passwordInput . value . trim ( ) ;
855+ const turnstileResponse = typeof turnstile !== 'undefined' ? turnstile . getResponse ( ) : null ;
841856
842857 if ( ! username || ! password ) {
843858 showStatus ( loginStatus , '請輸入帳號密碼' , 'error' ) ;
844859 return ;
845860 }
846861
862+ if ( ! turnstileResponse ) {
863+ showStatus ( loginStatus , '請完成驗證' , 'error' ) ;
864+ return ;
865+ }
866+
847867 showStatus ( loginStatus , '載入中...' , 'normal' ) ;
848868 confirmLogin . disabled = true ;
849869
@@ -852,7 +872,7 @@ function setupSyncFeature() {
852872 method : 'POST' ,
853873 headers : { 'Content-Type' : 'application/json' } ,
854874 credentials : 'include' ,
855- body : JSON . stringify ( { username, password } )
875+ body : JSON . stringify ( { username, password, turnstile_response : turnstileResponse } )
856876 } ) ;
857877 const data = await res . json ( ) ;
858878
@@ -866,9 +886,11 @@ function setupSyncFeature() {
866886 } , 500 ) ;
867887 } else {
868888 showStatus ( loginStatus , data . message || '登入失敗' , 'error' ) ;
889+ if ( typeof turnstile !== 'undefined' ) turnstile . reset ( ) ;
869890 }
870891 } catch ( error ) {
871892 showStatus ( loginStatus , '連線錯誤: ' + error . message , 'error' ) ;
893+ if ( typeof turnstile !== 'undefined' ) turnstile . reset ( ) ;
872894 } finally {
873895 confirmLogin . disabled = false ;
874896 }
0 commit comments