Files
TPSchedule/mobile/lib/features/school/school_browser_diagnostics.dart
T

127 lines
4.3 KiB
Dart

import 'dart:convert';
import 'package:webview_flutter/webview_flutter.dart';
import '../../services/diagnostic_log.dart';
/// Convert errors into fixed labels; never export the original console text.
String? schoolConsoleCode(String message) {
final text = message.toLowerCase();
if (text.contains('mixed content')) return 'mixed_content';
if (text.contains('content security policy')) {
return 'content_security_policy';
}
if (text.contains('cors')) return 'cross_origin';
if (text.contains('refused to')) return 'resource_refused';
if (text.contains('referenceerror')) {
if (RegExp(r'(?:jquery|\$) is not defined').hasMatch(text)) {
return 'jquery_missing';
}
if (text.contains('beangle is not defined')) return 'beangle_missing';
if (text.contains('require is not defined')) return 'require_missing';
return 'reference_error';
}
if (text.contains('syntaxerror')) return 'syntax_error';
if (text.contains('typeerror')) return 'type_error';
if (text.contains('securityerror')) return 'security_error';
if (text.contains('evalerror')) return 'eval_error';
if (text.contains('rangeerror')) return 'range_error';
return null;
}
// Counts and known library booleans only. This never reads text, forms, cookies,
// storage, or resource URLs. A different document or login form returns null.
const schoolPageDiagnosticsScript = r'''(() => {
if(location.href!==__EXPECTED_URL__ || document.querySelector('input[type="password"]'))return 'null';
const links=Array.from(document.querySelectorAll('link[rel~="stylesheet"]'));
const resources=performance.getEntriesByType('resource');
return JSON.stringify({scripts:document.scripts.length,styles:document.styleSheets.length,
links:links.length,loaded:links.filter(link=>link.sheet!==null).length,
jquery:typeof window.jQuery==='function',beangle:window.beangle!==null && typeof window.beangle==='object',
require:typeof window.require==='function',
script202:resources.filter(r=>r.initiatorType==='script' && r.responseStatus===202).length,
style202:resources.filter(r=>r.initiatorType==='link' && r.responseStatus===202).length});
})()''';
void recordSchoolPageState(DiagnosticLog log, String school, Object raw) {
dynamic value = raw;
try {
for (var i = 0; i < 2 && value is String; i++) {
value = jsonDecode(value);
}
if (value is! Map) return;
const counts = [
'scripts',
'styles',
'links',
'loaded',
'script202',
'style202',
];
const libraries = ['jquery', 'beangle', 'require'];
if (counts.any(
(key) => value[key] is! int || value[key] < 0 || value[key] > 100000,
) ||
libraries.any((key) => value[key] is! bool)) {
return;
}
for (final key in counts) {
if ((key == 'script202' || key == 'style202') && value[key] == 0) {
continue;
}
log.add(
'school_asset_state',
fields: {
'school': school,
'code': key == 'script202'
? 'script_202'
: key == 'style202'
? 'stylesheet_202'
: key,
'count': value[key] as int,
},
);
}
if (school == 'undergraduate') {
for (final key in libraries) {
log.add(
'school_library_state',
fields: {'school': school, 'code': key, 'result': value[key] as bool},
);
}
}
} catch (_) {
/* Diagnostics must not interfere with the school page. */
}
}
Future<void> recordSchoolPageDiagnostics(
WebViewController web,
String school,
String url, {
bool Function()? isCurrent,
}) async {
final uri = Uri.tryParse(url);
if (uri == null ||
uri.scheme != 'https' ||
!const {'eams.uestc.edu.cn', 'yjsjy.uestc.edu.cn'}.contains(uri.host)) {
return;
}
try {
if (isCurrent?.call() == false) return;
if (await web.currentUrl() != url || isCurrent?.call() == false) return;
final raw = await web.runJavaScriptReturningResult(
schoolPageDiagnosticsScript.replaceAll(
'__EXPECTED_URL__',
jsonEncode(url),
),
);
if (await web.currentUrl() != url || isCurrent?.call() == false) {
return;
}
recordSchoolPageState(DiagnosticLog.instance, school, raw);
} catch (_) {
/* No original exceptions or page data enter the log. */
}
}