Files
op-packages/luci-theme-liquid/root/usr/share/ucode/luci/controller/liquid.uc
T

272 lines
9.8 KiB
Ucode
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/* Liquid theme: save theme config (mode / accent / bing) directly.
Called via XHR POST to /cgi-bin/luci/admin/system/liquid/save_config
(like luci-app-pushbot's save_config), bypassing the uci rpc which is
unreliable across LuCI versions. Writes /etc/config/liquid with fs
(system()/uci CLI are not available in the ucode dispatcher sandbox). */
import * as fs from 'fs';
import { popen } from 'fs';
return {
act_version: function() {
let ver = "";
/* mtime 校验 + /tmp 缓存(与 header/footer 模板同一套机制),避免每次 fork */
let db_file = '/lib/apk/db/installed';
let db_mtime = 0;
let st = fs.stat(db_file);
if (st && st.type == 'file') {
db_mtime = st.mtime;
} else {
db_file = '/usr/lib/opkg/status';
st = fs.stat(db_file);
if (st && st.type == 'file')
db_mtime = st.mtime;
}
if (db_mtime > 0) {
let cm = match(fs.readfile('/tmp/liquid-version.cache') ?? '', /^(\d+) (.*)$/);
if (cm !== null && sprintf('%d', db_mtime) == cm[1])
ver = cm[2];
}
if (ver == "") {
/* apk (OpenWrt 24.10+): /lib/apk/db/installed */
let f = popen("awk '/^P:luci-theme-liquid$/{f=1;next} f&&/^V:/{print substr($0,3);exit}' " + db_file + " 2>/dev/null", "r");
if (f) { ver = replace(f.read("all"), /\s+/, ""); f.close(); }
/* opkg (legacy): /usr/lib/opkg/status */
if (ver == "") {
f = popen("awk '/^Package: luci-theme-liquid$/{f=1;next} f&&/^Version:/{print $2;exit}' /usr/lib/opkg/status 2>/dev/null", "r");
if (f) { ver = replace(f.read("all"), /\s+/, ""); f.close(); }
}
if (ver != "" && db_mtime > 0)
fs.writefile('/tmp/liquid-version.cache', sprintf('%d %s', db_mtime, ver));
}
http.prepare_content("application/json");
http.write_json({ version: ver });
},
act_save_config: function() {
http.prepare_content("application/json");
let body;
try { body = http.content(); } catch { body = null; }
if (!body) {
http.write_json({ ok: false, error: "no data" });
return;
}
let data;
try { data = json(body); } catch { data = null; }
if (type(data) != 'object') {
http.write_json({ ok: false, error: "invalid json" });
return;
}
/* 白名单字段 + 值校验 */
let writes = {};
for (let k in data) {
let v = sprintf("%s", data[k]);
if (k == 'mode' && match(v, /^(light|dark|auto)$/))
writes['mode'] = v;
else if (k == 'accent' && match(v, /^(blue|magenta|amber|purple|lime|custom)$/))
writes['accent'] = v;
else if (k == 'accent_custom' && match(v, /^#[0-9a-fA-F]{6}$/))
writes['accent_custom'] = v;
else if (k == 'bing' && match(v, /^(0|1)$/))
writes['bing'] = v;
else if (k == 'glass_opacity') {
let n = int(v, 10);
if (n >= 0 && n <= 100)
writes['glass_opacity'] = sprintf('%d', n);
}
}
let cfg_path = '/etc/config/liquid';
let cfg = fs.readfile(cfg_path) ?? '';
let lines = split(cfg, '\n');
let out = [];
if (!match(cfg, /config theme/))
push(out, "config theme 'theme'");
let inSection = false;
for (let i = 0; i < length(lines); i++) {
let line = lines[i];
if (match(line, /^config/))
inSection = true;
let m = match(line, /^[ \t]*option[ \t]+([a-z_]+)[ \t]+/);
if (inSection && m !== null && m[1] in writes) {
push(out, sprintf("\toption %s '%s'", m[1], writes[m[1]]));
delete writes[m[1]];
continue;
}
/* 丢弃纯空行(避免历史累积空行污染) */
if (trim(line) == '')
continue;
push(out, line);
}
/* 追加尚未出现的 option */
for (let k in writes)
push(out, sprintf("\toption %s '%s'", k, writes[k]));
fs.writefile(cfg_path, join('\n', out) + '\n');
http.write_json({ ok: true, saved: true });
},
/* ── OTA: detect package manager (apk vs opkg),与 pushbot 同款 ── */
act_detect_pkgmgr: function() {
let mgr = "opkg";
let f = popen("command -v apk 2>/dev/null", "r");
if (f) { let o = f.read("all"); f.close(); if (o && length(replace(o, /\s+/, "")) > 0) mgr = "apk"; }
http.prepare_content("application/json");
http.write_json({ pkgmgr: mgr });
},
/* ── OTA: 后台下载 Release 包(tag 方案 luci-theme-liquid-vX.Y-rN),与 pushbot 同款重试逻辑 ── */
act_ota_download: function() {
let ver = http.formvalue("ver") ?? "";
let rel = http.formvalue("rel") ?? "";
if (ver == "" || rel == "") {
http.prepare_content("application/json");
http.write_json({ ok: false, error: "missing ver/rel" });
return;
}
/* 防注入:只放行数字与点 */
ver = replace(ver, /[^0-9.]/g, "");
rel = replace(rel, /[^0-9]/g, "");
if (ver == "" || rel == "") {
http.prepare_content("application/json");
http.write_json({ ok: false, error: "invalid ver/rel" });
return;
}
/* 检测包管理器 */
let mgr = "opkg";
let f0 = popen("command -v apk 2>/dev/null", "r");
if (f0) { let o = f0.read("all"); f0.close(); if (o && length(replace(o, /\s+/, "")) > 0) mgr = "apk"; }
/* 组装下载地址(主题无 po/i18n 子包,单文件) */
let base = "https://github.com/zzsj0928/luci-theme-liquid/releases/download/luci-theme-liquid-v" + ver + "-r" + rel + "/";
let files;
if (mgr == "apk") {
files = ["luci-theme-liquid-" + ver + "-r" + rel + ".apk"];
} else {
files = ["luci-theme-liquid_" + ver + "-r" + rel + "_all.ipk"];
}
/* 进度文件 */
let pfile = "/tmp/liquid/ota_progress";
system("mkdir -p /tmp/liquid && echo '0' > " + pfile + " 2>/dev/null");
/* 后台下载脚本:每个文件最多重试 3 次,>10KB 视为有效 */
let dl_script = "#!/bin/sh\n"
+ "PFILE='" + pfile + "'\n"
+ "BASE='" + base + "'\n"
+ "MAX_RETRY=3\n"
+ "TOTAL=" + length(files) + "\n"
+ "OK=0\n"
+ "for f in " + join(" ", files) + "; do\n"
+ " URL=\"${BASE}${f}\"\n"
+ " DEST=\"/tmp/${f}\"\n"
+ " ATTEMPT=0\n"
+ " while [ $ATTEMPT -lt $MAX_RETRY ]; do\n"
+ " ATTEMPT=$((ATTEMPT+1))\n"
+ " curl -k -Lf --connect-timeout 15 --max-time 120 -o \"${DEST}\" \"${URL}\" 2>/dev/null\n"
+ " if [ $? -eq 0 ] && [ -s \"${DEST}\" ] && [ $(wc -c < \"${DEST}\") -gt 10000 ]; then\n"
+ " OK=$((OK+1))\n"
+ " echo \"$((OK * 100 / TOTAL))\" > \"${PFILE}\"\n"
+ " [ $OK -lt $TOTAL ] && sleep 1\n"
+ " break\n"
+ " fi\n"
+ " rm -f \"${DEST}\"\n"
+ " sleep 2\n"
+ " done\n"
+ "done\n"
+ "if [ $OK -eq $TOTAL ]; then\n"
+ " sleep 1\n"
+ " echo 'done' > \"${PFILE}\"\n"
+ "else\n"
+ " echo 'fail' > \"${PFILE}\"\n"
+ "fi\n";
fs.writefile("/tmp/liquid/ota_download.sh", dl_script);
system("chmod +x /tmp/liquid/ota_download.sh && /tmp/liquid/ota_download.sh &");
http.prepare_content("application/json");
http.write_json({ ok: true });
},
/* ── OTA: 轮询下载进度(0-100 / done / fail) ── */
act_ota_download_progress: function() {
let pfile = "/tmp/liquid/ota_progress";
let progress = "0";
let f = popen("cat " + pfile + " 2>/dev/null || echo '0'", "r");
if (f) { progress = replace(f.read("all"), /\s+/, ""); f.close(); }
if (progress == "") progress = "0";
http.prepare_content("application/json");
http.write_json({ progress: progress });
},
/* ── OTA: 安装已下载的包(apk/opkg 双支持) ── */
act_ota_install: function() {
let mgr = "opkg";
let f0 = popen("command -v apk 2>/dev/null", "r");
if (f0) { let o = f0.read("all"); f0.close(); if (o && length(replace(o, /\s+/, "")) > 0) mgr = "apk"; }
let ifile = "/tmp/liquid/ota_install.log";
let trust = "";
let cmd;
if (mgr == "apk") {
/* 装前检测 Zed 自签公钥:无则写入(添加不覆盖),随后安装
免 --allow-untrusted(老设备/新用户首次走 OTA 时设备上还没
有公钥,靠这步建立信任;包内也带同一公钥
root/etc/apk/keys/zed-openwrt-apk.pem,装完即常驻信任列表,
仅装本主题的用户同样获得信任)。 */
trust = "[ -f /etc/apk/keys/zed-openwrt-apk.pem ] || { mkdir -p /etc/apk/keys; "
+ "printf '%s" + "\\n" + "' '-----BEGIN PUBLIC KEY-----' "
+ "'MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE16+nzzY9Lx5wvzZoWs/18vZxsNZD' "
+ "'jv+CqECJLUj+fA7J228Iu13DVUO8CK9jQyLHtqkw0f4/X2bKLlLiz281zQ==' "
+ "'-----END PUBLIC KEY-----' > /etc/apk/keys/zed-openwrt-apk.pem; }; ";
cmd = "apk add /tmp/luci-theme-liquid-*.apk";
} else {
/* opkg 同版本会 up to date 跳过,需 --force-reinstall 覆盖 */
cmd = "opkg install --force-reinstall /tmp/luci-theme-liquid_*.ipk";
}
/* 后台安装 + 结果标记(postinst 自动清 luci 缓存并 reload rpcd)。
world 哈希锁清理【装前 + 装后各一次】:
- 装前:即使安装链中途被杀(重启/OOM),锁也已经清掉,
不会留下"中毒 world 卡死后续所有 apk 事务"的状态
- 装后:本次安装(本地文件)自己又会写一把新锁,再清一次
降级为裸包名(语义等价"保持安装",且这些包不在官方源、
无被替换风险)。
整链必须放进单个 ( ... ) & 后台执行:否则 system() 会同步
等安装结束,阻塞 rpcd 处理器(全站请求卡住)。 */
let heal = "[ -f /etc/apk/world ] && sed -i '/></ s/>.*$//' /etc/apk/world; ";
let install_cmd = "( "
+ trust
+ heal
+ cmd + " > " + ifile + " 2>&1; "
+ "RC=$?; "
+ heal
+ "if [ $RC -eq 0 ]; then echo 'ok' >> " + ifile + "; "
+ "else echo 'fail' >> " + ifile + "; fi ) &";
system("mkdir -p /tmp/liquid && " + install_cmd);
http.prepare_content("application/json");
http.write_json({ ok: true, pkgmgr: mgr });
},
/* ── OTA: 清理已下载的包与进度文件 ── */
act_ota_clear: function() {
let patterns = [
"/tmp/luci-theme-liquid-*.apk",
"/tmp/luci-theme-liquid_*.ipk",
"/tmp/liquid/ota_progress",
"/tmp/liquid/ota_install.log",
"/tmp/liquid/ota_download.sh"
];
system("rm -f " + join(" ", patterns) + " 2>/dev/null");
http.prepare_content("application/json");
http.write_json({ ok: true });
}
};