Files
TPSchedule/mobile/lib/features/school/undergraduate/page_probe.dart
T

122 lines
5.6 KiB
Dart

import 'dart:convert';
import 'navigation_policy.dart';
const undergraduateProbeBase = r'''(() => {
const capture=__CAPTURE__;
const topUrl=location.href;
const result={topUrl,authenticated:false,pages:[]};
const trusted=value=>{try{const u=new URL(value);return u.protocol==='https:' && u.hostname==='eams.uestc.edu.cn' &&
(!u.port || u.port==='443') && !u.username && !u.password && u.pathname.startsWith('/eams/');}catch(_){return false;}};
if(!trusted(topUrl) || document.querySelector('input[type="password"]'))return JSON.stringify(result);
const origin=new URL(topUrl).origin,seen=new Set();let total=0;
const headers=['课程序号','课程名称','考试日期','考试安排','考试地点','座位号','考试情况','考试类型','其它说明'];
const add=(kind,url,body)=>{
total+=new TextEncoder().encode(body).byteLength;
if(total>2*1024*1024)throw new Error('capture_limit');
result.pages.push({kind,url,html:body});
};
const visit=(doc,depth)=>{
if(!doc || seen.has(doc))return;
let url;try{url=doc.location.href;}catch(_){return;}
if(!trusted(url) || new URL(url).origin!==origin || doc.querySelector('input[type="password"]'))return;
if(depth>4 || seen.size>=16)throw new Error('capture_limit');seen.add(doc);
const timetable=doc.querySelector('#manualArrangeCourseTable');
const exams=Array.from(doc.querySelectorAll('table')).filter(table=>{
const row=table.querySelector('tr');if(!row)return false;
const values=Array.from(row.children).map(c=>c.textContent.trim());
return values.length===9 && headers.every((v,i)=>values[i]===v);
});
const logout=Array.from(doc.querySelectorAll('a[href]')).some(a=>{try{return new URL(a.getAttribute('href'),url).pathname==='/eams/logout.action';}catch(_){return false;}});
result.authenticated=result.authenticated || Boolean(logout || timetable || exams.length);
if(capture && (timetable || exams.length)){
const fields=Array.from(doc.querySelectorAll('input[name="semester.id"],input[name="examType.id"],input[name="params"]')).map(input=>{
const clone=input.cloneNode(true);clone.setAttribute('value',input.value);return clone.outerHTML;
}).join('');
const scripts=Array.from(doc.querySelectorAll('script')).filter(s=>/new\s+TaskActivity|\bunitCount\s*=|\bvar\s+params\s*=/.test(s.textContent)).map(s=>s.outerHTML).join('');
if(timetable){
const roster=Array.from(doc.querySelectorAll('table')).filter(table=>{
const row=table.querySelector('tr');return row && Array.from(row.children).some(c=>c.textContent.trim()==='课程序号') &&
Array.from(row.children).some(c=>c.textContent.trim()==='课程代码');
}).map(t=>t.outerHTML).join('');
add('timetable',url,fields+timetable.outerHTML+roster+scripts);
}
for(const table of exams)add('exams',url,'<form id="stdExamTableForm">'+fields+'</form>'+table.outerHTML+scripts);
}
for(const frame of doc.querySelectorAll('iframe,frame')){
let child;try{child=frame.contentDocument;}catch(_){continue;}visit(child,depth+1);
}
};
try{visit(document,0);}catch(_){result.error='capture_limit';result.pages=[];}
return JSON.stringify(result);
})()''';
final undergraduateStatusProbeScript = undergraduateProbeBase.replaceAll(
'__CAPTURE__',
'false',
);
final undergraduateCaptureScript = undergraduateProbeBase.replaceAll(
'__CAPTURE__',
'true',
);
class UndergraduateFragment {
const UndergraduateFragment(this.kind, this.url, this.html);
final String kind, html;
final Uri url;
}
class UndergraduatePageEvidence {
const UndergraduatePageEvidence(this.authenticated, this.pages);
final bool authenticated;
final List<UndergraduateFragment> pages;
static UndergraduatePageEvidence decode(Object raw, Uri current) {
try {
dynamic value = raw;
for (var i = 0; i < 2 && value is String; i++) {
value = jsonDecode(value);
}
if (value is! Map ||
value['topUrl'] != current.toString() ||
value['error'] != null ||
!UndergraduateNavigationPolicy.canNavigate(
current,
isMainFrame: true,
))
throw const FormatException('页面证据无效');
final pages = value['pages'];
if (pages is! List || pages.length > 32)
throw const FormatException('页面片段过多');
var bytes = 0;
final result = <UndergraduateFragment>[];
for (final page in pages) {
if (page is! Map ||
page['url'] is! String ||
page['html'] is! String ||
!const ['timetable', 'exams'].contains(page['kind']))
throw const FormatException('页面片段无效');
final uri = Uri.parse(page['url'] as String),
body = page['html'] as String;
bytes += utf8.encode(body).length;
if (bytes > 2 * 1024 * 1024 ||
!UndergraduateNavigationPolicy.isDataPage(current) ||
!UndergraduateNavigationPolicy.isDataPage(uri) ||
current.origin != uri.origin)
throw const FormatException('页面来源或大小无效');
result.add(UndergraduateFragment(page['kind'] as String, uri, body));
}
if (!UndergraduateNavigationPolicy.isDataPage(current) &&
(value['authenticated'] == true || result.isNotEmpty))
throw const FormatException('认证页面不能作为数据来源');
return UndergraduatePageEvidence(
value['authenticated'] == true,
List.unmodifiable(result),
);
} on FormatException {
rethrow;
} catch (_) {
throw const FormatException('学校页面读取未完成');
}
}
}