Skip to content

Commit 34c3756

Browse files
EdNovasclaude
andcommitted
perf/security: 同步站点优化批次 (来自 v2board, 仅迁移 main 适用部分)
安全: - /movie /tv SSR 页面 XSS: escapeHtml 转义 TMDB 插值 + JSON-LD '<' + id 必须数字 - 登录错误信息 innerHTML→textContent - /api/debug 不再泄露密码数量/env 状态 - 图片代理文件名正则收紧为 TMDB 实际格式 - apiLimiter/searchLimiter 用 ipKey(getClientIP+ipKeyGenerator) 按真实客户端 IP 限流(IPv6 归一化) 后端: - SQLite 显式 WAL + wal_autocheckpoint (防 WAL 无限增长) 前端/清理/PWA: - Hls 实例泄漏修复(新流就绪后安全销毁旧实例) - groupedList computed 去渲染期副作用 - ad-filter.js 删死代码(parseM3U8/detectAdSegments/detectAdTimeRanges/filterM3U8) 32K→14.7K - sw.js HTML 改 Stale-While-Revalidate, CACHE_VERSION v24 - iOS PWA meta / prefers-reduced-motion / 部分 aria-label / 触控44px / manifest 补全 注: v2board 专属改动(registerV2boardToken/v2board 限流)main 无对应代码, 已排除。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4a2938a commit 34c3756

5 files changed

Lines changed: 166 additions & 486 deletions

File tree

