feat: extend optional HTML feedback to graduate imports in 0.7.8
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
const appVersion = '0.7.6';
|
||||
const appBuildNumber = 24;
|
||||
const appVersion = '0.7.8';
|
||||
const appBuildNumber = 26;
|
||||
const appFilingNumber = '蜀ICP备2025169276号-5A';
|
||||
|
||||
@@ -120,7 +120,7 @@ class _FeedbackPageState extends State<FeedbackPage> {
|
||||
_includeHtml &&
|
||||
html == null) {
|
||||
throw const FeedbackException(
|
||||
'最近10分钟没有可用的本科导入页面。请返回学校页面再尝试导入,然后提交反馈;也可以取消附带HTML。',
|
||||
'最近10分钟没有可用的学校导入页面。请返回本科或研究生学校页面再尝试导入,然后提交反馈;也可以取消附带HTML。',
|
||||
);
|
||||
}
|
||||
_pendingSubmission ??= _kind == FeedbackKind.problem
|
||||
@@ -304,9 +304,9 @@ class _FeedbackPageState extends State<FeedbackPage> {
|
||||
CheckboxListTile(
|
||||
key: const Key('feedback-html'),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('附带最近一次本科导入页面HTML'),
|
||||
title: const Text('附带最近一次学校导入页面HTML'),
|
||||
subtitle: const Text(
|
||||
'需先勾选日志。网页可能含姓名、学号、课程、教师和考场信息,仅保留最近10分钟,单独作为邮件附件发送。',
|
||||
'适用于本科和研究生课表、考试导入。需先勾选日志。网页可能含姓名、学号、课程、教师和考场信息,仅保留最近10分钟,单独作为邮件附件发送。',
|
||||
),
|
||||
value: _includeHtml,
|
||||
onChanged: _submitting || !_includeLogs
|
||||
|
||||
@@ -281,6 +281,14 @@ class ProfilePage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
appFilingNumber,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: ink.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'今天也有值得期待的小事。',
|
||||
style: TextStyle(
|
||||
|
||||
@@ -7,6 +7,7 @@ import '../../data/json_schedule_import.dart';
|
||||
import '../../domain/exam_schedule.dart';
|
||||
import '../../domain/school_term.dart';
|
||||
import '../../services/diagnostic_log.dart';
|
||||
import '../../services/school_page_diagnostic.dart';
|
||||
import 'school_navigation_policy.dart';
|
||||
import 'school_page_adapter.dart';
|
||||
import 'school_exam_adapter.dart';
|
||||
@@ -257,6 +258,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
final closing = _logoutFuture;
|
||||
if (closing != null) await closing;
|
||||
if (serial != _openSerial) return;
|
||||
SchoolPageDiagnostic.instance.clear();
|
||||
_timeout?.cancel();
|
||||
_poll?.cancel();
|
||||
_manualRead = false;
|
||||
@@ -303,6 +305,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
final serial = ++_openSerial;
|
||||
SchoolPageDiagnostic.instance.clear();
|
||||
_poll?.cancel();
|
||||
_timeout?.cancel();
|
||||
_pageGeneration++;
|
||||
@@ -460,9 +463,8 @@ class SchoolSession extends ChangeNotifier {
|
||||
'term': selectedTerm?.id,
|
||||
},
|
||||
);
|
||||
if (!hasOpened ||
|
||||
generation != _pageGeneration ||
|
||||
await web.currentUrl() != url) {
|
||||
final liveUrl = await web.currentUrl();
|
||||
if (!hasOpened || generation != _pageGeneration || liveUrl != url) {
|
||||
return;
|
||||
}
|
||||
if (evidence.terms.isNotEmpty) _liveTerms = evidence.terms;
|
||||
@@ -480,6 +482,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
}
|
||||
for (final page in evidence.pages) {
|
||||
if (page.kind != (requestingExams ? 'exams' : 'timetable')) continue;
|
||||
SchoolPageDiagnostic.instance.remember(page.html, page.url);
|
||||
try {
|
||||
if (requestingExams) {
|
||||
final draft = SchoolExamAdapter.extract(page.html);
|
||||
@@ -586,17 +589,23 @@ class SchoolSession extends ChangeNotifier {
|
||||
throw UnsupportedSchoolLayout();
|
||||
}
|
||||
final evidence = await _evidence(uri);
|
||||
if (generation != _pageGeneration ||
|
||||
await web.currentUrl() != uri.toString()) {
|
||||
final liveUrl = await web.currentUrl();
|
||||
if (!hasOpened ||
|
||||
generation != _pageGeneration ||
|
||||
liveUrl != uri.toString()) {
|
||||
throw UnsupportedSchoolLayout();
|
||||
}
|
||||
for (final page in evidence.pages) {
|
||||
if (page.kind != 'timetable') continue;
|
||||
SchoolPageDiagnostic.instance.remember(page.html, page.url);
|
||||
try {
|
||||
final draft = await _reader.read(
|
||||
selectedTerm!.timetableUri,
|
||||
() async => page.html,
|
||||
);
|
||||
if (!hasOpened || generation != _pageGeneration) {
|
||||
throw UnsupportedSchoolLayout();
|
||||
}
|
||||
if (draft.schoolTerm != selectedTerm) continue;
|
||||
return draft;
|
||||
} on UnsupportedSchoolLayout {
|
||||
@@ -648,6 +657,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void invalidateAuthentication() {
|
||||
SchoolPageDiagnostic.instance.clear();
|
||||
_openSerial++;
|
||||
_pageGeneration++;
|
||||
_reader.invalidate();
|
||||
@@ -667,6 +677,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> _performLogout() async {
|
||||
SchoolPageDiagnostic.instance.clear();
|
||||
_openSerial++;
|
||||
_pageGeneration++;
|
||||
_reader.invalidate();
|
||||
@@ -697,6 +708,7 @@ class SchoolSession extends ChangeNotifier {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
SchoolPageDiagnostic.instance.clear();
|
||||
hasOpened = false;
|
||||
_pageGeneration++;
|
||||
_reader.invalidate();
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:html/dom.dart' as dom;
|
||||
import 'package:html/parser.dart' as html;
|
||||
|
||||
/// An opt-in mail attachment, separate from persistent structured logs.
|
||||
@@ -37,11 +38,18 @@ class SchoolPageDiagnostic {
|
||||
void remember(String content, Uri uri) {
|
||||
clear();
|
||||
try {
|
||||
if (uri.scheme != 'https' ||
|
||||
uri.host != 'eams.uestc.edu.cn' ||
|
||||
uri.port != 443 ||
|
||||
final undergraduate =
|
||||
uri.scheme == 'https' &&
|
||||
uri.port == 443 &&
|
||||
uri.host == 'eams.uestc.edu.cn' &&
|
||||
uri.path.startsWith('/eams/');
|
||||
final graduate =
|
||||
uri.host == 'yjsjy.uestc.edu.cn' &&
|
||||
(uri.scheme == 'https' && uri.port == 443 ||
|
||||
uri.scheme == 'http' && uri.port == 80) &&
|
||||
(uri.path == '/pyxx' || uri.path.startsWith('/pyxx/'));
|
||||
if ((!undergraduate && !graduate) ||
|
||||
uri.userInfo.isNotEmpty ||
|
||||
!uri.path.startsWith('/eams/') ||
|
||||
utf8.encode(content).length > maxBytes) {
|
||||
return;
|
||||
}
|
||||
@@ -50,12 +58,35 @@ class SchoolPageDiagnostic {
|
||||
final tables = doc
|
||||
.querySelectorAll('table')
|
||||
.where(
|
||||
(t) =>
|
||||
t.id == 'manualArrangeCourseTable' ||
|
||||
t.querySelector('tr')?.text.contains('课程序号') == true,
|
||||
(t) => graduate
|
||||
? const ['tbl', 'sample-table-1'].contains(t.id)
|
||||
: t.id == 'manualArrangeCourseTable' ||
|
||||
t.querySelector('tr')?.text.contains('课程序号') == true,
|
||||
)
|
||||
.toList();
|
||||
if (tables.isEmpty) return;
|
||||
final graduateTerms = <dom.Element>[];
|
||||
if (graduate) {
|
||||
for (final table in tables) {
|
||||
final id = table.id == 'tbl' ? 'drpxq' : 'xqbh';
|
||||
final original = doc.querySelector('select#$id');
|
||||
if (original == null) continue;
|
||||
final select = dom.Element.tag('select')..id = id;
|
||||
for (final originalOption in original.querySelectorAll('option')) {
|
||||
final option = dom.Element.tag('option')
|
||||
..text = originalOption.text;
|
||||
final value = originalOption.attributes['value'];
|
||||
if (value != null && RegExp(r'^\d{1,6}$').hasMatch(value)) {
|
||||
option.attributes['value'] = value;
|
||||
}
|
||||
if (originalOption.attributes.containsKey('selected')) {
|
||||
option.attributes['selected'] = '';
|
||||
}
|
||||
select.children.add(option);
|
||||
}
|
||||
graduateTerms.add(select);
|
||||
}
|
||||
}
|
||||
final fields = <String, Set<String>>{};
|
||||
void field(String key, String value) {
|
||||
if (const ['semester.id', 'examType.id'].contains(key) &&
|
||||
@@ -83,7 +114,7 @@ class SchoolPageDiagnostic {
|
||||
.join('\n');
|
||||
// Only retain the literal course statements our parser reads. All other
|
||||
// scripts, URL-bearing attributes and form/account fields are omitted.
|
||||
final statements = _courseStatements(script);
|
||||
final statements = undergraduate ? _courseStatements(script) : '';
|
||||
for (final table in tables) {
|
||||
for (final node in table.querySelectorAll(
|
||||
'script,style,link,iframe,object,embed,form,input,textarea,select,button,img,svg,video,audio,source,meta,base',
|
||||
@@ -93,8 +124,16 @@ class SchoolPageDiagnostic {
|
||||
for (final node in [table, ...table.querySelectorAll('*')]) {
|
||||
final attrs = Map<String, String>.from(node.attributes);
|
||||
node.attributes.clear();
|
||||
if (attrs['id'] == 'manualArrangeCourseTable') {
|
||||
node.id = 'manualArrangeCourseTable';
|
||||
final id = attrs['id'];
|
||||
if (node.localName == 'table' &&
|
||||
(id == 'manualArrangeCourseTable' ||
|
||||
graduate && const ['tbl', 'sample-table-1'].contains(id))) {
|
||||
node.id = id!;
|
||||
} else if (graduate &&
|
||||
const ['td', 'th'].contains(node.localName) &&
|
||||
id != null &&
|
||||
RegExp(r'^t[0-7][13579]$').hasMatch(id)) {
|
||||
node.id = id;
|
||||
}
|
||||
for (final key in ['rowspan', 'colspan']) {
|
||||
final value = attrs[key];
|
||||
@@ -108,6 +147,7 @@ class SchoolPageDiagnostic {
|
||||
'$marker<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>'
|
||||
'${graduateTerms.map((t) => t.outerHtml).join()}'
|
||||
'${fields.entries.expand((e) => e.value.map((v) => '<input type="hidden" name="${e.key}" value="$v">')).join()}'
|
||||
'${tables.map((t) => t.outerHtml).join()}'
|
||||
'<script type="application/x-tpschedule-course-data">${statements.replaceAll('<', r'\u003c')}</script></body></html>';
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: hepan_everyday
|
||||
description: TPSchedule 学生课表
|
||||
publish_to: none
|
||||
version: 0.7.6+24
|
||||
version: 0.7.8+26
|
||||
environment:
|
||||
sdk: ^3.13.5
|
||||
dependencies:
|
||||
|
||||
@@ -37,6 +37,40 @@ Future<void> reveal(
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('graduate HTML can be selected, consented and sent with logs', (
|
||||
tester,
|
||||
) async {
|
||||
final store = SchoolPageDiagnostic.instance;
|
||||
addTearDown(store.clear);
|
||||
store.remember(
|
||||
'<select id="drpxq"><option value="73" selected>2026-2027秋</option></select>'
|
||||
'<table id="tbl"><tr><td id="t11">研究生课程排查内容</td></tr></table>',
|
||||
Uri.parse('https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73'),
|
||||
);
|
||||
final sender = RecordingSender();
|
||||
await tester.pumpWidget(MaterialApp(home: FeedbackPage(sender: sender)));
|
||||
await tester.enterText(
|
||||
find.byKey(const Key('feedback-message')),
|
||||
'研究生课表导入失败',
|
||||
);
|
||||
await reveal(tester, find.byKey(const Key('feedback-html')));
|
||||
await tester.tap(find.byKey(const Key('feedback-logs')));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byKey(const Key('feedback-html')));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('同意附带'));
|
||||
await tester.pumpAndSettle();
|
||||
await reveal(tester, find.byKey(const Key('feedback-submit')));
|
||||
await tester.tap(find.byKey(const Key('feedback-submit')));
|
||||
await tester.pumpAndSettle();
|
||||
expect(sender.submissions, hasLength(1));
|
||||
expect(sender.submissions.single.diagnosticLog, isNotNull);
|
||||
expect(
|
||||
utf8.decode(sender.submissions.single.diagnosticHtml!),
|
||||
contains('研究生课程排查内容'),
|
||||
);
|
||||
expect(store.export(), isNull);
|
||||
});
|
||||
testWidgets('HTML is opt in, confirmed, and stays identical on retry', (
|
||||
tester,
|
||||
) async {
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:hepan_everyday/services/diagnostic_log.dart';
|
||||
import 'package:hepan_everyday/services/school_page_diagnostic.dart';
|
||||
import 'package:hepan_everyday/features/school/school_page_adapter.dart';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart';
|
||||
@@ -29,6 +31,132 @@ void main() {
|
||||
platform.delegate.finished(url);
|
||||
}
|
||||
|
||||
test(
|
||||
'graduate auto read retains HTML after leaving and clears on next query',
|
||||
() async {
|
||||
final cache = SchoolPageDiagnostic.instance;
|
||||
addTearDown(cache.clear);
|
||||
await session.openTimetable(term: SchoolTerm(73));
|
||||
platform.web.javascript = (_) async =>
|
||||
schoolPageEvidence(topUrl: platform.web.url!, html: fixture());
|
||||
navigate(SchoolSession.timetableUrl.toString());
|
||||
await flushSchoolEvents();
|
||||
expect(session.pendingDraft, isNotNull);
|
||||
expect(cache.export(), isNotNull);
|
||||
session.setReadingActive(false);
|
||||
expect(cache.export(), isNotNull);
|
||||
await session.openExams(term: SchoolTerm(73));
|
||||
expect(cache.export(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'failed graduate manual parse retains HTML and logout clears it',
|
||||
() async {
|
||||
final cache = SchoolPageDiagnostic.instance;
|
||||
addTearDown(cache.clear);
|
||||
await session.openTimetable(term: SchoolTerm(73));
|
||||
platform.web.url = SchoolSession.timetableUrl.toString();
|
||||
platform.web.javascript = (_) async => schoolPageEvidence(
|
||||
topUrl: platform.web.url!,
|
||||
html: fixture().replaceFirst('1-3周', '未知周'),
|
||||
);
|
||||
await expectLater(
|
||||
session.readTimetable(),
|
||||
throwsA(isA<UnsupportedSchoolLayout>()),
|
||||
);
|
||||
expect(cache.export(), isNotNull);
|
||||
expect(utf8.decode(cache.export()!), contains('未知周'));
|
||||
session.invalidateAuthentication();
|
||||
expect(cache.export(), isNull);
|
||||
},
|
||||
);
|
||||
|
||||
test('graduate exam read retains the recognized exam fragment', () async {
|
||||
final cache = SchoolPageDiagnostic.instance;
|
||||
addTearDown(cache.clear);
|
||||
await session.openExams(term: SchoolTerm(73));
|
||||
platform.web.javascript = (_) async => schoolPageEvidence(
|
||||
topUrl: platform.web.url!,
|
||||
html: examHtml(),
|
||||
kind: 'exams',
|
||||
);
|
||||
navigate(SchoolSession.examUrl.toString());
|
||||
await flushSchoolEvents();
|
||||
expect(session.pendingExams, isNotNull);
|
||||
expect(cache.export(), isNotNull);
|
||||
expect(
|
||||
SchoolExamAdapter.extract(utf8.decode(cache.export()!))
|
||||
.records
|
||||
.single
|
||||
.name,
|
||||
'矩阵理论',
|
||||
);
|
||||
await session.logout();
|
||||
expect(cache.export(), isNull);
|
||||
});
|
||||
|
||||
test(
|
||||
'stale graduate probe cannot replace the current diagnostic attachment',
|
||||
() async {
|
||||
final cache = SchoolPageDiagnostic.instance;
|
||||
addTearDown(cache.clear);
|
||||
await session.openTimetable(term: SchoolTerm(73));
|
||||
final old = Completer<Object>();
|
||||
platform.web.javascript = (_) => old.future;
|
||||
const home = 'https://yjsjy.uestc.edu.cn/pyxx/home/index';
|
||||
navigate(home);
|
||||
await flushSchoolEvents();
|
||||
platform.web.javascript = (_) async => schoolPageEvidence(
|
||||
topUrl: platform.web.url!,
|
||||
html: fixture().replaceAll('示例楼B212', '当前教室A101'),
|
||||
);
|
||||
navigate(SchoolSession.timetableUrl.toString());
|
||||
await flushSchoolEvents();
|
||||
final current = cache.export();
|
||||
expect(current, isNotNull);
|
||||
old.complete(schoolPageEvidence(topUrl: home, html: fixture()));
|
||||
await flushSchoolEvents();
|
||||
expect(cache.export(), current);
|
||||
expect(utf8.decode(cache.export()!), contains('当前教室A101'));
|
||||
},
|
||||
);
|
||||
|
||||
for (final manual in [false, true]) {
|
||||
test(
|
||||
'logout during final URL check rejects stale ${manual ? 'manual' : 'automatic'} HTML',
|
||||
() async {
|
||||
final cache = SchoolPageDiagnostic.instance;
|
||||
addTearDown(cache.clear);
|
||||
await session.openTimetable(term: SchoolTerm(73));
|
||||
final url = SchoolSession.timetableUrl.toString();
|
||||
platform.web.url = url;
|
||||
platform.web.javascript = (_) async =>
|
||||
schoolPageEvidence(topUrl: url, html: fixture());
|
||||
final waitingUrl = Completer<String?>();
|
||||
var reads = 0;
|
||||
platform.web.readCurrentUrl = () async {
|
||||
reads++;
|
||||
if (reads == (manual ? 2 : 3)) return waitingUrl.future;
|
||||
return platform.web.url;
|
||||
};
|
||||
final operation = manual
|
||||
? expectLater(
|
||||
session.readTimetable(),
|
||||
throwsA(isA<UnsupportedSchoolLayout>()),
|
||||
)
|
||||
: session.readCurrentPage();
|
||||
await flushSchoolEvents();
|
||||
expect(reads, manual ? 2 : 3);
|
||||
session.invalidateAuthentication();
|
||||
waitingUrl.complete(url);
|
||||
await operation;
|
||||
expect(cache.export(), isNull);
|
||||
expect(session.pendingDraft, isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
test('login diagnostic breadcrumbs distinguish probe and network failures without raw details', () async {
|
||||
await session.openTimetable(term: SchoolTerm(73));
|
||||
session.setReadingActive(true);
|
||||
|
||||
@@ -4,8 +4,118 @@ import 'dart:io';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hepan_everyday/services/school_page_diagnostic.dart';
|
||||
import 'package:hepan_everyday/features/school/undergraduate/timetable_adapter.dart';
|
||||
import 'package:hepan_everyday/features/school/school_page_adapter.dart';
|
||||
import 'package:hepan_everyday/features/school/school_exam_adapter.dart';
|
||||
|
||||
import '../school/recognized_timetable_test.dart' show fixture;
|
||||
import '../school/exam_adapter_test.dart' show examHtml;
|
||||
|
||||
void exportContractFixture(String name, List<int> bytes) {
|
||||
final directory = Platform.environment['TPSCHEDULE_DIAGNOSTIC_EXPORT_DIR'];
|
||||
if (directory != null) File('$directory/$name').writeAsBytesSync(bytes);
|
||||
}
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'graduate timetable preserves term, periods and course parser evidence',
|
||||
() {
|
||||
final store = SchoolPageDiagnostic();
|
||||
addTearDown(store.clear);
|
||||
final uri = Uri.parse(
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73',
|
||||
);
|
||||
store.remember(
|
||||
'''<header>ACCOUNT_SECRET</header>${fixture()}
|
||||
<input name="ticket" value="AUTH_SECRET"><script>const cookie="AUTH_SECRET";</script>''',
|
||||
uri,
|
||||
);
|
||||
expect(store.export(), isNotNull);
|
||||
final output = utf8.decode(store.export()!);
|
||||
exportContractFixture('client-graduate-timetable.html', store.export()!);
|
||||
final draft = SchoolPageAdapter().extract(output, uri);
|
||||
expect(draft.schoolTerm!.id, 73);
|
||||
expect(draft.courses, hasLength(4));
|
||||
expect(
|
||||
draft.courses
|
||||
.singleWhere((c) => c.weekday == 2 && c.startPeriod == 3)
|
||||
.teacher,
|
||||
'示例教师乙、示例教师丙',
|
||||
);
|
||||
expect(output, isNot(contains('SECRET')));
|
||||
expect(output, isNot(contains('name="ticket"')));
|
||||
},
|
||||
);
|
||||
test('graduate exams preserve pending arrangements and selected term', () {
|
||||
final store = SchoolPageDiagnostic();
|
||||
addTearDown(store.clear);
|
||||
store.remember(
|
||||
examHtml(room: '示例考场'),
|
||||
Uri.parse('https://yjsjy.uestc.edu.cn/pyxx/pygl/kskcxx/index?xq=73'),
|
||||
);
|
||||
expect(store.export(), isNotNull);
|
||||
exportContractFixture('client-graduate-exams.html', store.export()!);
|
||||
final exam = SchoolExamAdapter.extract(utf8.decode(store.export()!));
|
||||
expect(exam.termId, '73');
|
||||
expect(exam.records.single.room, '示例考场');
|
||||
expect(exam.records.single.time, isEmpty);
|
||||
});
|
||||
test(
|
||||
'graduate HTTP frame and malformed parser fields remain diagnosable',
|
||||
() {
|
||||
final store = SchoolPageDiagnostic();
|
||||
addTearDown(store.clear);
|
||||
final uri = Uri.parse('http://yjsjy.uestc.edu.cn/pyxx/home/index');
|
||||
final malformed = fixture().replaceFirst('1-3周', '未知周');
|
||||
store.remember(malformed, uri);
|
||||
expect(store.export(), isNotNull);
|
||||
final output = utf8.decode(store.export()!);
|
||||
expect(output, contains('未知周'));
|
||||
expect(
|
||||
() => SchoolPageAdapter().extract(
|
||||
output,
|
||||
Uri.parse('https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73'),
|
||||
),
|
||||
throwsA(isA<UnsupportedSchoolLayout>()),
|
||||
);
|
||||
for (final address in [
|
||||
'https://yjsjy.uestc.edu.cn.evil.test/pyxx/home/index',
|
||||
'https://yjsjy.uestc.edu.cn:444/pyxx/home/index',
|
||||
'http://yjsjy.uestc.edu.cn:8080/pyxx/home/index',
|
||||
'https://user@yjsjy.uestc.edu.cn/pyxx/home/index',
|
||||
'https://yjsjy.uestc.edu.cn/other/index',
|
||||
]) {
|
||||
store.remember(fixture(), Uri.parse(address));
|
||||
expect(store.export(), isNull, reason: address);
|
||||
}
|
||||
store.remember('<input type="password">${fixture()}', uri);
|
||||
expect(store.export(), isNull);
|
||||
},
|
||||
);
|
||||
test('graduate export removes events, URLs and account controls and expires at ten minutes', () {
|
||||
var now = DateTime.utc(2026, 10, 24);
|
||||
final store = SchoolPageDiagnostic(clock: () => now);
|
||||
addTearDown(store.clear);
|
||||
store.remember(
|
||||
fixture()
|
||||
.replaceFirst(
|
||||
'<select id="drpxq">',
|
||||
'<select id="drpxq" onchange="AUTH_SECRET()">',
|
||||
)
|
||||
.replaceFirst(
|
||||
'示例教师甲',
|
||||
'<a href="https://evil.test/?ticket=AUTH_SECRET" onclick="AUTH_SECRET()">示例教师甲</a><input name="cookie" value="AUTH_SECRET">',
|
||||
),
|
||||
Uri.parse('https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/73'),
|
||||
);
|
||||
expect(store.export(), isNotNull);
|
||||
final output = utf8.decode(store.export()!);
|
||||
expect(output, isNot(contains('AUTH_SECRET')));
|
||||
expect(output, isNot(contains('evil.test')));
|
||||
expect(output, isNot(contains('onchange')));
|
||||
expect(output, contains('示例教师甲'));
|
||||
now = now.add(const Duration(minutes: 10));
|
||||
expect(store.export(), isNull);
|
||||
});
|
||||
final uri = Uri.parse(
|
||||
'https://eams.uestc.edu.cn/eams/courseTableForStd.action',
|
||||
);
|
||||
|
||||
@@ -47,6 +47,7 @@ class FakeSchoolWeb extends PlatformWebViewController {
|
||||
String? documentUrl;
|
||||
Future<Object> Function(String)? javascript;
|
||||
Future<void> Function(Uri)? request;
|
||||
Future<String?> Function()? readCurrentUrl;
|
||||
@override
|
||||
Future<void> setJavaScriptMode(JavaScriptMode mode) async {}
|
||||
@override
|
||||
@@ -61,7 +62,7 @@ class FakeSchoolWeb extends PlatformWebViewController {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> currentUrl() async => url;
|
||||
Future<String?> currentUrl() => readCurrentUrl?.call() ?? Future.value(url);
|
||||
@override
|
||||
Future<Object> runJavaScriptReturningResult(String script) =>
|
||||
script == 'location.href'
|
||||
|
||||
Reference in New Issue
Block a user