feat: extend optional HTML feedback to graduate imports in 0.7.8
This commit is contained in:
@@ -7,6 +7,8 @@ RUN pip install --no-cache-dir -r requirements.txt \
|
||||
&& useradd --system --uid 10001 --create-home feedback \
|
||||
&& mkdir /app/data && chown feedback:feedback /app/data
|
||||
COPY feedback_service.py main.py ./
|
||||
RUN chmod 0755 /app \
|
||||
&& chmod 0644 /app/requirements.txt /app/feedback_service.py /app/main.py
|
||||
USER feedback
|
||||
EXPOSE 8000
|
||||
# The host uses localhost port publishing; only that trusted proxy may supply client IP headers.
|
||||
|
||||
@@ -17,7 +17,7 @@ Prepared for Ubuntu + 1Panel on `43.138.57.216`. Public endpoint: `https://app.g
|
||||
| `school` | Must be omitted | Required, nonblank, max 200 characters |
|
||||
| `attachments` | Optional, max 3 files, 8 MiB each, 20 MiB aggregate | Must be omitted |
|
||||
| `diagnostics` | Optional single UTF-8 text file, max 256 KiB, independent of media count; starts with `TPSchedule diagnostic log` followed by a newline | Must be omitted |
|
||||
| `diagnostic_html` | Optional single UTF-8 HTML file, max 2 MiB, independent of media count; starts with `<!doctype html><!-- TPSchedule school diagnostic -->` | Must be omitted |
|
||||
| `diagnostic_html` | Optional single UTF-8 inert HTML file for undergraduate or graduate imports, max 2 MiB, independent of media count; starts with `<!doctype html><!-- TPSchedule school diagnostic -->` | Must be omitted |
|
||||
|
||||
The app defaults diagnostics to off. When selected it snapshots the preceding ten minutes of structured app/login/import/reminder events, at most 500 entries. It excludes credentials, cookies, raw URLs, HTML, timetable contents and feedback text. The server attaches this file as `TPSchedule-diagnostics.txt`; retries must use the identical snapshot and submission ID. The server hashes it with the other attachments for idempotency, and does not log its contents.
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ class ApiError(Exception):
|
||||
class SchoolHtmlValidator(HTMLParser):
|
||||
"""Accept only the inert diagnostic schema emitted by the app, not web pages."""
|
||||
tags = set(('html head meta title body input table thead tbody tfoot tr td th caption colgroup col '
|
||||
'span div p b i strong em u small br hr a font center nobr label s sub sup pre code').split())
|
||||
'span div p b i strong em u small br hr a font center nobr label s sub sup pre code select option').split())
|
||||
void = {'meta', 'input', 'br', 'hr', 'col'}
|
||||
csp = "default-src 'none'; base-uri 'none'; form-action 'none'"
|
||||
|
||||
@@ -145,9 +145,21 @@ class SchoolHtmlValidator(HTMLParser):
|
||||
elif tag == 'script':
|
||||
if values != {'type': 'application/x-tpschedule-course-data'}:
|
||||
raise ValueError('script')
|
||||
elif tag == 'select':
|
||||
if (self.stack[-1:] != ['body'] or set(values) != {'id'}
|
||||
or values['id'] not in {'drpxq', 'xqbh'}):
|
||||
raise ValueError('semester selector')
|
||||
elif tag == 'option':
|
||||
if (self.stack[-1:] != ['select'] or set(values) - {'value', 'selected'}
|
||||
or 'value' in values and not re.fullmatch(r'\d{1,6}', values['value'] or '')
|
||||
or 'selected' in values and values['selected'] not in {'', 'selected'}):
|
||||
raise ValueError('semester option')
|
||||
else:
|
||||
for key, value in attrs:
|
||||
if key == 'id' and tag == 'table' and value == 'manualArrangeCourseTable':
|
||||
if (key == 'id' and tag == 'table'
|
||||
and value in {'manualArrangeCourseTable', 'tbl', 'sample-table-1'}):
|
||||
continue
|
||||
if key == 'id' and tag in {'td', 'th'} and re.fullmatch(r't[0-7][13579]', value or ''):
|
||||
continue
|
||||
if key in {'rowspan', 'colspan'} and re.fullmatch(r'\d{1,2}', value or ''):
|
||||
continue
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
# 0.7.5:可选本科导入HTML邮件附件更新
|
||||
# 0.7.8:本科、研究生可选导入HTML邮件附件
|
||||
|
||||
本次更新为你已部署的同一个反馈服务增加 `diagnostic_html` 文件字段。客户端默认关闭,需要学生在“反馈与共建→反馈问题和建议”勾选日志、勾选HTML并同意提示;邮件除日志外另附 `TPSchedule-school-page.html`,不占3个图片/视频名额。
|
||||
本次更新沿用你已部署的反馈服务及 `diagnostic_html` 文件字段,新增研究生课表/考试结构兼容,本科继续可用。客户端默认关闭,需要学生在“反馈与共建→反馈问题和建议”勾选日志、“附带最近一次学校导入页面HTML”并同意提示;邮件除日志外另附 `TPSchedule-school-page.html`,不占3个图片/视频名额。更新的是Compose服务`feedback`,与独立更新管理后台的`updates`服务不同。
|
||||
|
||||
## 你需要在1Panel执行的更新
|
||||
|
||||
1. 下载并解压 `TPSchedule-feedback-server.zip`,找到其中 `feedback/feedback_service.py`,覆盖服务器原部署目录中的同名文件,例如 `/opt/app-feedback/feedback_service.py`。不要覆盖服务器 `.env`,保留原 SMTP 密码、`IDEMPOTENCY_SECRET` 和数据库数据卷。
|
||||
1. 下载并解压 `TPSchedule-feedback-server-0.7.8.zip`,找到其中 `feedback/feedback_service.py` 和 `feedback/Dockerfile`,覆盖服务器原部署目录中的同名文件,例如 `/opt/app-feedback/`。Dockerfile增加普通用户所需的明确读取权限,避免上传/解压后文件为600导致无法启动。不要覆盖服务器 `.env`,保留原 SMTP 密码、`IDEMPOTENCY_SECRET` 和数据库数据卷。
|
||||
2. 在1Panel的 `app.glowtps.cn` 站点配置中,把原 `client_max_body_size 21m;` 改成 `client_max_body_size 24m;`,验证并重载OpenResty配置。雷池该站点如果有请求体大小限制,也提高到至少24MiB(若单位为字节则25165824)。原域名、证书、80/443→10080→18080转发不变。
|
||||
3. 在原目录执行:
|
||||
|
||||
```sh
|
||||
cd /opt/app-feedback
|
||||
docker compose up -d --build feedback
|
||||
docker compose up -d --build --force-recreate feedback
|
||||
docker compose ps
|
||||
curl --fail --max-time 10 http://127.0.0.1:18080/healthz
|
||||
```
|
||||
|
||||
健康检查返回 `{"status":"ok"}` 后再测试APP。不要删除数据卷,不要另建新的Compose项目。
|
||||
4. 同学安装0.7.5,在学校网页中打开课表并点击一次“导入当前课表”,失败后返回“我的→反馈与共建”。在10分钟内写明错误并勾选日志、HTML,确认后提交。考试读取失败同样保留最后一次捕获的考试表格。到 `tps-david@glowtps.cn` 核对两个附件;此步骤会真实发送邮件,由你操作。
|
||||
4. 同学覆盖安装0.7.8:本科在学校网页中打开课表并点击一次“导入当前课表”;研究生在APP中选学期后等待自动读取,手动“读取当前页面”也支持。失败后返回“我的→反馈与共建”,在10分钟内写明错误并勾选日志、HTML,确认后提交。两类学生的考试读取失败同样保留最后一次捕获的考试表格。没有捕获到可识别学校表格时会提示返回学校页面重试,不发送登录整页。到 `tps-david@glowtps.cn` 核对两个附件;此步骤会真实发送邮件,由你操作。
|
||||
|
||||
## 数据与兼容性
|
||||
|
||||
- HTML只含本科课表/课程名单或考试表格、学期/考试类型数字,以及解析需要的课程字面量脚本;移除学校页面账户头部、表单、链接地址、事件处理和无关脚本,不采集Cookie。附件中的课程脚本不执行,页面附带禁止网络请求的CSP。
|
||||
- 本科HTML只含课表/课程名单或考试表格、学期/考试类型数字,以及解析需要的课程字面量脚本;研究生只含课表/考试表格、受限节次ID及对应学期选择信息,保留排查需要的解析结构。移除学校页面账户头部、其他表单、链接地址、事件处理和无关脚本,不采集Cookie。附件中的课程脚本不执行,页面附带禁止网络请求的CSP。
|
||||
- 课程、教师、教室、考场及可能存在的个人信息用于排查,仍应按个人资料妥善处理。HTML不是原始整页的逐字备份。
|
||||
- 未提交的采集缓存仅在APP内存保留最多10分钟;退出学校登录、重新认证或开始新的导入会清除。勾选后实际提交时冻结附件,失败重试沿用同一编号和字节;重试表单关闭或APP结束后该冻结副本释放。
|
||||
- HTML最多2MiB,日志256KiB,媒体仍最多3个、每个8MiB、合计20MiB。总HTTP请求上限为20MiB+256KiB+2MiB+64KiB;代理放行24MiB。
|
||||
- 旧客户端不带新字段仍可使用。未更新服务端时,新附件可能被拒绝;APP保留内容并提示更新,取消HTML后仍可只发日志。
|
||||
- 旧客户端不带新字段仍可使用,旧本科HTML继续接受。仅更新APP而不更新反馈服务时,研究生附件可能被旧服务拒绝;应先更新服务,取消HTML后仍可只发日志。
|
||||
- 本工程只准备并测试代码,没有登录你的服务器、改配置或发送真实邮件。
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<!doctype html><!-- TPSchedule school diagnostic --><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'"><title>TPSchedule school diagnostic</title></head><body><select id="xqbh"><option value="73" selected="">2026-2027秋</option></select><table id="sample-table-1"><thead><tr>
|
||||
<th>id</th><th>序号</th><th>学号</th><th>姓名</th><th>课程代码</th><th>课程名称</th><th>考核方式</th><th>考生编号</th><th>考场顺序号</th><th>考场</th><th>座位号</th><th>考试时间</th>
|
||||
</tr></thead><tbody><tr>
|
||||
<td>1</td><td>1</td><td>private-id</td><td>private-name</td><td>MATH</td><td>矩阵理论</td><td>笔试</td><td></td><td></td><td>示例考场</td><td></td><td></td>
|
||||
</tr></tbody></table><script type="application/x-tpschedule-course-data"></script></body></html>
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><!-- TPSchedule school diagnostic --><html><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'none'; form-action 'none'"><title>TPSchedule school diagnostic</title></head><body><select id="drpxq"><option value="73" selected="">2026-2027秋</option></select><table id="tbl"><tbody><tr><th>节次</th><th>星期一</th><th>星期二</th><th>星期三</th><th>星期四</th><th>星期五</th><th>星期六</th><th>星期日</th></tr><tr><td id="t01">1-2节</td><td id="t11"></td><td id="t21"></td><td id="t31"></td><td id="t41"></td><td id="t51">101/示例集成电路设计(1班)/32/2/示例教师甲/1-3周/1~2节/示例楼B212<br>101/示例集成电路设计(1班)/32/2/示例教师甲/6-6周/1~2节/示例楼B212<br>101/示例集成电路设计(1班)/32/2/示例教师甲/7-9周/1~2节/示例楼B212</td><td id="t61"></td><td id="t71"></td></tr><tr><td id="t03">3-4节</td><td id="t13">101/示例集成电路设计(1班)/32/2/示例教师甲/1-5周/3~4节/示例楼B212</td><td id="t23">102/示例信号检测与估计(2班)/32/2/示例教师乙、示例教师丙/1-5周/3~4节/示例楼B102<br>102/示例信号检测与估计(2班)/32/2/示例教师乙、示例教师丙/7-9周/3~4节/示例楼B102</td><td id="t33"></td><td id="t43"></td><td id="t53"></td><td id="t63"></td><td id="t73"></td></tr><tr><td id="t05">5-6节</td><td id="t15"></td><td id="t25"></td><td id="t35"></td><td id="t45"></td><td id="t55"></td><td id="t65"></td><td id="t75"></td></tr><tr><td id="t07">7-8节</td><td id="t17"></td><td id="t27"></td><td id="t37"></td><td id="t47"></td><td id="t57"></td><td id="t67"></td><td id="t77"></td></tr><tr><td id="t09">9-11节</td><td id="t19"></td><td id="t29">103/示例现代化理论与实践(3班)/36/2/示例教师丁/1-12周/9~11节/示例楼B107</td><td id="t39"></td><td id="t49"></td><td id="t59"></td><td id="t69"></td><td id="t79"></td></tr></tbody></table><script type="application/x-tpschedule-course-data"></script></body></html>
|
||||
@@ -31,6 +31,35 @@ def test_real_client_generated_synthetic_html_is_accepted_as_attachment(service)
|
||||
assert attachment.get_payload(decode=True) == page
|
||||
|
||||
|
||||
@pytest.mark.parametrize('fixture', ['client-graduate-timetable.html', 'client-graduate-exams.html'])
|
||||
def test_real_graduate_client_html_is_mailed_without_changing_its_evidence(service, fixture):
|
||||
client, sender, _ = service
|
||||
page = (Path(__file__).parent / 'fixtures' / fixture).read_bytes()
|
||||
fields = [('submission_id', (None, ID)), ('type', (None, 'problem')), ('message', (None, 'Graduate import failed'))]
|
||||
response = client.post('/v1/feedback', files=fields + [('diagnostic_html', ('page.html', page, 'text/html'))])
|
||||
assert response.status_code == 200
|
||||
attachment = list(sender.messages[0].iter_attachments())[0]
|
||||
assert attachment.get_filename() == 'TPSchedule-school-page.html'
|
||||
assert attachment.get_payload(decode=True) == page
|
||||
|
||||
|
||||
@pytest.mark.parametrize('extra', [
|
||||
'<select id="account"><option value="73">Account</option></select>',
|
||||
'<select id="drpxq" onchange="steal()"><option value="73">Term</option></select>',
|
||||
'<select id="xqbh"><option value="https://evil.test/">Term</option></select>',
|
||||
'<option value="73" selected>Outside semester selector</option>',
|
||||
'<select id="drpxq"><option value="73" selected onclick="steal()">Term</option></select>',
|
||||
'<table id="account"><tr><td id="token">Secret</td></tr></table>',
|
||||
])
|
||||
def test_graduate_diagnostics_reject_unrelated_or_active_controls(service, extra):
|
||||
client, sender, _ = service
|
||||
page = HTML.replace(b'</body>', extra.encode() + b'</body>')
|
||||
fields = [('submission_id', (None, ID)), ('type', (None, 'problem')), ('message', (None, 'Graduate import failed'))]
|
||||
response = client.post('/v1/feedback', files=fields + [('diagnostic_html', ('page.html', page, 'text/html'))])
|
||||
assert response.status_code == 415
|
||||
assert not sender.messages
|
||||
|
||||
|
||||
@pytest.mark.parametrize('extra', [
|
||||
'<script>alert(1)</script>', '<img src="https://evil.test/">',
|
||||
'<table onclick="alert(1)"></table>', '<meta http-equiv="refresh" content="0;url=https://evil.test/">',
|
||||
|
||||
Reference in New Issue
Block a user