public/index.html

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
<!-- SW 版本检测和强制更新(必须在最开头) -->
6262
<script data-cfasync="false">
6363
(function () {
64-
var REQUIRED_SW_VERSION = 'v23';
64+
var REQUIRED_SW_VERSION = 'v24';
6565
var STORAGE_KEY = 'sw_cleared_' + REQUIRED_SW_VERSION;
6666

6767
// 如果已经为这个版本清理过,不再重复
@@ -117,6 +117,13 @@
117117
<link rel="icon" href="icon.png" type="image/png">
118118
<meta name="theme-color" content="#141414">
119119

120+
<!-- iOS PWA 支持(iOS 不读 manifest,需要这些 meta 才能添加到主屏并全屏) -->
121+
<meta name="apple-mobile-web-app-capable" content="yes">
122+
<meta name="mobile-web-app-capable" content="yes">
123+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
124+
<meta name="apple-mobile-web-app-title" content="E视界">
125+
<link rel="apple-touch-icon" href="icon.png">
126+
120127
<!-- 关键 CSS (优先加载) -->
121128
<link href="libs/css/bootstrap.min.css" rel="stylesheet">
122129
<link rel="stylesheet" href="libs/css/fontawesome.min.css">
@@ -134,6 +141,16 @@
134141
<script data-cfasync="false" src="libs/js/bootstrap.bundle.min.js" defer></script>
135142

136143
<style>
144+
/* ♿ 尊重用户的"减少动态效果"系统偏好:大幅削弱动画/过渡,
145+
缓解前庭功能障碍/晕动症用户的不适(保留极短时长以不破坏交互反馈) */
146+
@media (prefers-reduced-motion: reduce) {
147+
*, *::before, *::after {
148+
animation-duration: 0.01ms !important;
149+
animation-iteration-count: 1 !important;
150+
transition-duration: 0.01ms !important;
151+
scroll-behavior: auto !important;
152+
}
153+
}
137154
:root {
138155
--bg-color: #141414;
139156
--panel-bg: #1f1f1f;
@@ -1964,6 +1981,8 @@
19641981

19651982
.source-pill {
19661983
padding: 8px 16px;
1984+
min-height: 44px; /* ♿ 移动端触控目标 ≥44px */
1985+
box-sizing: border-box;
19671986
border-radius: 6px;
19681987
background: #2a2a2a;
19691988
border: 1px solid #333;
@@ -2003,6 +2022,11 @@
20032022
border: 1px solid transparent;
20042023
color: #ddd;
20052024
padding: 12px 5px;
2025+
min-height: 44px; /* ♿ 移动端触控目标 ≥44px */
2026+
box-sizing: border-box;
2027+
display: flex;
2028+
align-items: center;
2029+
justify-content: center;
20062030
text-align: center;
20072031
border-radius: 6px;
20082032
font-size: 14px;
@@ -2968,8 +2992,8 @@ <h1 class="hero-title">{{ item.title }}</h1>
29682992

29692993
<!-- 横向滚动 -->
29702994
<div class="scroll-container">
2971-
<button class="scroll-btn left" v-show="historyCanScrollLeft" @click="scrollHistoryRow(-1)"><i
2972-
class="fas fa-chevron-left"></i></button>
2995+
<button class="scroll-btn left" v-show="historyCanScrollLeft" @click="scrollHistoryRow(-1)" aria-label="向左滚动观看历史"><i
2996+
class="fas fa-chevron-left" aria-hidden="true"></i></button>
29732997
<div class="hot-row" ref="watchHistoryRow" @scroll="onHistoryScroll">
29742998
<div class="history-card" v-for="(item, index) in watchHistory" :key="item.name + index">
29752999
<!-- 删除按钮 (放在 card 外层避免随缩放移动) -->
@@ -3002,8 +3026,8 @@ <h1 class="hero-title">{{ item.title }}</h1>
30023026
<div class="movie-date" style="font-size: 11px;">{{ formatWatchTime(item.watchedAt) }}</div>
30033027
</div>
30043028
</div>
3005-
<button class="scroll-btn right" v-show="historyCanScrollRight" @click="scrollHistoryRow(1)"><i
3006-
class="fas fa-chevron-right"></i></button>
3029+
<button class="scroll-btn right" v-show="historyCanScrollRight" @click="scrollHistoryRow(1)" aria-label="向右滚动观看历史"><i
3030+
class="fas fa-chevron-right" aria-hidden="true"></i></button>
30073031
</div>
30083032
</div>
30093033

@@ -3049,8 +3073,8 @@ <h1 class="hero-title">{{ item.title }}</h1>
30493073
<template v-if="rowLists[key] && rowLists[key].length > 0">
30503074
<!-- 收起状态:横向滚动 -->
30513075
<div class="scroll-container" v-show="!expandedRows[key]">
3052-
<button class="scroll-btn left" @click="scrollRow(key, -1)" v-show="rowLeftButtons[key]"><i
3053-
class="fas fa-chevron-left"></i></button>
3076+
<button class="scroll-btn left" @click="scrollRow(key, -1)" v-show="rowLeftButtons[key]" aria-label="向左滚动"><i
3077+
class="fas fa-chevron-left" aria-hidden="true"></i></button>
30543078
<div class="hot-row" :ref="key" @scroll="onRowScroll(key)">
30553079
<div class="hot-card" v-for="item in rowLists[key]" :key="item.id"
30563080
@click="autoSearch(item.title || item.name, item.original_title || item.original_name)">
@@ -3067,8 +3091,8 @@ <h1 class="hero-title">{{ item.title }}</h1>
30673091
</div>
30683092
</div>
30693093
</div>
3070-
<button class="scroll-btn right" @click="scrollRow(key, 1)"><i
3071-
class="fas fa-chevron-right"></i></button>
3094+
<button class="scroll-btn right" @click="scrollRow(key, 1)" aria-label="向右滚动"><i
3095+
class="fas fa-chevron-right" aria-hidden="true"></i></button>
30723096
</div>
30733097
<!-- 展开状态:分页网格布局 -->
30743098
<div class="grid-expanded" v-show="expandedRows[key]">
@@ -3277,7 +3301,7 @@ <h2>{{ infoModalData.title || infoModalData.name }}</h2>
32773301

32783302
<!-- ★★★ 播放详情页 (影院布局) ★★★ -->
32793303
<div class="detail-overlay" :class="{ active: showDetail }">
3280-
<div class="close-btn" tabindex="1" @click="closeDetail" @keydown.enter="closeDetail"><i
3304+
<div class="close-btn" tabindex="1" role="button" aria-label="关闭" @click="closeDetail" @keydown.enter="closeDetail"><i
32813305
class="fas fa-times"></i></div>
32823306

32833307
<div class="player-layout" v-if="currentGroup">
@@ -3494,7 +3518,7 @@ <h1 class="video-title">{{ currentGroup.name }}</h1>
34943518
function showPasswordError(message) {
34953519
const errorDiv = document.getElementById('password-error');
34963520
const errorText = document.getElementById('password-error-text');
3497-
errorText.textContent = message;
3521+
errorText.textContent = message; // 用 textContent 防止服务器返回的错误信息触发 XSS
34983522
errorDiv.style.display = 'flex';
34993523
}
35003524

@@ -4071,7 +4095,8 @@ <h1 class="video-title">{{ currentGroup.name }}</h1>
40714095
if (!groups[name]) groups[name] = { name: name, pic: item.vod_pic, sources: [] };
40724096
const isDefault = (url) => !url || url.includes('mac_default') || url.includes('nopic');
40734097
if (isDefault(groups[name].pic) && !isDefault(item.vod_pic)) groups[name].pic = item.vod_pic;
4074-
if (isDefault(groups[name].pic)) this.handleImgError(name);
4098+
// 注:图片回退(handleImgError)已在搜索 onmessage 中按新到达的项处理,
4099+
// computed 内不再调用,保持纯函数、避免渲染期副作用引发的重渲染抖动。
40754100

40764101
groups[name].sources.push({
40774102
...item,
@@ -4670,7 +4695,7 @@ <h1 class="video-title">{{ currentGroup.name }}</h1>
46704695
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
46714696
border: 1px solid rgba(229, 9, 20, 0.3);
46724697
`;
4673-
toast.innerHTML = message;
4698+
toast.textContent = message; // textContent 防 XSS
46744699
document.body.appendChild(toast);
46754700

46764701
// 延迟刷新以便用户看到提示
@@ -7393,13 +7418,31 @@ <h1 class="video-title">{{ currentGroup.name }}</h1>
73937418

73947419
let progressRestored = false;
73957420

7396-
// 注:DPlayer.switchVideo 不会销毁旧 Hls 实例(库层泄漏,待后续更稳妥地处理)。
7397-
// 此处不主动 destroy,因为 hls.destroy() 会 detach media 触发 <video> 短暂报错,
7398-
// 可能被错误处理误判为"播放失败"而误跳过实际可播的线路。
7421+
// 🧹 修复 Hls 实例泄漏:DPlayer.switchVideo 会新建 Hls 但不销毁旧的。
7422+
// 安全时序——先捕获旧实例,switchVideo 创建新实例后,等"新流真正 playing"
7423+
// 再销毁旧的:此时 <video> 已挂上可用的新源,detach 旧实例不会触发"无源"报错,
7424+
// 从而既回收泄漏又不会误跳过线路(旧实现是在 switchVideo 前 destroy 才出的问题)。
7425+
const _oldHls = (dp.plugins && dp.plugins.hls) || null;
73997426

74007427
dp.switchVideo({ url: finalUrl, type: 'hls' });
74017428
console.log('[进度记忆-切换] switchVideo 已调用' + (finalUrl !== url ? ' (代理模式)' : ''));
74027429

7430+
if (_oldHls) {
7431+
let _destroyed = false; // 防止 'playing' 与 5s 兜底重复销毁
7432+
const destroyOldHls = () => {
7433+
if (_destroyed) return;
7434+
_destroyed = true;
7435+
if (dp.video) dp.video.removeEventListener('playing', destroyOldHls);
7436+
// 仅当它确实是被替换掉的旧实例时才销毁,避免误杀当前实例
7437+
if (_oldHls !== (dp.plugins && dp.plugins.hls)) {
7438+
try { _oldHls.destroy(); } catch (e) { }
7439+
}
7440+
};
7441+
if (dp.video) dp.video.addEventListener('playing', destroyOldHls);
7442+
// 兜底:5秒后无论是否 playing 都回收(仍只在它非当前实例时)
7443+
setTimeout(destroyOldHls, 5000);
7444+
}
7445+
74037446
// ▶ 修复"切换资源/集数后不自动播放 + 中央大按钮图标反向":
74047447
// switchVideo 会复用同一个 <video> 元素但重置 src,HLS 尚未就绪时立刻 dp.play()
74057448
// 既无法真正播放,又会让 dp.on('play') 乐观地把中央按钮设成⏸而与实际(暂停)相反。

0 commit comments

Comments
 (0)