mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-02 23:43:56 +08:00
🌈 Sync 2026-10-01 13:09:37
This commit is contained in:
1 parent
dceb6de0e4
commit
889951b6d0
116 files changed
+6376
-1093
No files matched your search
@@ -37,22 +37,50 @@ function index()
|
||||
entry({"substore", "download"}, call("action_download"), nil)
|
||||
end
|
||||
|
||||
-- 表单字段统一取字符串。
|
||||
--
|
||||
-- LuCI 的 formvalue **不保证返回字符串**:同名字段提交多次时返回的是 table。
|
||||
-- 依据上游 luci/http.lua 的 urldecode_message_body:
|
||||
-- elseif what == parser.VALUE and name then
|
||||
-- local val = msg.params[name]
|
||||
-- if type(val) == "table" then val[#val+1] = ...
|
||||
-- elseif val ~= nil then msg.params[name] = { val, ... } -- ← 第二次出现变成 table
|
||||
-- 而 formvalue 原样返回 msg.params[name];上游 luadoc 也写着
|
||||
-- "@return HTTP input value or table of all input value"。
|
||||
--
|
||||
-- 本项目多处直接对返回值 :gsub / util.trim / urlencode,遇到 table 会抛
|
||||
-- "attempt to index a table value" → HTTP 500。攻击面不限于已登录用户:
|
||||
-- /substore/download 是**无需登录**的入口(供 Passwall / OpenClash 拉取),
|
||||
-- 对它 POST 一个重复的 token 字段即可触发。统一收敛后这类输入不再崩溃。
|
||||
--
|
||||
-- 取值语义:table 取最后一个(与「同名参数后者覆盖前者」的直觉一致),
|
||||
-- nil 保持 nil(post_ok 依赖这一点区分「无 token」与「空 token」)。
|
||||
--
|
||||
-- 注意:必须定义在 post_ok **之前** —— Lua 的 local function 只捕获定义时
|
||||
-- 已可见的局部变量,写在后面会让 post_ok 里的 fv 落到全局(nil)。
|
||||
local function fv(http, key)
|
||||
local v = http.formvalue(key)
|
||||
if type(v) == "table" then v = v[#v] end
|
||||
if v == nil then return nil end
|
||||
return tostring(v)
|
||||
end
|
||||
|
||||
local function post_ok()
|
||||
-- 拒绝缺失 CSRF token 的 POST
|
||||
local http = require("luci.http")
|
||||
return http.formvalue("token") ~= nil
|
||||
return fv(http, "token") ~= nil
|
||||
end
|
||||
|
||||
-- read cron fields from form, validate cron expression, return cron_enable cron_time
|
||||
local function read_cron_fields()
|
||||
local http = require("luci.http")
|
||||
local core = require("substore.core")
|
||||
local cron_enable = http.formvalue("cron_enable") or "0"
|
||||
local m = http.formvalue("cron_min") or "0"
|
||||
local h = http.formvalue("cron_hour") or "3"
|
||||
local dom = http.formvalue("cron_dom") or "*"
|
||||
local mon = http.formvalue("cron_mon") or "*"
|
||||
local dow = http.formvalue("cron_dow") or "*"
|
||||
local cron_enable = fv(http, "cron_enable") or "0"
|
||||
local m = fv(http, "cron_min") or "0"
|
||||
local h = fv(http, "cron_hour") or "3"
|
||||
local dom = fv(http, "cron_dom") or "*"
|
||||
local mon = fv(http, "cron_mon") or "*"
|
||||
local dow = fv(http, "cron_dow") or "*"
|
||||
local cron_time = table.concat({m,h,dom,mon,dow}, " ")
|
||||
if not core.cron_time_valid(cron_time) then
|
||||
cron_enable = "0"
|
||||
@@ -65,19 +93,21 @@ end
|
||||
-- read subscription level rules from form, return rules table
|
||||
local function read_rules_fields()
|
||||
local http = require("luci.http")
|
||||
local core = require("substore.core")
|
||||
local proto_list = {}
|
||||
for _, p in ipairs({"vmess","vless","trojan","shadowsocks","ssr","hysteria2","tuic","hysteria","wireguard","socks"}) do
|
||||
if http.formvalue("proto_filter_"..p) then proto_list[#proto_list+1]=p end
|
||||
-- 协议清单来自 core.RULE_PROTOS,与表单模板共用一份(见该常量的说明)
|
||||
for _, p in ipairs(core.RULE_PROTOS) do
|
||||
if fv(http, "proto_filter_"..p) then proto_list[#proto_list+1]=p end
|
||||
end
|
||||
local rules_enable = http.formvalue("rules_enable") or "0"
|
||||
local rules_enable = fv(http, "rules_enable") or "0"
|
||||
if rules_enable ~= "1" then rules_enable = "0" end
|
||||
return {
|
||||
rules_enable = rules_enable,
|
||||
proto_filter = table.concat(proto_list, ","),
|
||||
keyword_include = http.formvalue("keyword_include") or "",
|
||||
keyword_exclude = http.formvalue("keyword_exclude") or "",
|
||||
dedup = http.formvalue("dedup") or "0",
|
||||
rename_map = http.formvalue("rename_map") or "",
|
||||
keyword_include = fv(http, "keyword_include") or "",
|
||||
keyword_exclude = fv(http, "keyword_exclude") or "",
|
||||
dedup = fv(http, "dedup") or "0",
|
||||
rename_map = fv(http, "rename_map") or "",
|
||||
}
|
||||
end
|
||||
|
||||
@@ -85,11 +115,11 @@ function action_create()
|
||||
local http = require("luci.http")
|
||||
local core = require("substore.core")
|
||||
if post_ok() then
|
||||
local name = (http.formvalue("name") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local url = (http.formvalue("url") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local proxy_enable = http.formvalue("proxy_enable") or "0"
|
||||
local name = (fv(http, "name") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local url = (fv(http, "url") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local proxy_enable = fv(http, "proxy_enable") or "0"
|
||||
if proxy_enable ~= "1" then proxy_enable = "0" end
|
||||
local proxy = (http.formvalue("proxy") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local proxy = (fv(http, "proxy") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
if name == "" or url == "" then
|
||||
return back_to_list("名称和 URL 不能为空")
|
||||
end
|
||||
@@ -112,12 +142,12 @@ function action_save()
|
||||
local http = require("luci.http")
|
||||
local core = require("substore.core")
|
||||
if post_ok() then
|
||||
local id = http.formvalue("id") or ""
|
||||
local name = (http.formvalue("name") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local url = (http.formvalue("url") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local proxy_enable = http.formvalue("proxy_enable") or "0"
|
||||
local id = fv(http, "id") or ""
|
||||
local name = (fv(http, "name") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local url = (fv(http, "url") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local proxy_enable = fv(http, "proxy_enable") or "0"
|
||||
if proxy_enable ~= "1" then proxy_enable = "0" end
|
||||
local proxy = (http.formvalue("proxy") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local proxy = (fv(http, "proxy") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
if id == "" then return back_to_list("缺少订阅 ID") end
|
||||
if name == "" or url == "" then
|
||||
return back_to_list("名称和 URL 不能为空")
|
||||
@@ -141,9 +171,9 @@ function action_local_create()
|
||||
local http = require("luci.http")
|
||||
local core = require("substore.core")
|
||||
if post_ok() then
|
||||
local name = (http.formvalue("name") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local content = http.formvalue("content") or ""
|
||||
local local_mode = http.formvalue("local_mode") or "text"
|
||||
local name = (fv(http, "name") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local content = fv(http, "content") or ""
|
||||
local local_mode = fv(http, "local_mode") or "text"
|
||||
local rules = read_rules_fields()
|
||||
-- §19:空名称 / 空内容必须明确报错,不能无声创建无效订阅
|
||||
if name == "" then return back_to_list("名称不能为空") end
|
||||
@@ -164,10 +194,10 @@ function action_local_save()
|
||||
local http = require("luci.http")
|
||||
local core = require("substore.core")
|
||||
if post_ok() then
|
||||
local id = http.formvalue("id") or ""
|
||||
local name = (http.formvalue("name") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local content = http.formvalue("content") or ""
|
||||
local local_mode = http.formvalue("local_mode") or "text"
|
||||
local id = fv(http, "id") or ""
|
||||
local name = (fv(http, "name") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local content = fv(http, "content") or ""
|
||||
local local_mode = fv(http, "local_mode") or "text"
|
||||
local rules = read_rules_fields()
|
||||
if id == "" then return back_to_list("缺少订阅 ID") end
|
||||
if name == "" then return back_to_list("名称不能为空") end
|
||||
@@ -194,7 +224,11 @@ function action_delete()
|
||||
local http = require("luci.http")
|
||||
local core = require("substore.core")
|
||||
if post_ok() then
|
||||
core.remove(http.formvalue("id") or "")
|
||||
-- §18:删除失败必须让用户看见。core.remove 对「非法 ID / 订阅不存在」
|
||||
-- 返回 false(或 false, err),此前返回值被整个丢弃 —— 用户点了删除,
|
||||
-- 页面正常跳回,订阅却还在,看起来像「删了但没生效」。
|
||||
local ok, err = core.remove(fv(http, "id") or "")
|
||||
if not ok then return back_to_list(err or "删除失败:订阅不存在") end
|
||||
core.write_cron()
|
||||
end
|
||||
back_to_list()
|
||||
@@ -204,10 +238,10 @@ end
|
||||
-- 返回节点页。err 非空时把失败原因带到页面显示(§18:失败必须让用户看见,
|
||||
-- 不能「失败了却看起来像成功」)。与 back_to_list 同一套约定。
|
||||
local function back_to_nodes(http, err)
|
||||
local id = http.formvalue("id") or ""
|
||||
local id = fv(http, "id") or ""
|
||||
local qs = "?id=" .. luci.util.urlencode(id)
|
||||
for _, k in ipairs({ "proto", "keyword", "group", "sort", "desc" }) do
|
||||
local v = http.formvalue(k)
|
||||
local v = fv(http, k)
|
||||
if v and v ~= "" then qs = qs .. "&" .. k .. "=" .. luci.util.urlencode(v) end
|
||||
end
|
||||
if err ~= nil and tostring(err) ~= "" then
|
||||
@@ -222,9 +256,9 @@ function action_node_save()
|
||||
local core = require("substore.core")
|
||||
local parser = require("substore.parser")
|
||||
if post_ok() then
|
||||
local id = http.formvalue("id") or ""
|
||||
local idx = tonumber(http.formvalue("idx") or "")
|
||||
local content = http.formvalue("content") or ""
|
||||
local id = fv(http, "id") or ""
|
||||
local idx = tonumber(fv(http, "idx") or "")
|
||||
local content = fv(http, "content") or ""
|
||||
local nodes = core.read_nodes(id)
|
||||
-- §18:任何一条失败路径都必须把原因带回页面。
|
||||
-- 原来只在成功分支里做事,其余情况一律静默重定向,
|
||||
@@ -258,14 +292,18 @@ function action_node_delete()
|
||||
local http = require("luci.http")
|
||||
local core = require("substore.core")
|
||||
if post_ok() then
|
||||
local id = http.formvalue("id") or ""
|
||||
local idx_param = http.formvalue("idx") or ""
|
||||
if type(idx_param) == "table" then idx_param = table.concat(idx_param, ",") end
|
||||
local id = fv(http, "id") or ""
|
||||
local idx_param = fv(http, "idx") or ""
|
||||
local nodes = core.read_nodes(id)
|
||||
local idxs = {}
|
||||
for s in tostring(idx_param):gmatch("%d+") do
|
||||
idxs[#idxs + 1] = tonumber(s)
|
||||
end
|
||||
-- §18:什么都没删掉时必须说清楚,不能静默跳回列表。
|
||||
if #idxs == 0 then
|
||||
back_to_nodes(http, "未指定要删除的节点")
|
||||
return
|
||||
end
|
||||
-- 倒序删除,避免 table.remove 后下标偏移
|
||||
table.sort(idxs, function(a, b) return a > b end)
|
||||
local removed = false
|
||||
@@ -275,10 +313,16 @@ function action_node_delete()
|
||||
removed = true
|
||||
end
|
||||
end
|
||||
if removed and core.write_nodes(id, nodes) then
|
||||
core.save_meta(id, { node_count = #nodes })
|
||||
core.refresh_combos(id)
|
||||
if not removed then
|
||||
back_to_nodes(http, "节点不存在或已被删除")
|
||||
return
|
||||
end
|
||||
if not core.write_nodes(id, nodes) then
|
||||
back_to_nodes(http, "写入节点数据失败")
|
||||
return
|
||||
end
|
||||
core.save_meta(id, { node_count = #nodes })
|
||||
core.refresh_combos(id)
|
||||
end
|
||||
back_to_nodes(http)
|
||||
end
|
||||
@@ -293,9 +337,9 @@ function action_node_set_group()
|
||||
http.write(util.json_encode({ ok = false, err = "forbidden" }))
|
||||
return
|
||||
end
|
||||
local id = http.formvalue("id") or ""
|
||||
local idx = tonumber(http.formvalue("idx") or "")
|
||||
local group = util.trim(http.formvalue("group") or "")
|
||||
local id = fv(http, "id") or ""
|
||||
local idx = tonumber(fv(http, "idx") or "")
|
||||
local group = util.trim(fv(http, "group") or "")
|
||||
local nodes = core.read_nodes(id)
|
||||
if not idx or not nodes[idx] then
|
||||
http.write(util.json_encode({ ok = false, err = "node not found" }))
|
||||
@@ -315,27 +359,32 @@ function action_combo_save()
|
||||
local core = require("substore.core")
|
||||
local util = require("substore.util")
|
||||
if post_ok() then
|
||||
local id = util.trim(http.formvalue("id") or "")
|
||||
local name = (http.formvalue("name") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local id = util.trim(fv(http, "id") or "")
|
||||
local name = (fv(http, "name") or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
local sources = {}
|
||||
for _, it in ipairs(core.list()) do
|
||||
if not it.combo and http.formvalue("src_" .. it.id) then
|
||||
if not it.combo and fv(http, "src_" .. it.id) then
|
||||
sources[#sources + 1] = it.id
|
||||
end
|
||||
end
|
||||
if name ~= "" then
|
||||
local rules = read_rules_fields()
|
||||
local o = {
|
||||
rules_enable = rules.rules_enable, proto_filter = rules.proto_filter,
|
||||
keyword_include = rules.keyword_include, keyword_exclude = rules.keyword_exclude,
|
||||
dedup = rules.dedup,
|
||||
}
|
||||
if id == "" then
|
||||
core.add_combo(name, sources, o)
|
||||
else
|
||||
core.save_combo(id, name, sources, o)
|
||||
end
|
||||
-- §18:此前名称留空、或一个来源都没勾选时,这里整段跳过、直接跳回列表 ——
|
||||
-- 用户填了表单却什么都没发生,页面上也没有任何提示。
|
||||
if name == "" then return back_to_list("名称不能为空") end
|
||||
local rules = read_rules_fields()
|
||||
local o = {
|
||||
rules_enable = rules.rules_enable, proto_filter = rules.proto_filter,
|
||||
keyword_include = rules.keyword_include, keyword_exclude = rules.keyword_exclude,
|
||||
dedup = rules.dedup,
|
||||
}
|
||||
-- 返回值此前被整个丢弃:add_combo / save_combo 的失败(非法 ID、
|
||||
-- 未选来源、写入失败)一律静默。
|
||||
local nid, err
|
||||
if id == "" then
|
||||
nid, err = core.add_combo(name, sources, o)
|
||||
else
|
||||
nid, err = core.save_combo(id, name, sources, o)
|
||||
end
|
||||
if not nid then return back_to_list(err or "保存组合订阅失败") end
|
||||
end
|
||||
back_to_list()
|
||||
end
|
||||
@@ -344,12 +393,15 @@ function action_update()
|
||||
local http = require("luci.http")
|
||||
local core = require("substore.core")
|
||||
if post_ok() then
|
||||
local id = http.formvalue("id") or ""
|
||||
local id = fv(http, "id") or ""
|
||||
local meta = core.get(id)
|
||||
if meta and meta["local"] then
|
||||
-- local subscription does not support auto update
|
||||
return
|
||||
end
|
||||
-- §18:订阅不存在(ID 拼错 / 已被删除)时此前静默跳回列表,
|
||||
-- 用户点了「更新」却看不到任何反馈。
|
||||
if not meta then return back_to_list("订阅不存在") end
|
||||
-- pcall 只保证「不抛异常」;core.sync 的失败是「返回 nil, err」而非抛错,
|
||||
-- 因此必须同时检查两层结果,否则失败永远不会写回列表状态。
|
||||
local ok, res, err = pcall(core.sync, id)
|
||||
@@ -377,8 +429,8 @@ function action_probe()
|
||||
http.write("invalid token")
|
||||
return
|
||||
end
|
||||
local id = util.trim(http.formvalue("id") or "")
|
||||
local mode = util.trim(http.formvalue("mode") or "")
|
||||
local id = util.trim(fv(http, "id") or "")
|
||||
local mode = util.trim(fv(http, "mode") or "")
|
||||
if mode ~= "ping" and mode ~= "tcping" and mode ~= "url" then
|
||||
http.status(400, "Bad Request")
|
||||
http.prepare_content("text/plain; charset=utf-8")
|
||||
@@ -394,8 +446,8 @@ function action_probe()
|
||||
end
|
||||
-- 与 nodes 页一致的过滤:按当前 proto / keyword 过滤后逐个探测
|
||||
local nodes = core.read_nodes(id)
|
||||
local proto = util.trim(http.formvalue("proto") or "")
|
||||
local keyword = util.trim(http.formvalue("keyword") or "")
|
||||
local proto = util.trim(fv(http, "proto") or "")
|
||||
local keyword = util.trim(fv(http, "keyword") or "")
|
||||
if proto ~= "" then nodes = node.filter(nodes, { proto = proto }) end
|
||||
if keyword ~= "" then nodes = node.filter(nodes, { keyword = keyword }) end
|
||||
nodes = node.sort(nodes, "name", false)
|
||||
@@ -421,8 +473,8 @@ function action_download()
|
||||
local http = require("luci.http")
|
||||
local core = require("substore.core")
|
||||
local util = require("substore.util")
|
||||
local token = util.trim(http.formvalue("token") or "")
|
||||
local target = util.trim(http.formvalue("target") or "ClashMeta")
|
||||
local token = util.trim(fv(http, "token") or "")
|
||||
local target = util.trim(fv(http, "target") or "ClashMeta")
|
||||
|
||||
local content, ct, filename, err = core.generate_link(token, target)
|
||||
if not content then
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
local ki = (it and it.keyword_include) or ""
|
||||
local ke = (it and it.keyword_exclude) or ""
|
||||
local dedup = it and (it.dedup == "1" or it.dedup == true)
|
||||
-- 协议筛选 / 重命名:控制器一直在读这两项(read_rules_fields),但此前三个表单
|
||||
-- 都没有提交它们的控件 —— 字段永远写空,且每次保存会把 UCI 里已有的值重置为 ""。
|
||||
local proto_sel = {}
|
||||
for p in ((it and it.proto_filter) or ""):gmatch("[^,]+") do
|
||||
proto_sel[p:match("^%s*(.-)%s*$")] = true
|
||||
end
|
||||
local rm = (it and it.rename_map) or ""
|
||||
local sel = {}
|
||||
if it and type(it.sources) == "table" then
|
||||
for _, s in ipairs(it.sources) do sel[s] = true end
|
||||
@@ -63,7 +70,21 @@
|
||||
<input type="checkbox" name="dedup" value="1" <%=dedup and "checked" or ""%>/>
|
||||
</div>
|
||||
<div class="cbi-section-descr" style="margin:0.25em 0">
|
||||
<span style="color:#808080">多条关键词用逗号分隔(如:香港,美国,日本);包含 = 命中任一即保留,排除 = 命中任一即去除。</span>
|
||||
<label style="display:inline-block;min-width:10em;vertical-align:top">协议筛选</label>
|
||||
<% for _, p in ipairs(core.RULE_PROTOS) do %>
|
||||
<label style="margin-right:0.75em;white-space:nowrap">
|
||||
<input type="checkbox" name="proto_filter_<%=p%>" value="1" <%=proto_sel[p] and "checked" or ""%>/> <%=p%>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="cbi-section-descr" style="margin:0.25em 0">
|
||||
<label style="display:inline-block;min-width:10em;vertical-align:top">重命名</label>
|
||||
<textarea name="rename_map" rows="3" style="width:60%"><%=luci.util.pcdata(rm)%></textarea>
|
||||
</div>
|
||||
<div class="cbi-section-descr" style="margin:0.25em 0">
|
||||
<span style="color:#808080">多条关键词用逗号分隔(如:香港,美国,日本);包含 = 命中任一即保留,排除 = 命中任一即去除。<br/>
|
||||
协议筛选:全部不勾选 = 不筛选(保留所有协议)。<br/>
|
||||
重命名每行一条:<code>旧名称=新名称</code>(精确匹配)、<code>模式 -> 替换</code>(正则替换)、<code>{server}_{port}_{proto}</code>(占位符模板)。</span>
|
||||
</div>
|
||||
</div>
|
||||
<% if is_edit then %>
|
||||
|
||||
@@ -13,6 +13,13 @@
|
||||
local ki = (it and it.keyword_include) or ""
|
||||
local ke = (it and it.keyword_exclude) or ""
|
||||
local dedup = it and (it.dedup == "1" or it.dedup == true)
|
||||
-- 协议筛选 / 重命名:控制器一直在读这两项(read_rules_fields),但此前三个表单
|
||||
-- 都没有提交它们的控件 —— 字段永远写空,且每次保存会把 UCI 里已有的值重置为 ""。
|
||||
local proto_sel = {}
|
||||
for p in ((it and it.proto_filter) or ""):gmatch("[^,]+") do
|
||||
proto_sel[p:match("^%s*(.-)%s*$")] = true
|
||||
end
|
||||
local rm = (it and it.rename_map) or ""
|
||||
local proxy_enabled = it and (it.proxy_enable == true or it.proxy_enable == "1") or false
|
||||
local util = require("substore.util")
|
||||
-- 剩余流量 / 剩余时长(仅编辑页且有 subscription-userinfo 数据时显示,首页不显示)
|
||||
@@ -89,7 +96,21 @@
|
||||
<input type="checkbox" name="dedup" value="1" <%=dedup and "checked" or ""%>/>
|
||||
</div>
|
||||
<div class="cbi-section-descr" style="margin:0.25em 0">
|
||||
<span style="color:#808080">多条关键词用逗号分隔(如:香港,美国,日本);包含 = 命中任一即保留,排除 = 命中任一即去除。</span>
|
||||
<label style="display:inline-block;min-width:10em;vertical-align:top">协议筛选</label>
|
||||
<% for _, p in ipairs(core.RULE_PROTOS) do %>
|
||||
<label style="margin-right:0.75em;white-space:nowrap">
|
||||
<input type="checkbox" name="proto_filter_<%=p%>" value="1" <%=proto_sel[p] and "checked" or ""%>/> <%=p%>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="cbi-section-descr" style="margin:0.25em 0">
|
||||
<label style="display:inline-block;min-width:10em;vertical-align:top">重命名</label>
|
||||
<textarea name="rename_map" rows="3" style="width:60%"><%=luci.util.pcdata(rm)%></textarea>
|
||||
</div>
|
||||
<div class="cbi-section-descr" style="margin:0.25em 0">
|
||||
<span style="color:#808080">多条关键词用逗号分隔(如:香港,美国,日本);包含 = 命中任一即保留,排除 = 命中任一即去除。<br/>
|
||||
协议筛选:全部不勾选 = 不筛选(保留所有协议)。<br/>
|
||||
重命名每行一条:<code>旧名称=新名称</code>(精确匹配)、<code>模式 -> 替换</code>(正则替换)、<code>{server}_{port}_{proto}</code>(占位符模板)。</span>
|
||||
</div>
|
||||
</div>
|
||||
<% if is_edit and it.last_update then %>
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
local ki = (it and it.keyword_include) or ""
|
||||
local ke = (it and it.keyword_exclude) or ""
|
||||
local dedup = it and (it.dedup == "1" or it.dedup == true)
|
||||
-- 协议筛选 / 重命名:控制器一直在读这两项(read_rules_fields),但此前三个表单
|
||||
-- 都没有提交它们的控件 —— 字段永远写空,且每次保存会把 UCI 里已有的值重置为 ""。
|
||||
local proto_sel = {}
|
||||
for p in ((it and it.proto_filter) or ""):gmatch("[^,]+") do
|
||||
proto_sel[p:match("^%s*(.-)%s*$")] = true
|
||||
end
|
||||
local rm = (it and it.rename_map) or ""
|
||||
local local_mode = (it and it.local_mode) or "text"
|
||||
local content = (it and it.raw_content) or ""
|
||||
-- 注入 JS 的初始内容:JSON 字符串字面量,转义 "</" 防止提前闭合 script
|
||||
@@ -77,6 +84,22 @@
|
||||
<label style="display:inline-block;min-width:10em"><%:Dedup%></label>
|
||||
<input type="checkbox" name="dedup" value="1" <%=dedup and "checked" or ""%>/>
|
||||
</div>
|
||||
<div class="cbi-section-descr" style="margin:0.25em 0">
|
||||
<label style="display:inline-block;min-width:10em;vertical-align:top"><%:Protocol filter%></label>
|
||||
<% for _, p in ipairs(core.RULE_PROTOS) do %>
|
||||
<label style="margin-right:0.75em;white-space:nowrap">
|
||||
<input type="checkbox" name="proto_filter_<%=p%>" value="1" <%=proto_sel[p] and "checked" or ""%>/> <%=p%>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="cbi-section-descr" style="margin:0.25em 0">
|
||||
<label style="display:inline-block;min-width:10em;vertical-align:top"><%:Rename%></label>
|
||||
<textarea name="rename_map" rows="3" style="width:60%"><%=luci.util.pcdata(rm)%></textarea>
|
||||
</div>
|
||||
<div class="cbi-section-descr" style="margin:0.25em 0">
|
||||
<span style="color:#808080"><%:Protocol filter: with none ticked no filtering is applied.%><br/>
|
||||
<%:One rename rule per line: OLD=NEW (exact), PATTERN -> REPLACEMENT (regex), or {server}_{port}_{proto} (placeholder template).%></span>
|
||||
</div>
|
||||
</div>
|
||||
<% if is_edit and it.last_update then %>
|
||||
<div class="cbi-section-descr" style="margin:0.5em 0">
|
||||
|
||||
Reference in new issue
Block a user