/** * protect.js — 前台錯誤自動上報(2026-04 改版) * * 原先的「禁止右鍵 / F12 / Ctrl+U / Ctrl+S / Ctrl+P / 拖曳」等封鎖邏輯已移除: * 1. 無法真正阻擋原始碼檢視(view-source: / curl / Network 皆可繞過) * 2. 破壞無障礙(螢幕閱讀器、鍵盤導航需要右鍵選單與快捷鍵) * 3. 傷害正常使用者(Ctrl+P 列印物件資料、Ctrl+S 離線備份) * 4. 可能違反 WCAG 2.1 可及性規範 * * 本檔僅保留「JS 錯誤 / 資源失敗 / Promise rejection」自動上報至 Firestore 的功能, * 後台管理員可在「前台錯誤日誌」頁檢視。 */ (function () { 'use strict'; // 後台管理頁面不上報(管理員自行察覺) var path = location.pathname; if (path.indexOf('homeadmin') > -1 || path.indexOf('admin-mobile') > -1) return; // 開發/測試環境不上報(避免本機 preview 污染正式環境日誌) var host = location.hostname; if (host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || /^192\.168\./.test(host) || /^10\./.test(host) || host.endsWith('.local')) return; var PROJECT_ID = 'rental-website-23b56'; var API_KEY = 'AIzaSyAduhL2XJyjvArZislgcMzwUkNiKxImXLU'; var ENDPOINT = 'https://firestore.googleapis.com/v1/projects/' + PROJECT_ID + '/databases/(default)/documents/artifacts/' + PROJECT_ID + '/public/data/error_logs?key=' + API_KEY; // 去重:同頁面同錯誤訊息 60 秒內只上報一次 var _seen = Object.create(null); function sendError(level, message, source, extra) { if (!message || String(message).length < 3) return; var src = String(source || ''); // 過濾:瀏覽器擴充套件錯誤(非本站問題) if (src.indexOf('extension://') > -1 || src.indexOf('moz-extension') > -1) return; // 過濾:已知無害警告 if (String(message).indexOf('ResizeObserver loop') > -1) return; if (String(message).indexOf('Non-Error promise rejection') > -1) return; var key = level + ':' + String(message).slice(0, 120); var now = Date.now(); if (_seen[key] && now - _seen[key] < 60000) return; _seen[key] = now; var cleanSrc = src.replace(location.origin, '').slice(0, 300); var ex = extra || {}; try { fetch(ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fields: { level: { stringValue: level }, message: { stringValue: String(message).slice(0, 500) }, source: { stringValue: cleanSrc }, stack: { stringValue: String(ex.stack || '').slice(0, 1500) }, lineno: { integerValue: String(ex.lineno || 0) }, colno: { integerValue: String(ex.colno || 0) }, page: { stringValue: location.pathname }, url: { stringValue: location.href.slice(0, 300) }, ua: { stringValue: navigator.userAgent.slice(0, 200) }, isMobile: { booleanValue: /Mobi|Android/i.test(navigator.userAgent) }, timestamp: { integerValue: String(now) } } }), keepalive: true }).catch(function () { /* 靜默失敗 */ }); } catch (e) { /* 靜默失敗 */ } } // JS 執行錯誤 + 資源載入失敗(capture=true 才能捕捉資源錯誤) window.addEventListener('error', function (e) { var t = e.target; if (t && t !== window) { if (t.tagName === 'IMG' || t.tagName === 'SCRIPT' || t.tagName === 'LINK') { sendError('warning', '資源載入失敗: <' + t.tagName + '>', t.src || t.href || ''); } return; } if (e.message && e.message !== 'Script error.') { sendError('error', e.message, e.filename || '', { stack: e.error ? (e.error.stack || '') : '', lineno: e.lineno || 0, colno: e.colno || 0 }); } }, true); // 未捕捉的 Promise rejection window.addEventListener('unhandledrejection', function (e) { var reason = e.reason || {}; var msg = (reason.message || String(reason)).slice(0, 300) || 'Unhandled Promise Rejection'; sendError('error', msg, '', { stack: reason.stack || '' }); }); })();