Publish nexusphp v1.0.5
This commit is contained in:
@@ -195,6 +195,83 @@ function parseIndexStats(indexHtml) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 邮箱提取 ----
|
||||||
|
// 资料页的「邮箱」值单元格往往不只有邮箱本身 (PTTIME: 格内还混着 "UID:98067" / "📝修改此项" / 🔐 标记,
|
||||||
|
// 且整页还有捐赠/页脚链接), 不同 NexusPHP 变体标签可能是 邮箱/电子邮件/電子郵件/Email,
|
||||||
|
// 值可能是 <a href="mailto:"> 也可能是纯文本, 甚至被反爬混淆成 "xxx [at] qq [dot] com"。
|
||||||
|
// 故按「标签行定位 → 值内 mailto → 值文本 → 整页 mailto → 标签邻近文本」逐级提取:
|
||||||
|
// 既不会抓到页脚的联系邮箱, 也不会因标签与值不在同一行/大小写 mailto 而漏抓。
|
||||||
|
var EMAIL_PATTERN = '[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}';
|
||||||
|
var EMAIL_LABELS = '(?:邮箱|电子邮件|電子郵件|邮件|邮件地址|邮箱地址|E-?mail)';
|
||||||
|
|
||||||
|
// 反混淆: "xxx [at] qq [dot] com" / "xxx (at) qq (dot) com" / "xxx at qq dot com"
|
||||||
|
function deobfuscateEmail(s) {
|
||||||
|
return String(s || '')
|
||||||
|
.replace(/\s*[\[({]\s*(?:at|@)\s*[\])}]\s*/gi, '@')
|
||||||
|
.replace(/\s*[\[({]\s*dot\s*[\])}]\s*/gi, '.')
|
||||||
|
.replace(/\s+(?:at|@)\s+/gi, '@')
|
||||||
|
.replace(/\s+dot\s+/gi, '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从一段文本里取第一个邮箱 (先直取, 失败再反混淆后取)
|
||||||
|
function firstEmailIn(s) {
|
||||||
|
if (!s) return null;
|
||||||
|
var m = String(s).match(new RegExp(EMAIL_PATTERN));
|
||||||
|
if (m) return m[0];
|
||||||
|
var m2 = deobfuscateEmail(s).match(new RegExp(EMAIL_PATTERN));
|
||||||
|
return m2 ? m2[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从节点下的 <a href="mailto:"> 取邮箱 (大小写不敏感 + URL 解码 + 去掉 ?subject= 等参数)
|
||||||
|
function emailFromLinks(node) {
|
||||||
|
if (!node || typeof node.queryList !== 'function') return null;
|
||||||
|
var links = node.queryList('a') || [];
|
||||||
|
for (var i = 0; i < links.length; i++) {
|
||||||
|
var href = (typeof links[i].attr === 'function' ? links[i].attr('href') : null) || '';
|
||||||
|
var m = String(href).match(/^\s*mailto:\s*(\S+)/i);
|
||||||
|
if (!m) continue;
|
||||||
|
var addr = m[1].split('?')[0];
|
||||||
|
try { addr = decodeURIComponent(addr); } catch (e) { /* 非法转义: 保持原值 */ }
|
||||||
|
var hit = firstEmailIn(addr);
|
||||||
|
if (hit) return hit;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从单个单元格取邮箱: 先 mailto 链接, 再单元格文本
|
||||||
|
function emailFromCell(cell) {
|
||||||
|
if (!cell) return null;
|
||||||
|
return emailFromLinks(cell) || firstEmailIn(typeof cell.text === 'function' ? cell.text() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractEmail(doc, text) {
|
||||||
|
// 1) 资料表格: 定位标签为「邮箱/邮件/Email」的行, 取右侧值单元格 (最准确)
|
||||||
|
var rows = (typeof doc.queryList === 'function' ? doc.queryList('tr') : null) || [];
|
||||||
|
for (var i = 0; i < rows.length; i++) {
|
||||||
|
var cells = rows[i].queryList('td') || [];
|
||||||
|
if (cells.length < 2) continue;
|
||||||
|
var label = (cells[0].text() || '').replace(/[\s::]/g, '');
|
||||||
|
if (!label) continue;
|
||||||
|
if (!new RegExp('^' + EMAIL_LABELS + '$', 'i').test(label)) continue;
|
||||||
|
for (var c = 1; c < cells.length; c++) {
|
||||||
|
var hit = emailFromCell(cells[c]);
|
||||||
|
if (hit) return hit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2) 整页 mailto 链接 (标签行解析不到时的兜底, 例如标签与值不在同一 tr)
|
||||||
|
var byLink = emailFromLinks(doc);
|
||||||
|
if (byLink) return byLink;
|
||||||
|
// 3) 整页文本: 标签后紧跟邮箱
|
||||||
|
var em = String(text || '').match(new RegExp(EMAIL_LABELS + '\\s*[::]?\\s*(' + EMAIL_PATTERN + ')', 'i'));
|
||||||
|
if (em) return em[1];
|
||||||
|
// 4) 标签与值之间夹了别的内容 (PTTIME: "邮箱27800734@qq.comUID:98067🔐") 时, 在标签后 120 字符内找
|
||||||
|
var near = String(text || '').match(new RegExp(EMAIL_LABELS + '[^\\n]{0,120}?(' + EMAIL_PATTERN + ')', 'i'));
|
||||||
|
if (near) return near[1];
|
||||||
|
// 5) 反混淆兜底 (邮箱被拆成 "xxx [at] qq [dot] com")
|
||||||
|
var obf = deobfuscateEmail(text).match(new RegExp(EMAIL_LABELS + '[^\\n]{0,160}?(' + EMAIL_PATTERN + ')', 'i'));
|
||||||
|
return obf ? obf[1] : null;
|
||||||
|
}
|
||||||
|
|
||||||
// 解析 userdetails.php 账号资料:邮箱/等级/注册日期/最近动向/做种积分/封存状态 + uid/用户名
|
// 解析 userdetails.php 账号资料:邮箱/等级/注册日期/最近动向/做种积分/封存状态 + uid/用户名
|
||||||
// 注意:userdetails 的字段在 #info_block 之外的资料表格中,故用整页 body 文本解析
|
// 注意:userdetails 的字段在 #info_block 之外的资料表格中,故用整页 body 文本解析
|
||||||
function parseUserDetails(userHtml) {
|
function parseUserDetails(userHtml) {
|
||||||
@@ -202,20 +279,7 @@ function parseUserDetails(userHtml) {
|
|||||||
var body = doc.querySelector('body') || doc;
|
var body = doc.querySelector('body') || doc;
|
||||||
var text = body.text() || userHtml;
|
var text = body.text() || userHtml;
|
||||||
|
|
||||||
var email = null;
|
var email = extractEmail(doc, text);
|
||||||
// DOM 优先:从 <a href="mailto:"> 提取最准确
|
|
||||||
var mailLink = doc.querySelector('a[href*="mailto:"]');
|
|
||||||
if (mailLink) {
|
|
||||||
var href = mailLink.attr('href') || '';
|
|
||||||
var mm = href.match(/mailto:([\w.+-]+@[\w.-]+\.\w+)/i);
|
|
||||||
if (mm) email = mm[1];
|
|
||||||
else email = mailLink.text();
|
|
||||||
}
|
|
||||||
// 退路:正则从 body text 搜
|
|
||||||
if (!email) {
|
|
||||||
var em = text.match(/(?:邮箱|Email|E-mail)\s*[::]?\s*([\w.+-]+@[\w.-]+\.\w+)/i);
|
|
||||||
if (em) email = em[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等级:DOM 解析,遍历每行 tr,找 label td 文本为 等级/级别/Class 的行,取其右边 td 的内容
|
// 等级:DOM 解析,遍历每行 tr,找 label td 文本为 等级/级别/Class 的行,取其右边 td 的内容
|
||||||
var classLevel = null;
|
var classLevel = null;
|
||||||
@@ -259,8 +323,29 @@ function parseUserDetails(userHtml) {
|
|||||||
if (!isNaN(sv)) seedingScore = sv;
|
if (!isNaN(sv)) seedingScore = sv;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 封存/封禁检测
|
// 封存/封禁检测:优先看「账号状态/用户状态」行的取值,其次认状态类表述 ("已封存"/"已封禁"/"封存中")。
|
||||||
var isSealed = /封存|帳號已?被封|账号已?被封/.test(text);
|
// 排除 pttime 个人中心的干扰文本:操作行的「封存用户」按钮、"封存400天…自动封禁" 说明,
|
||||||
|
// 以及「已封禁次数」计数行 —— 否则正常账号会被误判为已封存。
|
||||||
|
var isSealed = false;
|
||||||
|
var statusRows = doc.queryList('tr') || [];
|
||||||
|
for (var s = 0; s < statusRows.length && !isSealed; s++) {
|
||||||
|
var stCells = statusRows[s].queryList('td') || [];
|
||||||
|
if (stCells.length < 2) continue;
|
||||||
|
var stLabel = (stCells[0].text() || '').replace(/[\s::]/g, '');
|
||||||
|
if (!/^(?:账号状态|帐号状态|用戶狀態|用户状态|状态|狀態)$/.test(stLabel)) continue;
|
||||||
|
if (/封存|封禁|停用|禁用/.test(stCells[1].text() || '')) isSealed = true;
|
||||||
|
}
|
||||||
|
if (!isSealed) {
|
||||||
|
var sealedRe = /已(?:被|经|經)?\s*(?:封存|封禁)([\s\S]{0,2})/g;
|
||||||
|
var sm;
|
||||||
|
while ((sm = sealedRe.exec(text)) !== null) {
|
||||||
|
// "已封禁次数" 是计数行, 不代表账号当前被封
|
||||||
|
if (!/^次/.test(sm[1] || '')) { isSealed = true; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 末级兜底只认"封存中/封禁中"这类进行中状态; 不再写 "账号…封" 这类宽松模式 ——
|
||||||
|
// pttime 个人中心的「自ban账号 / 封禁用户」按钮文本会命中它, 造成正常账号误判。
|
||||||
|
if (!isSealed) isSealed = /封(?:存|禁)中/.test(text);
|
||||||
|
|
||||||
var identity = parseIdentity(doc.querySelector('#info_block') || doc);
|
var identity = parseIdentity(doc.querySelector('#info_block') || doc);
|
||||||
return {
|
return {
|
||||||
@@ -275,6 +360,23 @@ function parseUserDetails(userHtml) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 访客视图(会话失效 / Cookie 域名不匹配 / 未登录)识别:
|
||||||
|
// NexusPHP 对未登录访客只给公开字段(等级、注册日期),邮箱等「上锁」字段整个隐藏,
|
||||||
|
// 若照常当成功处理,表现就是「同步成功但邮箱一直为空」且没有任何失败样本可排查。
|
||||||
|
// 判据:页面出现登录表单,或出现"请先登录/登录后查看"等提示。
|
||||||
|
function looksLikeGuestView(userHtml) {
|
||||||
|
try {
|
||||||
|
var d = html.parse(userHtml);
|
||||||
|
if (d.querySelector('form[action*="login"]')) return true;
|
||||||
|
if (d.querySelector('input[type="password"]')) return true;
|
||||||
|
var b = d.querySelector('body');
|
||||||
|
var t = (b && typeof b.text === 'function') ? b.text() : userHtml;
|
||||||
|
return /(?:请先登录|請先登入|需要登录|登录后查看|您尚未登录|尚未登录|未登录|Not logged in|Please log in)/i.test(t || '');
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ================= 能力: index(用户统计)=================
|
// ================= 能力: index(用户统计)=================
|
||||||
// ctx: { baseUrl, ... }。解析首页信息块,返回当前登录用户统计。
|
// ctx: { baseUrl, ... }。解析首页信息块,返回当前登录用户统计。
|
||||||
function index(input) {
|
function index(input) {
|
||||||
@@ -368,6 +470,11 @@ function sync(input) {
|
|||||||
if (profile && profile.email === null && profile.classLevel === null && profile.registerDate === null) {
|
if (profile && profile.email === null && profile.classLevel === null && profile.registerDate === null) {
|
||||||
profile = null;
|
profile = null;
|
||||||
}
|
}
|
||||||
|
// 访客视图: 只拿到公开字段而上锁字段(邮箱)被隐藏 —— 不算解析成功, 否则会静默回传 email=null,
|
||||||
|
// 表现为"同步一直成功但邮箱始终为空"且无样本可排查。
|
||||||
|
if (profile && profile.email === null && looksLikeGuestView(userHtml)) {
|
||||||
|
profile = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
profile = null;
|
profile = null;
|
||||||
@@ -423,10 +530,20 @@ function parse(content, url) {
|
|||||||
// 签到(signInPath 默认 /attendance.php,可在站点 definition 中覆盖,如 BTSCHOOL: /index.php?action=addbonus)
|
// 签到(signInPath 默认 /attendance.php,可在站点 definition 中覆盖,如 BTSCHOOL: /index.php?action=addbonus)
|
||||||
// 成功时若响应页含 #info_block (含魔力值) 则一并返回 bonus 供服务端更新 Site.Statistics.Bonus;
|
// 成功时若响应页含 #info_block (含魔力值) 则一并返回 bonus 供服务端更新 Site.Statistics.Bonus;
|
||||||
// 站点页面不含 / 解析不到时缺省不返回该字段, 服务端不处理 (report §四 签到奖励自动更新).
|
// 站点页面不含 / 解析不到时缺省不返回该字段, 服务端不处理 (report §四 签到奖励自动更新).
|
||||||
|
//
|
||||||
|
// 站点特化:
|
||||||
|
// URL 含 {uid} 占位符 → 用 ctx.uid (数据库已存) 替换 → GET 请求 (PTTime: /attendance.php?type=sign&uid={uid});
|
||||||
|
// 其他情况一律 POST (兼容 BTSCHOOL /index.php?action=addbonus 等带查询参数的 POST 签到).
|
||||||
function sign(input) {
|
function sign(input) {
|
||||||
var ctx = __asCtx(input);
|
var ctx = __asCtx(input);
|
||||||
var signPath = ctx.signInPath || '/attendance.php';
|
var signPath = ctx.signInPath || '/attendance.php';
|
||||||
var res = http.post(signPath);
|
var method = 'POST';
|
||||||
|
if (signPath.indexOf('{uid}') >= 0) {
|
||||||
|
signPath = signPath.replace('{uid}', (ctx && ctx.uid) || '');
|
||||||
|
method = 'GET';
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = method === 'GET' ? http.get(signPath) : http.post(signPath);
|
||||||
if (res && res.indexOf('成功') >= 0) {
|
if (res && res.indexOf('成功') >= 0) {
|
||||||
var out = { signed: true, message: '签到成功' };
|
var out = { signed: true, message: '签到成功' };
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user