127 lines
4.7 KiB
Dart
127 lines
4.7 KiB
Dart
import 'dart:convert';
|
|
|
|
import '../../domain/school_term.dart';
|
|
import 'school_navigation_policy.dart';
|
|
|
|
/// Only serialize recognized table fragments, never a login document.
|
|
const schoolPageProbeScript = r'''(() => {
|
|
const trusted = value => {
|
|
try {
|
|
const u = new URL(value);
|
|
return u.hostname === 'yjsjy.uestc.edu.cn' && !u.username && !u.password &&
|
|
((u.protocol === 'https:' && (!u.port || u.port === '443')) ||
|
|
(u.protocol === 'http:' && (!u.port || u.port === '80'))) &&
|
|
/^\/pyxx(?:\/|$)/.test(u.pathname);
|
|
} catch (_) { return false; }
|
|
};
|
|
const topUrl = location.href;
|
|
const result = {topUrl, authenticated: false, pages: [], terms: []};
|
|
if (!trusted(topUrl) || document.querySelector('input[type="password"]')) {
|
|
return JSON.stringify(result);
|
|
}
|
|
const topOrigin = new URL(topUrl).origin;
|
|
const home = /^\/pyxx\/home\/(?:index)?\/?$/.test(location.pathname);
|
|
const chrome = document.querySelector('#navbar') && document.querySelector('#main-container');
|
|
result.authenticated = typeof window.logout === 'function' && Boolean(chrome || home);
|
|
const seen = new Set();
|
|
const visit = (doc, depth) => {
|
|
if (!doc || depth > 4 || seen.size >= 16 || seen.has(doc)) return;
|
|
seen.add(doc);
|
|
let url;
|
|
try { url = doc.location.href; } catch (_) { return; }
|
|
if (!trusted(url) || new URL(url).origin !== topOrigin ||
|
|
doc.querySelector('input[type="password"]')) return;
|
|
for (const [kind, tableId, termId] of [
|
|
['timetable', '#tbl', '#drpxq'], ['exams', '#sample-table-1', '#xqbh']
|
|
]) {
|
|
const table = doc.querySelector(tableId), term = doc.querySelector(termId);
|
|
if (term && term.options && term.options.length <= 146) {
|
|
for (const option of Array.from(term.options)) {
|
|
const id = Number(option.value);
|
|
if (/^[1-9]\d*$/.test(option.value) && id <= 9999 &&
|
|
!result.terms.includes(id) && result.terms.length < 146) result.terms.push(id);
|
|
}
|
|
}
|
|
if (!table || !term || !term.options || term.selectedIndex < 0) continue;
|
|
const selected = term.cloneNode(true);
|
|
Array.from(selected.options).forEach((option, index) => {
|
|
if (index === term.selectedIndex) option.setAttribute('selected', 'selected');
|
|
else option.removeAttribute('selected');
|
|
});
|
|
const html = selected.outerHTML + table.outerHTML;
|
|
if (html.length <= 2 * 1024 * 1024) result.pages.push({kind, url, html});
|
|
}
|
|
for (const frame of doc.querySelectorAll('iframe,frame')) {
|
|
try { visit(frame.contentDocument, depth + 1); } catch (_) {}
|
|
}
|
|
};
|
|
visit(document, 0);
|
|
return JSON.stringify(result);
|
|
})()''';
|
|
|
|
class SchoolTableFragment {
|
|
const SchoolTableFragment(this.kind, this.url, this.html);
|
|
final String kind;
|
|
final Uri url;
|
|
final String html;
|
|
}
|
|
|
|
class SchoolPageEvidence {
|
|
const SchoolPageEvidence(
|
|
this.authenticated,
|
|
this.pages, {
|
|
this.terms = const [],
|
|
});
|
|
final bool authenticated;
|
|
final List<SchoolTableFragment> pages;
|
|
final List<SchoolTerm> terms;
|
|
|
|
static SchoolPageEvidence decode(Object raw, Uri current) {
|
|
dynamic value = raw;
|
|
for (var i = 0; i < 2 && value is String; i++) {
|
|
value = jsonDecode(value);
|
|
}
|
|
if (value is! Map ||
|
|
value['topUrl'] != current.toString() ||
|
|
!SchoolNavigationPolicy.isGraduatePage(current)) {
|
|
throw const FormatException('Untrusted school page evidence');
|
|
}
|
|
final pages = <SchoolTableFragment>[];
|
|
final candidates = value['pages'];
|
|
if (candidates is List && candidates.length <= 32) {
|
|
for (final page in candidates) {
|
|
if (page is! Map || page['url'] is! String || page['html'] is! String) {
|
|
continue;
|
|
}
|
|
final uri = Uri.tryParse(page['url'] as String);
|
|
final content = page['html'] as String;
|
|
if (uri == null ||
|
|
!SchoolNavigationPolicy.isGraduatePage(uri) ||
|
|
uri.origin != current.origin ||
|
|
utf8.encode(content).length > 2 * 1024 * 1024 ||
|
|
!const ['timetable', 'exams'].contains(page['kind'])) {
|
|
continue;
|
|
}
|
|
pages.add(SchoolTableFragment(page['kind'] as String, uri, content));
|
|
}
|
|
}
|
|
final terms = <SchoolTerm>[];
|
|
final candidatesTerms = value['terms'];
|
|
if (candidatesTerms is List && candidatesTerms.length <= 146) {
|
|
final ids =
|
|
candidatesTerms
|
|
.whereType<int>()
|
|
.where((id) => id >= 1 && id <= 9999)
|
|
.toSet()
|
|
.toList()
|
|
..sort((a, b) => b.compareTo(a));
|
|
terms.addAll(ids.map(SchoolTerm.new));
|
|
}
|
|
return SchoolPageEvidence(
|
|
value['authenticated'] == true,
|
|
pages,
|
|
terms: terms,
|
|
);
|
|
}
|
|
}
|