import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../../data/json_schedule_import.dart'; import 'school_navigation_policy.dart'; import 'school_page_adapter.dart'; enum SchoolConnectionState { signedOut, authenticating, connected, expired, unknown, } class SchoolSession extends ChangeNotifier { SchoolSession() { web = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..setNavigationDelegate( NavigationDelegate( onNavigationRequest: (request) { final uri = Uri.tryParse(request.url); if (uri == null || !SchoolNavigationPolicy.canNavigate( uri, isMainFrame: request.isMainFrame, )) { message = '学校跳转到了其他地址,已停止自动交接登录信息。'; notifyListeners(); return NavigationDecision.prevent; } return NavigationDecision.navigate; }, onPageStarted: (_) { _pageGeneration++; _reader.invalidate(); pendingDraft = null; if (!hasOpened) { return; } _loading = true; formReady = false; notifyListeners(); }, onPageFinished: _loaded, onWebResourceError: (error) { if (error.isForMainFrame == true) { _loading = false; message = '学校页面暂时未能加载,请检查网络或使用统一认证入口。'; notifyListeners(); } }, onHttpError: (error) async { final current = Uri.tryParse(await web.currentUrl() ?? ''); if (!hasOpened || error.request == null || error.request!.uri != current) { return; } _loading = false; message = '学校页面请求未完成,可稍后重试。'; notifyListeners(); }, ), ); } late final WebViewController web; final _reader = SchoolReader(SchoolPageAdapter()); SchoolConnectionState connectionState = SchoolConnectionState.signedOut; bool _loading = false, _submitting = false, hasOpened = false, formReady = false; bool get busy => _loading || _submitting || _reader.busy; int _pageGeneration = 0; ImportDraft? pendingDraft; String message = '请通过学校统一身份认证登录。'; String get statusLabel => switch (connectionState) { SchoolConnectionState.signedOut => '未登录', SchoolConnectionState.authenticating => '请在学校页面完成认证', SchoolConnectionState.connected => '学校认证已确认', SchoolConnectionState.expired => '学校登录已过期', SchoolConnectionState.unknown => '学校页面已打开 · 等待完整课表', }; static final authUrl = Uri.parse( 'https://idas.uestc.edu.cn/authserver/login', ); static final timetableUrl = Uri.parse( 'https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/1', ); Future open() => openTimetable(); Future openTimetable() async { hasOpened = true; _loading = true; pendingDraft = null; message = '正在打开研究生课表页面…'; notifyListeners(); try { await web.loadRequest(timetableUrl); } catch (_) { _loading = false; message = '学校课表页面无法打开,请检查校园网络。'; notifyListeners(); } } Future openAuthentication() async { hasOpened = true; _loading = true; message = '正在准备学校认证…'; notifyListeners(); try { await web.loadRequest(authUrl); } catch (_) { _loading = false; message = '学校认证页面暂时不可用。'; notifyListeners(); } } static const _readyScript = """(() => { if (location.origin !== 'https://idas.uestc.edu.cn' || location.pathname !== '/authserver/login') return false; const f = document.querySelector('.login-main #pwdFromId'); if (!f || f.method.toLowerCase() !== 'post') return false; const action = new URL(f.action, location.href); return action.origin === location.origin && action.pathname === '/authserver/login' && !!f.querySelector('#username') && !!f.querySelector('#password') && !!f.querySelector('#saltPassword') && !!f.querySelector('#pwdEncryptSalt') && !!f.querySelector('#login_submit') && typeof window.startLogin === 'function' && typeof window.encryptPassword === 'function' && typeof window.checkUserCaptcha === 'function'; })()"""; bool _truth(Object value) => value == true || value.toString() == 'true'; Future _loaded(String url) async { if (!hasOpened) { return; } _loading = false; final generation = _pageGeneration; final uri = Uri.tryParse(url); if (uri != null && SchoolNavigationPolicy.canInjectCredentials(uri)) { connectionState = connectionState == SchoolConnectionState.connected ? SchoolConnectionState.expired : SchoolConnectionState.authenticating; try { final ready = _truth( await web.runJavaScriptReturningResult(_readyScript), ); if (!hasOpened || generation != _pageGeneration) return; formReady = ready; } catch (_) { if (!hasOpened || generation != _pageGeneration) return; formReady = false; } message = formReady ? '可以使用应用界面登录;验证码会在学校页面完成。' : '请使用学校原页面完成认证。'; } else if (uri != null && SchoolNavigationPolicy.isTimetable(uri)) { connectionState = SchoolConnectionState.unknown; message = '已打开课表页面,正在核对课程结构。'; try { final draft = await readTimetable(); if (!hasOpened || generation != _pageGeneration) return; pendingDraft = draft; connectionState = SchoolConnectionState.connected; } catch (error) { if (!hasOpened || generation != _pageGeneration) return; message = error is UnsupportedSchoolLayout ? error.toString() : '课表读取未完成,已有课表保持不变。'; } } else { connectionState = SchoolConnectionState.unknown; message = '可点击右上角日历打开学校课表。'; } notifyListeners(); } Future submitCredentials(String username, String password) async { if (busy) return; _submitting = true; message = '正在交接学校认证…'; notifyListeners(); try { final url = Uri.tryParse(await web.currentUrl() ?? ''); if (url == null || !SchoolNavigationPolicy.canInjectCredentials(url) || !_truth(await web.runJavaScriptReturningResult(_readyScript))) { throw StateError('学校表单尚未准备好,请使用学校页面登录'); } final result = await web.runJavaScriptReturningResult('''(() => { if (!($_readyScript)) return false; const f = document.querySelector('.login-main #pwdFromId'); const u = f.querySelector('#username'), p = f.querySelector('#password'); u.value = ${jsonEncode(username)}; p.value = ${jsonEncode(password)}; p.removeAttribute('readonly'); u.dispatchEvent(new Event('input', {bubbles:true})); p.dispatchEvent(new Event('input', {bubbles:true})); window.checkUserCaptcha(); return true; })()'''); if (!_truth(result)) throw StateError('学校表单已变化,请使用学校页面登录'); var settled = false; for (var i = 0; i < 20; i++) { if (_truth( await web.runJavaScriptReturningResult( '!!window.jQuery && window.jQuery.active === 0', ), )) { settled = true; break; } await Future.delayed(const Duration(milliseconds: 200)); } final current = Uri.tryParse(await web.currentUrl() ?? ''); if (!settled || current == null || !SchoolNavigationPolicy.canInjectCredentials(current)) { message = '请在学校页面继续完成验证。'; return; } await web.runJavaScript('''(() => { if (!($_readyScript)) return; window.startLogin(document.querySelector('.login-main #pwdFromId #login_submit')); })()'''); message = '已交给学校处理;如有验证码,请在学校页面完成。'; } catch (_) { message = '登录交接未完成,请使用学校页面继续。'; } finally { _submitting = false; notifyListeners(); } } Future readTimetable() async { final uri = Uri.tryParse(await web.currentUrl() ?? ''); if (uri == null) throw UnsupportedSchoolLayout(); return _reader.read(uri, () async { final raw = await web.runJavaScriptReturningResult( 'JSON.stringify(document.documentElement.outerHTML)', ); final value = raw is String ? raw : raw.toString(); try { final decoded = jsonDecode(value); return decoded is String ? decoded : value; } catch (_) { return value; } }); } Future logout() async { _pageGeneration++; _reader.invalidate(); hasOpened = false; await WebViewCookieManager().clearCookies(); await web.clearCache(); await web.clearLocalStorage(); await web.loadHtmlString(''); _loading = false; hasOpened = false; pendingDraft = null; formReady = false; connectionState = SchoolConnectionState.signedOut; message = '已退出学校登录。'; notifyListeners(); } }