update 2026-09-24 16:56:26

This commit is contained in:
action committed 2026-09-24 16:56:26 +08:00
1 parent b31a5c3878
commit e4cee331e9
6885 files changed
+3240523

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
module("luci.controller.oaf", package.seeall)
function index()
entry({"admin", "services", "oaf"}, firstchild(), _("Parental Control"), 20).dependent = true
end
@@ -0,0 +1,5 @@
module("luci.controller.oaf_about", package.seeall)
function index()
entry({"admin", "services", "oaf", "about"}, template("oaf/about"), _("About"), 100).leaf = true
end
@@ -0,0 +1,127 @@
module("luci.controller.oaf_advanced", package.seeall)
local util = require "luci.util"
function index()
entry({"admin", "services", "oaf", "advanced"}, template("oaf/advanced"), _("Settings"), 90).leaf = true
entry({"admin", "services", "oaf", "api", "system", "get_system_info"}, call("get_system_info"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "system", "set_system_info"}, call("set_system_info"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "system", "get_work_mode"}, call("get_work_mode"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "system", "set_work_mode"}, call("set_work_mode"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "system", "get_tcp_rst"}, call("get_tcp_rst"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "system", "set_tcp_rst"}, call("set_tcp_rst"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "system", "get_advanced_settings"}, call("get_advanced_settings"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "system", "set_advanced_settings"}, call("set_advanced_settings"), nil).leaf = true
end
local function ubus_call(api, payload)
payload = payload or {}
payload.api = api
return util.ubus("fwx", "common", payload) or {code = 1}
end
function get_system_info()
local http = require "luci.http"
local resp = ubus_call("get_system_info", {data = {}})
http.prepare_content("application/json")
http.write_json(resp)
end
function set_system_info()
local http = require "luci.http"
local lan_ifname = http.formvalue("lan_ifname") or ""
local theme_mode = tonumber(http.formvalue("theme_mode") or "0") or 0
if theme_mode ~= 0 and theme_mode ~= 1 then
theme_mode = 0
end
local resp = ubus_call("set_system_info", {
data = {
fwx = {
lan_ifname = lan_ifname,
theme_mode = theme_mode
}
}
})
http.prepare_content("application/json")
http.write_json(resp)
end
function get_work_mode()
local http = require "luci.http"
local resp = ubus_call("get_work_mode", {data = {}})
http.prepare_content("application/json")
http.write_json(resp)
end
function set_work_mode()
local http = require "luci.http"
local work_mode = tonumber(http.formvalue("work_mode") or "0") or 0
if work_mode ~= 0 and work_mode ~= 1 then
http.prepare_content("application/json")
http.write_json({code = 1})
return
end
local resp = ubus_call("set_work_mode", {
data = {
work_mode = work_mode
}
})
http.prepare_content("application/json")
http.write_json(resp)
end
function get_tcp_rst()
local http = require "luci.http"
local resp = ubus_call("get_tcp_rst", {data = {}})
http.prepare_content("application/json")
http.write_json(resp)
end
function set_tcp_rst()
local http = require "luci.http"
local tcp_rst = tonumber(http.formvalue("tcp_rst") or "1") or 1
tcp_rst = tcp_rst == 0 and 0 or 1
local resp = ubus_call("set_tcp_rst", {
data = {
tcp_rst = tcp_rst
}
})
http.prepare_content("application/json")
http.write_json(resp)
end
function get_advanced_settings()
local http = require "luci.http"
local resp = ubus_call("get_advanced_settings", {data = {}})
http.prepare_content("application/json")
http.write_json(resp)
end
function set_advanced_settings()
local http = require "luci.http"
local disable_hnat = tonumber(http.formvalue("disable_hnat") or "0") or 0
disable_hnat = disable_hnat == 1 and 1 or 0
local resp = ubus_call("set_advanced_settings", {
data = {
disable_hnat = disable_hnat
}
})
http.prepare_content("application/json")
http.write_json(resp)
end
@@ -0,0 +1,371 @@
module("luci.controller.oaf_app_filter", package.seeall)
local utl = require "luci.util"
function index()
local fs = require "nixio.fs"
if not fs.access("/etc/config/appfilter") then
return
end
entry({"admin", "services", "oaf", "app_filter"}, alias("admin", "services", "oaf", "app_filter", "rules"), _("App Filter"), 30).dependent = true
entry({"admin", "services", "oaf", "app_filter", "rules"}, cbi("oaf/app_filter/rules", {hideapplybtn=true, hidesavebtn=true, hideresetbtn=true}), _("Filter Rules"), 10).leaf=true
entry({"admin", "services", "oaf", "api", "app_filter", "class_list"}, call("get_class_list"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_filter", "get_all_users"}, call("get_all_users"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_filter", "get_filter_rules"}, call("get_filter_rules"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_filter", "add_filter_rule"}, call("add_filter_rule"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_filter", "update_filter_rule"}, call("update_filter_rule"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_filter", "delete_filter_rule"}, call("delete_filter_rule"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_filter", "get_appfilter_whitelist"}, call("get_appfilter_whitelist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_filter", "add_appfilter_whitelist"}, call("add_appfilter_whitelist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_filter", "del_appfilter_whitelist"}, call("del_appfilter_whitelist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_filter", "get_app_filter_adv"}, call("get_app_filter_adv"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_filter", "set_app_filter_adv"}, call("set_app_filter_adv"), nil).leaf = true
end
function get_class_list()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.CopyRight = "www.fanchmwrt.com"
req_obj.api = "class_list"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
llog("get class list");
luci.http.write_json(resp_obj.data)
else
llog("get class list failed");
luci.http.write_json({class_list = {}})
end
end
function get_all_users()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_all_users"
req_obj.data = {
flag = luci.http.formvalue("flag"),
page = luci.http.formvalue("page")
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json({data = resp_obj.data})
else
luci.http.write_json({data = resp_obj or {}})
end
end
function handle_file_upload()
local http = require "luci.http"
local fs = require "nixio.fs"
local upload_dir = "/tmp/uploads/"
local file_name = "uploaded_file"
llog("handle_file_upload started");
if not fs.access(upload_dir) then
fs.mkdir(upload_dir)
end
llog("Upload directory checked/created");
local file_path = upload_dir .. file_name
local fp
llog("file_path: " .. file_path);
http.setfilehandler(
function(meta, chunk, eof)
llog("File upload metadata: " .. (meta and meta.name or "nil") .. ", " .. (meta and meta.file or "nil"))
llog("File upload chunk size: " .. (chunk and #chunk or 0))
if not fp then
fp = io.open(file_path, "w")
llog("File opened for writing: " .. file_path)
end
if fp and chunk then
fp:write(chunk)
llog("Chunk written to file")
end
if fp and eof then
fp:close()
llog("File upload completed and file closed")
process_uploaded_file(file_path)
luci.http.prepare_content("application/json")
luci.http.write_json({ success = true, message = "File uploaded successfully" })
end
end
)
llog("handle_file_upload setup complete");
end
function process_uploaded_file(file_path)
llog("Processing uploaded file: " .. file_path)
local permanent_path = "/etc/config/" .. file_name
os.execute("mv " .. file_path .. " " .. permanent_path)
llog("File moved to: " .. permanent_path)
end
function llog(message)
local log_file = "/tmp/log/oaf_luci.log"
local fd = io.open(log_file, "a")
if fd then
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
fd:write(string.format("[%s] %s\n", timestamp, message))
fd:close()
end
end
function get_filter_rules()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_filter_rules"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data and resp_obj.data.list then
local rules_data = resp_obj.data.list
llog("get_filter_rules: returning " .. #rules_data .. " rules")
local json_str = json.stringify({code = 0, data = rules_data, message = "success"})
luci.http.write(json_str)
else
llog("get_filter_rules: failed, returning empty array")
local json_str = json.stringify({code = 0, data = {}, message = "success"})
luci.http.write(json_str)
end
end
function add_filter_rule()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local data_str = luci.http.formvalue("data")
if not data_str then
luci.http.write_json({code = 1, message = "Invalid request data"})
return
end
local rule_data = json.parse(data_str)
llog("add_filter_rule: " .. json.stringify(rule_data))
if not rule_data.name or not rule_data.mode or not rule_data.time_rules or not rule_data.app_ids then
luci.http.write_json({code = 1, message = "Missing required fields"})
return
end
local get_req_obj = {}
get_req_obj.api = "get_filter_rules"
get_req_obj.data = {}
local get_resp_obj = utl.ubus("fwx", "common", get_req_obj)
if get_resp_obj and get_resp_obj.code == 2000 and get_resp_obj.data and get_resp_obj.data.data then
local existing_rules = get_resp_obj.data.data
if #existing_rules >= 32 then
luci.http.write_json({code = 1, message = "Maximum 32 rules allowed"})
return
end
end
local req_obj = {}
req_obj.api = "add_filter_rule"
req_obj.data = rule_data
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 0, message = "Rule added successfully"})
else
luci.http.write_json({code = 1, message = "Failed to add rule"})
end
end
function update_filter_rule()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local data_str = luci.http.formvalue("data")
if not data_str then
luci.http.write_json({code = 1, message = "Invalid request data"})
return
end
local rule_data = json.parse(data_str)
llog("update_filter_rule: " .. json.stringify(rule_data))
if not rule_data.id then
luci.http.write_json({code = 1, message = "Missing rule id"})
return
end
local req_obj = {}
req_obj.api = "update_filter_rule"
req_obj.data = rule_data
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 0, message = "Rule updated successfully"})
else
luci.http.write_json({code = 1, message = "Failed to update rule"})
end
end
function delete_filter_rule()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local rule_id = luci.http.formvalue("rule_id")
if not rule_id then
luci.http.write_json({code = 1, message = "Invalid rule_id"})
return
end
llog("delete_filter_rule: " .. rule_id)
local req_obj = {}
req_obj.api = "delete_filter_rule"
req_obj.data = {
id = tonumber(rule_id)
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 0, message = "Rule deleted successfully"})
else
luci.http.write_json({code = 1, message = "Failed to delete rule"})
end
end
function get_appfilter_whitelist()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_appfilter_whitelist"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json({code = 2000, data = resp_obj.data, message = "success"})
else
luci.http.write_json({code = 2000, data = {list = {}}, message = "success"})
end
end
function add_appfilter_whitelist()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local data_str = luci.http.formvalue("data")
if not data_str then
luci.http.write_json({code = 1, message = "Invalid request data"})
return
end
local whitelist_data = json.parse(data_str)
llog("add_appfilter_whitelist: " .. json.stringify(whitelist_data))
if not whitelist_data.mac_list or type(whitelist_data.mac_list) ~= "table" then
luci.http.write_json({code = 1, message = "Invalid mac_list"})
return
end
local req_obj = {}
req_obj.api = "add_appfilter_whitelist"
req_obj.data = whitelist_data
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 2000, message = "Whitelist added successfully"})
else
luci.http.write_json({code = 1, message = "Failed to add whitelist"})
end
end
function del_appfilter_whitelist()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local mac = luci.http.formvalue("mac")
if not mac then
luci.http.write_json({code = 1, message = "Invalid mac address"})
return
end
llog("del_appfilter_whitelist: " .. mac)
local req_obj = {}
req_obj.api = "del_appfilter_whitelist"
req_obj.data = {
mac = mac
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 2000, message = "Whitelist deleted successfully"})
else
luci.http.write_json({code = 1, message = "Failed to delete whitelist"})
end
end
function get_app_filter_adv()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_app_filter_adv"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json({code = 0, data = resp_obj.data, message = "success"})
end
end
function set_app_filter_adv()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local enable = tonumber(luci.http.formvalue("enable")) or 1
local req_obj = {}
req_obj.api = "set_app_filter_adv"
req_obj.data = {
enable = enable,
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 0, message = "Saved successfully"})
else
luci.http.write_json({code = 1, message = "Failed to save"})
end
end
@@ -0,0 +1,83 @@
module("luci.controller.oaf_app_record", package.seeall)
local function normalize_page(value, default_value)
local n = tonumber(value or "") or default_value
if n < 1 then
n = default_value
end
return n
end
local function write_empty_list(page, page_size)
luci.http.write_json({
total_num = 0,
total_page = 1,
page = page,
page_size = page_size,
list = {}
})
end
function index()
entry({"admin", "services", "oaf", "app_record"}, template("oaf/app_record"), _("App Record"), 70).leaf = true
entry({"admin", "services", "oaf", "api", "app_record", "get_active_app_records"}, call("get_active_app_records")).leaf = true
entry({"admin", "services", "oaf", "api", "app_record", "get_app_history_records"}, call("get_app_history_records")).leaf = true
end
function get_active_app_records()
local util = require "luci.util"
local page = normalize_page(luci.http.formvalue("page"), 1)
local page_size = normalize_page(luci.http.formvalue("page_size"), 15)
luci.http.prepare_content("application/json")
local req_obj = {
api = "get_active_app_records",
data = {
page = page,
page_size = page_size
}
}
local resp_obj = util.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
write_empty_list(page, page_size)
end
end
function get_app_history_records()
local util = require "luci.util"
local mac = luci.http.formvalue("mac")
local start_time = tonumber(luci.http.formvalue("start_time") or "0") or 0
local end_time = tonumber(luci.http.formvalue("end_time") or "0") or 0
local appid = tonumber(luci.http.formvalue("appid") or "0") or 0
local page = normalize_page(luci.http.formvalue("page"), 1)
local page_size = normalize_page(luci.http.formvalue("page_size"), 15)
if appid < 0 then
appid = 0
end
luci.http.prepare_content("application/json")
local req_obj = {
api = "get_app_history_records",
data = {
mac = mac,
start_time = start_time,
end_time = end_time,
appid = appid,
page = page,
page_size = page_size
}
}
local resp_obj = util.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
write_empty_list(page, page_size)
end
end
@@ -0,0 +1,229 @@
module("luci.controller.oaf_dashboard", package.seeall)
function index()
entry({"admin", "services", "oaf", "dashboard"}, cbi("oaf/dashboard", {hideapplybtn=true, hidesavebtn=true, hideresetbtn=true}),
_("Dashboard"), 10).leaf = true
entry({"admin", "services", "oaf", "api", "dashboard", "get_dashboard_common"}, call("get_dashboard_common")).leaf = true
entry({"admin", "services", "oaf", "api", "dashboard", "get_init_status"}, call("get_init_status")).leaf = true
entry({"admin", "services", "oaf", "api", "dashboard", "set_init_status"}, call("set_init_status")).leaf = true
entry({"admin", "services", "oaf", "api", "dashboard", "set_notice_status"}, call("set_notice_status")).leaf = true
entry({"admin", "services", "oaf", "api", "dashboard", "get_daily_top_users"}, call("get_daily_top_users")).leaf = true
entry({"admin", "services", "oaf", "api", "dashboard", "get_history_traffic_stats"}, call("get_history_traffic_stats")).leaf = true
entry({"admin", "services", "oaf", "api", "dashboard", "get_global_traffic_stats"}, call("get_global_traffic_stats")).leaf = true
entry({"admin", "services", "oaf", "api", "dashboard", "get_active_users"}, call("get_active_users")).leaf = true
entry({"admin", "services", "oaf", "api", "dashboard", "get_app_type_stats"}, call("get_app_type_stats")).leaf = true
end
function get_dashboard_common()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_dashboard_common"
req_obj.CopyRight = "www.fanchmwrt.com"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({
system_status = {},
network_status = {},
active_app = {total = 0, list = {}},
advanced = {disable_hnat = 0, notice_status = 0},
interface_traffic = {interface = "wan", traffic = {}}
})
end
end
function get_global_traffic_stats()
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_global_traffic_stats"
req_obj.data = {}
local date = luci.http.formvalue("date")
if date then
req_obj.data.date = tonumber(date) or 0
end
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({
date = 0,
is_today = 1,
hourly_traffic = {}
})
end
end
function get_history_traffic_stats()
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_history_traffic_stats"
req_obj.data = {}
local days = luci.http.formvalue("days")
if days then
req_obj.data.days = tonumber(days) or 30
else
req_obj.data.days = 30
end
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({
days = req_obj.data.days,
total_up_bytes = 0,
total_down_bytes = 0,
total_bytes = 0,
list = {}
})
end
end
function get_init_status()
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_init_status"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({init_status = 1})
end
end
function set_init_status()
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "set_init_status"
req_obj.data = { init_status = 1 }
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({init_status = 0})
end
end
function set_notice_status()
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "set_dashboard_notice_status"
req_obj.data = { notice_status = 1 }
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 2000, notice_status = 1})
else
luci.http.write_json({code = 1})
end
end
function get_daily_top_users()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_daily_top_users"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({
date = 0,
total_count = 0,
users = {}
})
end
end
function get_active_users()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_active_users"
req_obj.data = {}
local count = luci.http.formvalue("count")
if count then
req_obj.data.count = tonumber(count) or 8
else
req_obj.data.count = 8
end
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({
total_count = 0,
users = {}
})
end
end
function get_app_type_stats()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local stat_type = luci.http.formvalue("type") or "hourly"
local req_obj = {}
req_obj.api = "get_global_app_type_stats"
req_obj.data = {
type = stat_type,
limit = 10
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({
type = stat_type,
limit = 10,
total_count = 0,
types = {}
})
end
end
@@ -0,0 +1,118 @@
module("luci.controller.oaf_feature", package.seeall)
function index()
entry({"admin", "services", "oaf", "feature"},
template("oaf/feature"),
_("Feature Library"), 80).dependent = true
entry({"admin", "services", "oaf", "feature", "info"}, call("get_feature_info"), nil).leaf = true
entry({"admin", "services", "oaf", "feature", "class_list"}, call("get_feature_class_list"), nil).leaf = true
entry({"admin", "services", "oaf", "feature", "online_config"}, call("get_feature_online_config"), nil).leaf = true
entry({"admin", "services", "oaf", "feature", "online_save"}, call("set_feature_online_config"), nil).leaf = true
entry({"admin", "services", "oaf", "feature", "online_list"}, call("get_feature_online_list"), nil).leaf = true
entry({"admin", "services", "oaf", "feature", "online_start"}, call("start_feature_online_update"), nil).leaf = true
entry({"admin", "services", "oaf", "feature", "online_status"}, call("get_feature_online_update_status"), nil).leaf = true
entry({"admin", "services", "oaf", "feature", "custom_list"}, call("get_custom_feature_list"), nil).leaf = true
entry({"admin", "services", "oaf", "feature", "custom_class_list"}, call("get_custom_feature_class_list"), nil).leaf = true
entry({"admin", "services", "oaf", "feature", "custom_save"}, call("set_custom_feature_list"), nil).leaf = true
end
function get_feature_info()
local json = require "luci.jsonc"
local util = require "luci.util"
local http = require "luci.http"
local resp = util.ubus("fwx", "common", {api = "get_feature_info", data = {}})
http.prepare_content("application/json")
http.write(json.stringify(resp or {code = 4000}))
end
local function write_fwx_response(api, data)
local json = require "luci.jsonc"
local util = require "luci.util"
local http = require "luci.http"
local resp = util.ubus("fwx", "common", {api = api, data = data or {}})
http.prepare_content("application/json")
http.write(json.stringify(resp or {code = 4000}))
end
function get_feature_online_config()
write_fwx_response("get_feature_online_config", {})
end
function set_feature_online_config()
local http = require "luci.http"
write_fwx_response("set_feature_online_config", {
token = http.formvalue("token") or ""
})
end
function get_feature_online_list()
local http = require "luci.http"
local lang = http.formvalue("lang") or "cn"
local refresh = tonumber(http.formvalue("refresh") or "0") or 0
if lang ~= "cn" and lang ~= "en" then
lang = "cn"
end
write_fwx_response("get_feature_online_list", {
lang = lang,
device_lang = http.formvalue("device_lang") or "",
refresh = refresh
})
end
function start_feature_online_update()
local http = require "luci.http"
local lang = http.formvalue("lang") or "cn"
if lang ~= "cn" and lang ~= "en" then
lang = "cn"
end
write_fwx_response("start_feature_online_update", {
id = http.formvalue("id") or "",
lang = lang,
md5 = http.formvalue("md5") or ""
})
end
function get_feature_online_update_status()
write_fwx_response("get_feature_online_update_status", {})
end
function get_custom_feature_list()
write_fwx_response("get_custom_feature", {})
end
function get_custom_feature_class_list()
write_fwx_response("get_custom_feature_class_list", {})
end
function set_custom_feature_list()
local json = require "luci.jsonc"
local http = require "luci.http"
local data_str = http.formvalue("data")
local ok, data_obj = pcall(json.parse, data_str or "")
if not ok then
data_obj = nil
end
if type(data_obj) ~= "table" or type(data_obj.app_list) ~= "table" then
http.prepare_content("application/json")
http.write(json.stringify({code = 4000, data = {error = "invalid request data"}}))
return
end
write_fwx_response("set_custom_feature", data_obj)
end
function get_feature_class_list()
local json = require "luci.jsonc"
local util = require "luci.util"
local http = require "luci.http"
local resp = util.ubus("fwx", "common", {CopyRight = "www.fanchmwrt.com", api = "class_list", data = {}})
http.prepare_content("application/json")
if resp and resp.code == 2000 and resp.data then
http.write(json.stringify(resp.data))
else
http.write(json.stringify({class_list = {}}))
end
end
@@ -0,0 +1,113 @@
module("luci.controller.oaf_internet_audit", package.seeall)
function index()
entry({"admin", "services", "oaf", "api", "internet_audit", "get_record_base"}, call("get_record_base"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "internet_audit", "set_record_base"}, call("set_record_base"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "internet_audit", "record_action"}, call("record_action"), nil).leaf = true
end
function ensure_record_section(cur)
local sid
cur:foreach("fwx", "record", function(s)
sid = s[".name"]
end)
if not sid then
sid = cur:add("fwx", "record")
end
return sid
end
function get_record_base()
local json = require "luci.jsonc"
local http = require "luci.http"
local utl = require "luci.util"
local req_obj = { api = "get_record_base", data = {} }
local resp = utl.ubus("fwx", "common", req_obj) or {code = 1}
http.prepare_content("application/json")
http.write(json.stringify(resp))
end
function set_record_base()
local json = require "luci.jsonc"
local http = require "luci.http"
local utl = require "luci.util"
local enable = tonumber(http.formvalue("enable") or 0) or 0
local record_time = tonumber(http.formvalue("record_time") or 0) or 0
local app_valid_time = tonumber(http.formvalue("app_valid_time") or 0) or 0
local history_data_size = http.formvalue("history_data_size") or ""
local history_data_path = http.formvalue("history_data_path") or ""
local base_data_path = http.formvalue("base_data_path") or http.formvalue("terminal_data_path") or ""
if record_time < 0 then record_time = 0 end
if app_valid_time < 0 then app_valid_time = 0 end
if history_data_size and history_data_size ~= "" then
local size_num = tonumber(history_data_size)
if not size_num or size_num < 1 or size_num > 1024 or size_num ~= math.floor(size_num) then
http.prepare_content("application/json")
http.write(json.stringify({code = 1, msg = "History data size must be an integer between 1 and 1024 MB"}))
return
end
end
if not history_data_path or history_data_path == "" or history_data_path == "/" then
http.prepare_content("application/json")
http.write(json.stringify({code = 1, msg = "History data path cannot be empty or /"}))
return
end
if not base_data_path or base_data_path == "" or base_data_path == "/" then
http.prepare_content("application/json")
http.write(json.stringify({code = 1, msg = "Base data path cannot be empty or /"}))
return
end
if #history_data_path > 64 then
http.prepare_content("application/json")
http.write(json.stringify({code = 1, msg = "History data path maximum length is 64 characters"}))
return
end
if #base_data_path > 64 then
http.prepare_content("application/json")
http.write(json.stringify({code = 1, msg = "Base data path maximum length is 64 characters"}))
return
end
local req_obj = {
api = "set_record_base",
data = {
enable = enable,
record_time = record_time,
app_valid_time = app_valid_time,
history_data_size = history_data_size,
history_data_path = history_data_path,
base_data_path = base_data_path
}
}
local resp = utl.ubus("fwx", "common", req_obj) or {code = 1}
http.prepare_content("application/json")
http.write(json.stringify(resp))
end
function record_action()
local json = require "luci.jsonc"
local http = require "luci.http"
local utl = require "luci.util"
local action = http.formvalue("action") or ""
local req_obj = {
api = "record_action",
data = {
action = action
}
}
local resp = utl.ubus("fwx", "common", req_obj) or {code = 1}
http.prepare_content("application/json")
http.write(json.stringify(resp))
end
@@ -0,0 +1,461 @@
module("luci.controller.oaf_mac_filter", package.seeall)
local utl = require "luci.util"
function index()
local fs = require "nixio.fs"
if not fs.access("/etc/config/macfilter") then
return
end
entry({"admin", "services", "oaf", "mac_filter"}, alias("admin", "services", "oaf", "mac_filter", "rules"), _("Access Control"), 40).dependent = true
entry({"admin", "services", "oaf", "mac_filter", "rules"}, cbi("oaf/mac_filter/rules", {hideapplybtn=true, hidesavebtn=true, hideresetbtn=true}), _("Filter Rules"), 10).leaf=true
entry({"admin", "services", "oaf", "api", "mac_filter", "get_mac_filter_base"}, call("get_mac_filter_base"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "set_mac_filter_base"}, call("set_mac_filter_base"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "set_mac_filter_time"}, call("set_mac_filter_time"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "get_mac_filter_time"}, call("get_mac_filter_time"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "get_mac_filter_user"}, call("get_mac_filter_user"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "set_mac_filter_user"}, call("set_mac_filter_user"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "del_mac_filter_user"}, call("del_mac_filter_user"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "add_mac_filter_user"}, call("add_mac_filter_user"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "get_mf_status"}, call("get_mf_status"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "get_mac_filter_whitelist"}, call("get_mac_filter_whitelist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "del_mac_filter_whitelist"}, call("del_mac_filter_whitelist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "add_mac_filter_whitelist"}, call("add_mac_filter_whitelist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "get_mac_filter_rules"}, call("get_mac_filter_rules"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "add_mac_filter_rule"}, call("add_mac_filter_rule"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "update_mac_filter_rule"}, call("update_mac_filter_rule"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "delete_mac_filter_rule"}, call("delete_mac_filter_rule"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "get_mac_filter_adv"}, call("get_mac_filter_adv"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "mac_filter", "set_mac_filter_adv"}, call("set_mac_filter_adv"), nil).leaf = true
end
function get_mf_status()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_mf_status"
req_obj.data = {}
local resp_obj=utl.ubus("fwx", "common", req_obj);
luci.http.write_json(resp_obj);
end
function get_mac_filter_user()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_mac_filter_user"
req_obj.data = {}
local resp_obj=utl.ubus("fwx", "common", req_obj);
luci.http.write_json(resp_obj);
end
function del_mac_filter_user()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
local mac = luci.http.formvalue("mac")
llog("del macfilter user "..mac);
req_obj.api = "del_mac_filter_user"
req_obj.data = {
mac = mac
}
local resp_obj=utl.ubus("fwx", "common", req_obj);
luci.http.write_json(resp_obj);
end
function add_mac_filter_user()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "add_mac_filter_user"
local data_str = luci.http.formvalue("data")
local data = json.parse(data_str)
req_obj.data = data
local resp_obj=utl.ubus("fwx", "common", req_obj);
luci.http.write_json(resp_obj);
end
function get_mac_filter_base()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_mac_filter_base"
req_obj.data = {}
local resp_obj=utl.ubus("fwx", "common", req_obj);
luci.http.write_json(resp_obj);
end
function set_mac_filter_user()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "set_mac_filter_user"
local mode = luci.http.formvalue("mode")
req_obj.data = {
mode = mode
}
local resp_obj=utl.ubus("fwx", "common", req_obj);
luci.http.write_json(resp_obj);
end
function set_mac_filter_base()
local json = require "luci.jsonc"
llog("set macfilter base");
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "set_mac_filter_base"
local enable = luci.http.formvalue("enable")
req_obj.data = {
enable = enable
}
local resp_obj=utl.ubus("fwx", "common", req_obj);
luci.http.write_json(resp_obj);
end
function set_mac_filter_time()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "set_mac_filter_time"
local data_str = luci.http.formvalue("data")
local data = json.parse(data_str)
req_obj.data = data
local resp_obj=utl.ubus("fwx", "common", req_obj);
luci.http.write_json(resp_obj);
end
function get_mac_filter_time()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_mac_filter_time"
req_obj.data = {}
local resp_obj=utl.ubus("fwx", "common", req_obj);
luci.http.write_json(resp_obj);
end
function get_mac_filter_whitelist()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_mac_filter_whitelist"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json({code = 2000, data = resp_obj.data, message = "success"})
else
luci.http.write_json({code = 2000, data = {list = {}}, message = "success"})
end
end
function add_mac_filter_whitelist()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local data_str = luci.http.formvalue("data")
if not data_str then
luci.http.write_json({code = 1, message = "Invalid request data"})
return
end
local whitelist_data = json.parse(data_str)
llog("add_mac_filter_whitelist: " .. json.stringify(whitelist_data))
if not whitelist_data.mac_list or type(whitelist_data.mac_list) ~= "table" then
luci.http.write_json({code = 1, message = "Invalid mac_list"})
return
end
local req_obj = {}
req_obj.api = "add_mac_filter_whitelist"
req_obj.data = whitelist_data
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 2000, message = "Whitelist added successfully"})
else
luci.http.write_json({code = 1, message = "Failed to add whitelist"})
end
end
function del_mac_filter_whitelist()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local mac = luci.http.formvalue("mac")
if not mac then
luci.http.write_json({code = 1, message = "Invalid mac address"})
return
end
llog("del_mac_filter_whitelist: " .. mac)
local req_obj = {}
req_obj.api = "del_mac_filter_whitelist"
req_obj.data = {
mac = mac
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 2000, message = "Whitelist deleted successfully"})
else
luci.http.write_json({code = 1, message = "Failed to delete whitelist"})
end
end
function llog(message)
local log_file = "/tmp/log/oaf_luci.log"
local fd = io.open(log_file, "a")
if fd then
local timestamp = os.date("%Y-%m-%d %H:%M:%S")
fd:write(string.format("[%s] %s\n", timestamp, message))
fd:close()
end
end
function get_mac_filter_rules()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_mac_filter_rules"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data and resp_obj.data.list then
local rules_data = resp_obj.data.list
local json_str = json.stringify({code = 0, data = rules_data, message = "success"})
luci.http.write(json_str)
else
local json_str = json.stringify({code = 0, data = {}, message = "success"})
luci.http.write(json_str)
end
end
function add_mac_filter_rule()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local data_str = luci.http.formvalue("data")
if not data_str then
luci.http.write_json({code = 1, message = "Invalid request data"})
return
end
local rule_data = json.parse(data_str)
llog("add_mac_filter_rule: " .. json.stringify(rule_data))
if not rule_data.name or not rule_data.mode then
luci.http.write_json({code = 1, message = "Missing required fields"})
return
end
local time_mode = tonumber(rule_data.time_mode) or 1
if time_mode == 1 then
if (not rule_data.time_list or #rule_data.time_list == 0) and (not rule_data.time_rules or #rule_data.time_rules == 0) then
luci.http.write_json({code = 1, message = "Missing required time_list"})
return
end
elseif time_mode == 2 then
if (not rule_data.time_limit or rule_data.time_limit == "") and (not rule_data.time_rules or #rule_data.time_rules == 0) then
luci.http.write_json({code = 1, message = "Missing required time_limit"})
return
end
elseif time_mode == 3 then
if (not rule_data.flow_limit or rule_data.flow_limit == "") and (not rule_data.time_rules or #rule_data.time_rules == 0) then
luci.http.write_json({code = 1, message = "Missing required flow_limit"})
return
end
end
local get_req_obj = {}
get_req_obj.api = "get_mac_filter_rules"
get_req_obj.data = {}
local get_resp_obj = utl.ubus("fwx", "common", get_req_obj)
if get_resp_obj and get_resp_obj.code == 2000 and get_resp_obj.data and get_resp_obj.data.data then
local existing_rules = get_resp_obj.data.data
if #existing_rules >= 32 then
luci.http.write_json({code = 1, message = "Maximum 32 rules allowed"})
return
end
end
local req_obj = {}
req_obj.api = "add_mac_filter_rule"
req_obj.data = rule_data
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 0, message = "Rule added successfully"})
else
luci.http.write_json({code = 1, message = "Failed to add rule"})
end
end
function update_mac_filter_rule()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local data_str = luci.http.formvalue("data")
if not data_str then
luci.http.write_json({code = 1, message = "Invalid request data"})
return
end
local rule_data = json.parse(data_str)
llog("update_mac_filter_rule: " .. json.stringify(rule_data))
if not rule_data.id then
luci.http.write_json({code = 1, message = "Missing rule id"})
return
end
local req_obj = {}
req_obj.api = "update_mac_filter_rule"
req_obj.data = rule_data
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 0, message = "Rule updated successfully"})
else
luci.http.write_json({code = 1, message = "Failed to update rule"})
end
end
function delete_mac_filter_rule()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local rule_id = luci.http.formvalue("rule_id")
if not rule_id then
luci.http.write_json({code = 1, message = "Invalid rule_id"})
return
end
llog("delete_mac_filter_rule: " .. rule_id)
local req_obj = {}
req_obj.api = "delete_mac_filter_rule"
req_obj.data = {
id = tonumber(rule_id)
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 0, message = "Rule deleted successfully"})
else
luci.http.write_json({code = 1, message = "Failed to delete rule"})
end
end
function get_mac_filter_adv()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_mac_filter_adv"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json({code = 0, data = resp_obj.data, message = "success"})
else
luci.http.write_json({code = 1, message = "Failed to load"})
end
end
function set_mac_filter_adv()
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local enable = tonumber(luci.http.formvalue("enable")) or 0
if enable ~= 0 and enable ~= 1 then
luci.http.write_json({code = 1, message = "Invalid enable value, must be 0 or 1"})
return
end
local req_obj = {}
req_obj.api = "set_mac_filter_adv"
req_obj.data = {
enable = enable
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 then
llog("Set macfilter enable: " .. enable)
luci.http.write_json({code = 0, message = "Saved successfully"})
else
luci.http.write_json({code = 1, message = "Failed to save"})
end
end
+452
View File
@@ -0,0 +1,452 @@
module("luci.controller.oaf_user", package.seeall)
local utl = require "luci.util"
function index()
local page
entry({"admin", "services", "oaf", "users"}, alias("admin", "services", "oaf", "users", "list"), _("User List"), 20).dependent = true
entry({"admin", "services", "oaf", "users", "list"}, cbi("oaf/user_list", {hideapplybtn=true, hidesavebtn=true, hideresetbtn=true}),
nil).leaf = true
entry({"admin", "services", "oaf", "users", "detail"}, cbi("oaf/user_detail", {hideapplybtn=true, hidesavebtn=true, hideresetbtn=true}), nil).leaf = true
entry({"admin", "services", "oaf", "api", "user_status"}, call("user_status"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "dev_visit_list"}, call("get_dev_visit_list"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "dev_visit_time"}, call("get_dev_visit_time"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "app_class_visit_time"}, call("get_app_class_visit_time"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "get_class_list"}, call("get_class_list"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "get_all_users"}, call("get_all_users"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "get_system_base_info"}, call("get_system_base_info"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "get_mac_blacklist"}, call("get_mac_blacklist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "add_mac_blacklist"}, call("add_mac_blacklist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "del_mac_blacklist"}, call("del_mac_blacklist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "get_parental_control_detail"}, call("get_parental_control_detail"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "set_nickname"}, call("set_nickname"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "get_hourly_stats"}, call("get_hourly_stats"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "get_user_basic_info"}, call("get_user_basic_info"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "get_online_offline_records"}, call("get_online_offline_records"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "get_user_parental_control_rules"}, call("get_user_parental_control_rules"), nil).leaf = true
end
function get_hostname_by_mac(dst_mac)
leasefile="/tmp/dhcp.leases"
local fd = io.open(leasefile, "r")
if not fd then return end
while true do
local ln = fd:read("*l")
if not ln then
break
end
local ts, mac, ip, name, duid = ln:match("^(%d+) (%S+) (%S+) (%S+) (%S+)")
if dst_mac == mac then
fd:close()
return name
end
end
fd:close()
return ""
end
function get_app_name_by_id(appid)
local class_fd = io.popen("find /tmp/appfilter/ -type f -name *.class |xargs cat |grep "..appid.."|awk '{print $2}'")
if class_fd then
local name = class_fd:read("*l")
class_fd:close()
if name and name ~= "" then
return name
end
end
if tonumber(appid) and tonumber(appid) > 0 then
return "App" .. tostring(appid)
end
return ""
end
function cmp_func(a,b)
return a.latest_time > b.latest_time
end
function normalize_mac(mac)
if not mac then
return ""
end
return tostring(mac):gsub("^%s+", ""):gsub("%s+$", ""):upper()
end
function call_fwx_common(api, data)
local req_obj = {}
req_obj.api = api
req_obj.data = data or {}
return utl.ubus("fwx", "common", req_obj)
end
function user_status()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local fd = io.open("/proc/net/af_client","r")
status_buf=fd:read('*a')
fd:close()
user_array=json.parse(status_buf)
local req_obj = {}
req_obj.api = "visit_list"
req_obj.data = {}
local visit_obj = utl.ubus("fwx", "common", req_obj)
local user_array = {}
if visit_obj and visit_obj.code == 2000 and visit_obj.data and visit_obj.data.dev_list then
user_array = visit_obj.data.dev_list
end
local history={}
for i, v in pairs(user_array) do
visit_array=user_array[i].visit_info
for j,s in pairs(visit_array) do
print(user_array[i].mac, user_array[i].ip,visit_array[j].appid, visit_array[j].latest_time)
total_time=visit_array[j].latest_time - visit_array[j].first_time;
history[#history+1]={
mac=user_array[i].mac,
ip=user_array[i].ip,
hostname=get_hostname_by_mac(user_array[i].mac),
appid=visit_array[j].appid,
appname=get_app_name_by_id(visit_array[j].appid),
total_num=0,
drop_num=0,
latest_action=visit_array[j].latest_action,
latest_time=os.date("%Y/%m/%d %H:%M:%S", visit_array[j].latest_time),
first_time=os.date("%Y/%m/%d %H:%M:%S", visit_array[j].first_time),
total_time=total_time
}
end
end
table.sort(history, cmp_func)
luci.http.write_json(history);
end
function get_class_list()
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.CopyRight = "www.fanchmwrt.com"
req_obj.api = "class_list"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({class_list = {}})
end
end
function get_all_users()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_all_users"
req_obj.data = {
flag = luci.http.formvalue("flag"),
page = luci.http.formvalue("page"),
page_size = luci.http.formvalue("page_size")
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json({data = resp_obj.data})
else
luci.http.write_json({data = resp_obj or {}})
end
end
function get_system_base_info()
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_system_base_info"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({
user_session_enable = 0
})
end
end
function get_mac_blacklist()
luci.http.prepare_content("application/json")
local resp_obj = call_fwx_common("get_mac_blacklist", {})
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json({code = 0, data = resp_obj.data, message = "success"})
else
luci.http.write_json({code = 1, data = {list = {}}, message = "failed"})
end
end
function add_mac_blacklist()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local mac_list = {}
local mac = normalize_mac(luci.http.formvalue("mac"))
if mac ~= "" then
table.insert(mac_list, mac)
else
local data_str = luci.http.formvalue("data")
if data_str and data_str ~= "" then
local data = json.parse(data_str)
if data and type(data.mac_list) == "table" then
for _, item in ipairs(data.mac_list) do
local normalized_mac = normalize_mac(item)
if normalized_mac ~= "" then
table.insert(mac_list, normalized_mac)
end
end
end
end
end
if #mac_list == 0 then
luci.http.write_json({code = 1, message = "Invalid mac"})
return
end
local resp_obj = call_fwx_common("add_mac_blacklist", {mac_list = mac_list})
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 0, message = "success"})
else
luci.http.write_json({code = 1, message = "failed"})
end
end
function del_mac_blacklist()
luci.http.prepare_content("application/json")
local mac = normalize_mac(luci.http.formvalue("mac"))
if mac == "" then
luci.http.write_json({code = 1, message = "Invalid mac"})
return
end
local resp_obj = call_fwx_common("del_mac_blacklist", {mac = mac})
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json({code = 0, message = "success"})
else
luci.http.write_json({code = 1, message = "failed"})
end
end
function get_parental_control_detail()
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_parental_control_detail"
req_obj.data = {
mac = luci.http.formvalue("mac")
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({
pc_status = "unlimited",
pc_status_key = "unlimited",
af_whitelist = 0,
mf_whitelist = 0,
appfilter_rules = {},
macfilter_rules = {}
})
end
end
function get_user_parental_control_rules()
luci.http.prepare_content("application/json")
local target_mac = normalize_mac(luci.http.formvalue("mac"))
if target_mac == "" then
luci.http.write_json({
mac = "",
af_whitelist = 0,
mf_whitelist = 0,
list = {}
})
return
end
local req_obj = {
api = "get_user_parental_control_rules",
data = {
mac = target_mac
}
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and type(resp_obj.data) == "table" then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({
mac = target_mac,
af_whitelist = 0,
mf_whitelist = 0,
list = {}
})
end
end
function get_oaf_status()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_oaf_status"
req_obj.data = {}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json(resp_obj or {})
end
end
function set_nickname()
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "set_nickname"
req_obj.data = {
mac = luci.http.formvalue("mac"),
nickname = luci.http.formvalue("nickname")
}
local resp_obj = utl.ubus("fwx", "common", req_obj);
if resp_obj and resp_obj.code == 2000 then
luci.http.write_json(resp_obj.data or {})
else
luci.http.write_json(resp_obj or {})
end
end
function get_dev_visit_time(mac)
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "dev_visit_time"
req_obj.data = {
mac = mac
}
local visit_obj = utl.ubus("fwx", "common", req_obj)
local visit_list = {}
if visit_obj and visit_obj.code == 2000 and visit_obj.data and visit_obj.data.list then
visit_list = visit_obj.data.list
end
luci.http.write_json(visit_list)
end
function get_app_class_visit_time(mac)
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "app_class_visit_time"
req_obj.data = {
mac = mac
}
local visit_obj = utl.ubus("fwx", "common", req_obj)
local class_array = {}
if visit_obj and visit_obj.code == 2000 and visit_obj.data and visit_obj.data.class_list then
class_array = visit_obj.data.class_list
end
luci.http.write_json(class_array)
end
function get_dev_visit_list(mac)
local json = require "luci.jsonc"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "dev_visit_list"
req_obj.data = {
mac = mac
}
local page = luci.http.formvalue("page")
local page_size = luci.http.formvalue("page_size")
if page then
req_obj.data.page = tonumber(page) or 1
end
if page_size then
req_obj.data.page_size = tonumber(page_size) or 15
end
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json(resp_obj or {})
end
end
function get_hourly_stats(mac)
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_hourly_top_apps"
req_obj.data = {
mac = mac
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({
mac = mac,
date = 0,
is_today = 1,
hourly_stats = {}
})
end
end
function get_user_basic_info(mac)
local json = require "luci.jsonc"
local utl = require "luci.util"
luci.http.prepare_content("application/json")
local req_obj = {}
req_obj.api = "get_user_basic_info"
req_obj.data = {
mac = mac
}
local resp_obj = utl.ubus("fwx", "common", req_obj)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
luci.http.write_json(resp_obj.data)
else
luci.http.write_json({})
end
end
@@ -0,0 +1,181 @@
module("luci.controller.oaf_whitelist", package.seeall)
local util = require "luci.util"
local jsonc = require "luci.jsonc"
local function normalize_mac(mac)
return string.upper((mac or ""):gsub("^%s+", ""):gsub("%s+$", ""))
end
local function is_valid_mac(mac)
return mac and mac:match("^%x%x:%x%x:%x%x:%x%x:%x%x:%x%x$") ~= nil
end
local function write_json(data)
luci.http.prepare_content("application/json")
luci.http.write_json(data)
end
local function get_record_whitelist_page(page, page_size)
local req_obj = {
api = "get_record_whitelist",
data = {
page = page,
page_size = page_size
}
}
return util.ubus("fwx", "common", req_obj)
end
function index()
local fs = require "nixio.fs"
local has_app_filter = fs.access("/etc/config/appfilter")
local has_access_control = fs.access("/etc/config/macfilter")
local has_record = fs.access("/etc/config/fwx_record")
if not has_app_filter and not has_access_control and not has_record then
return
end
entry({"admin", "services", "oaf", "whitelist"}, cbi("oaf/whitelist", {hideapplybtn=true, hidesavebtn=true, hideresetbtn=true}), _("Whitelist"), 50).leaf = true
entry({"admin", "services", "oaf", "api", "record_whitelist", "get_record_whitelist"}, call("get_record_whitelist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "record_whitelist", "get_record_whitelist_all"}, call("get_record_whitelist_all"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "record_whitelist", "add_record_whitelist"}, call("add_record_whitelist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "record_whitelist", "del_record_whitelist"}, call("del_record_whitelist"), nil).leaf = true
entry({"admin", "services", "oaf", "api", "record_whitelist", "get_all_users"}, call("get_all_users"), nil).leaf = true
end
function get_record_whitelist()
local page = tonumber(luci.http.formvalue("page") or "1") or 1
local page_size = tonumber(luci.http.formvalue("page_size") or "200") or 200
if page < 1 then page = 1 end
if page_size < 1 then page_size = 200 end
if page_size > 200 then page_size = 200 end
local resp_obj = get_record_whitelist_page(page, page_size)
if resp_obj and resp_obj.code == 2000 and resp_obj.data then
write_json({code = 2000, data = resp_obj.data, message = "success"})
else
write_json({code = 2000, data = {total_num = 0, total_page = 1, page = page, page_size = page_size, list = {}}, message = "success"})
end
end
function get_record_whitelist_all()
local all_list = {}
local total_page = 1
local page = 1
local page_size = 200
while page <= total_page do
local resp_obj = get_record_whitelist_page(page, page_size)
if not (resp_obj and resp_obj.code == 2000 and resp_obj.data and type(resp_obj.data.list) == "table") then
break
end
local i
for i = 1, #resp_obj.data.list do
all_list[#all_list + 1] = resp_obj.data.list[i]
end
total_page = tonumber(resp_obj.data.total_page or 1) or 1
if total_page < 1 then total_page = 1 end
page = page + 1
end
write_json({code = 2000, data = {list = all_list}, message = "success"})
end
function add_record_whitelist()
local data_str = luci.http.formvalue("data")
local mac = normalize_mac(luci.http.formvalue("mac"))
local mac_list = {}
if data_str and data_str ~= "" then
local parsed = jsonc.parse(data_str)
if parsed and type(parsed.mac_list) == "table" then
local i
for i = 1, #parsed.mac_list do
local one_mac = normalize_mac(parsed.mac_list[i])
if is_valid_mac(one_mac) then
mac_list[#mac_list + 1] = one_mac
end
end
end
end
if #mac_list == 0 and is_valid_mac(mac) then
mac_list = {mac}
end
if #mac_list == 0 then
write_json({code = 1, message = "Invalid mac_list"})
return
end
local resp_obj = util.ubus("fwx", "common", {
api = "add_record_whitelist",
data = {mac_list = mac_list}
})
if resp_obj and resp_obj.code == 2000 then
write_json({code = 2000, message = "Whitelist added successfully"})
else
write_json({code = 1, message = "Failed to add whitelist"})
end
end
function del_record_whitelist()
local mac = normalize_mac(luci.http.formvalue("mac"))
if not is_valid_mac(mac) then
write_json({code = 1, message = "Invalid mac address"})
return
end
local resp_obj = util.ubus("fwx", "common", {
api = "del_record_whitelist",
data = {mac = mac}
})
if resp_obj and resp_obj.code == 2000 then
write_json({code = 2000, message = "Whitelist deleted successfully"})
else
write_json({code = 1, message = "Failed to delete whitelist"})
end
end
function get_all_users()
local all_list = {}
local total_page = 1
local page = 1
local page_size = 200
while page <= total_page do
local req_obj = {
api = "get_all_users",
data = {
flag = 2,
page = page,
page_size = page_size
}
}
local resp_obj = util.ubus("fwx", "common", req_obj)
if not (resp_obj and resp_obj.code == 2000 and resp_obj.data and type(resp_obj.data.list) == "table") then
break
end
local i
for i = 1, #resp_obj.data.list do
all_list[#all_list + 1] = resp_obj.data.list[i]
end
total_page = tonumber(resp_obj.data.total_page or 1) or 1
if total_page < 1 then total_page = 1 end
page = page + 1
end
write_json({code = 2000, data = {list = all_list}, message = "success"})
end
@@ -0,0 +1,20 @@
local ds = require "luci.dispatcher"
local nxo = require "nixio"
local nfs = require "nixio.fs"
local ipc = require "luci.ip"
local sys = require "luci.sys"
local utl = require "luci.util"
local dsp = require "luci.dispatcher"
local uci = require "luci.model.uci"
local lng = require "luci.i18n"
local jsc = require "luci.jsonc"
local m, s
arg[1] = arg[1] or ""
m = Map("app_filter", translate(""), translate(""))
local v
v = m:section(SimpleSection)
v.template = "oaf/app_filter/rules"
return m
@@ -0,0 +1,19 @@
local ds = require "luci.dispatcher"
local nxo = require "nixio"
local nfs = require "nixio.fs"
local ipc = require "luci.ip"
local sys = require "luci.sys"
local utl = require "luci.util"
local dsp = require "luci.dispatcher"
local uci = require "luci.model.uci"
local lng = require "luci.i18n"
local jsc = require "luci.jsonc"
local m, s
arg[1] = arg[1] or ""
m = Map("fwx_dashboard", translate(""), translate(""))
local v
v = m:section(SimpleSection)
v.template = "oaf/dashboard"
return m
@@ -0,0 +1,20 @@
local ds = require "luci.dispatcher"
local nxo = require "nixio"
local nfs = require "nixio.fs"
local ipc = require "luci.ip"
local sys = require "luci.sys"
local utl = require "luci.util"
local dsp = require "luci.dispatcher"
local uci = require "luci.model.uci"
local lng = require "luci.i18n"
local jsc = require "luci.jsonc"
local m, s
arg[1] = arg[1] or ""
m = Map("macfilter", translate(""), translate(""))
local v
v = m:section(SimpleSection)
v.template = "oaf/mac_filter/mac_filter"
return m
@@ -0,0 +1,19 @@
local ds = require "luci.dispatcher"
local nxo = require "nixio"
local nfs = require "nixio.fs"
local ipc = require "luci.ip"
local sys = require "luci.sys"
local utl = require "luci.util"
local dsp = require "luci.dispatcher"
local uci = require "luci.model.uci"
local lng = require "luci.i18n"
local jsc = require "luci.jsonc"
local m, s
arg[1] = arg[1] or ""
m = Map("macfilter", translate(""), translate(""))
local v
v = m:section(SimpleSection)
v.template = "oaf/mac_filter/rules"
return m
@@ -0,0 +1,19 @@
local ds = require "luci.dispatcher"
local nxo = require "nixio"
local nfs = require "nixio.fs"
local ipc = require "luci.ip"
local sys = require "luci.sys"
local utl = require "luci.util"
local dsp = require "luci.dispatcher"
local uci = require "luci.model.uci"
local lng = require "luci.i18n"
local jsc = require "luci.jsonc"
local m, s
arg[1] = arg[1] or ""
m = Map("macfilter", translate(""), translate(""))
local v
v = m:section(SimpleSection)
v.template = "oaf/mac_filter/user"
return m
@@ -0,0 +1,9 @@
local ds = require "luci.dispatcher"
local m, s
m = Map("appfilter", translate(""), translate(""))
m:section(SimpleSection).template = "oaf/user_detail"
return m
@@ -0,0 +1,19 @@
local ds = require "luci.dispatcher"
local nxo = require "nixio"
local nfs = require "nixio.fs"
local ipc = require "luci.ip"
local sys = require "luci.sys"
local utl = require "luci.util"
local dsp = require "luci.dispatcher"
local uci = require "luci.model.uci"
local lng = require "luci.i18n"
local jsc = require "luci.jsonc"
local http = luci.http
local SYS = require "luci.sys"
local m, s
m = Map("appfilter", translate(""), translate(""))
m:section(SimpleSection).template = "oaf/user_status"
return m
@@ -0,0 +1,15 @@
local nfs = require "nixio.fs"
local config = "fwx_record"
if nfs.access("/etc/config/appfilter") then
config = "appfilter"
elseif nfs.access("/etc/config/macfilter") then
config = "macfilter"
end
local m = Map(config, translate(""), translate(""))
local s = m:section(SimpleSection)
s.template = "oaf/whitelist"
return m
Binary file not shown.
+128
View File
@@ -0,0 +1,128 @@
<%+header%>
<link rel="stylesheet" href="<%=resource%>/oaf/css/common.css">
<div class="oaf-about-page">
<section class="oaf-about-card">
<div class="oaf-about-brand">OpenAppFilter</div>
<p class="oaf-about-intro">
<%:OAF is a powerful parental control software. FanchmWrt integrates OAF by default and includes more plugins developed by the author.%>
</p>
<div class="oaf-about-info">
<div class="oaf-about-row">
<div class="oaf-about-label"><%:Author%></div>
<div class="oaf-about-value">destan19(TT)</div>
</div>
<div class="oaf-about-row">
<div class="oaf-about-label">OAF</div>
<div class="oaf-about-value">
<a href="http://www.openappfilter.com" target="_blank" rel="noopener noreferrer">www.openappfilter.com</a>
</div>
</div>
<div class="oaf-about-row">
<div class="oaf-about-label">FanchmWrt</div>
<div class="oaf-about-value">
<a href="http://www.fanchmwrt.com" target="_blank" rel="noopener noreferrer">www.fanchmwrt.com</a>
</div>
</div>
<div class="oaf-about-row">
<div class="oaf-about-label"><%:GitHub Source%></div>
<div class="oaf-about-value">
<a href="https://github.com/destan19/OpenAppFilter" target="_blank" rel="noopener noreferrer">https://github.com/destan19/OpenAppFilter</a>
</div>
</div>
</div>
<div class="oaf-about-copyright">
Copyright © 2026 FanchmWrt. All rights reserved.
</div>
</section>
</div>
<style>
.oaf-about-page {
padding: 10px;
}
.oaf-about-card {
padding: 22px;
border: 1px solid var(--border-color-low, #e5e7eb);
border-radius: 8px;
background: var(--background-color-high, #fff);
color: var(--text-color-high, #333);
max-width: 860px;
}
.oaf-about-brand {
font-size: 28px;
font-weight: 700;
line-height: 1.25;
margin-bottom: 6px;
}
.oaf-about-intro {
margin: 0 0 20px;
color: var(--text-color-high, #333);
font-size: 16px;
line-height: 1.7;
}
.oaf-about-info {
display: grid;
gap: 10px;
max-width: 760px;
margin-bottom: 22px;
}
.oaf-about-row {
display: grid;
grid-template-columns: 130px minmax(0, 1fr);
gap: 16px;
align-items: center;
padding: 12px 14px;
border: 1px solid var(--border-color-low, #e5e7eb);
border-radius: 6px;
background: var(--background-color-low, #f5f5f5);
font-size: 15px;
}
.oaf-about-label {
color: var(--text-color-medium, #6b7280);
font-weight: 600;
}
.oaf-about-value {
min-width: 0;
color: var(--text-color-high, #333);
word-break: break-word;
}
.oaf-about-value a {
color: var(--primary-color, #2563eb);
text-decoration: none;
}
.oaf-about-value a:hover {
text-decoration: underline;
}
.oaf-about-copyright {
padding-top: 14px;
border-top: 1px solid var(--border-color-low, #e5e7eb);
color: var(--text-color-medium, #6b7280);
font-size: 14px;
}
@media (max-width: 600px) {
.oaf-about-card {
padding: 16px;
}
.oaf-about-row {
grid-template-columns: 1fr;
gap: 5px;
}
}
</style>
<%+footer%>
+462
View File
@@ -0,0 +1,462 @@
<%
local nfs = require "nixio.fs"
local has_app_filter = nfs.access("/etc/config/appfilter")
local has_access_control = nfs.access("/etc/config/macfilter")
%>
<%+header%>
<link rel="stylesheet" href="<%=resource%>/oaf/css/common.css">
<script src="<%=resource%>/oaf/jquery-3.7.1.min.js"></script>
<script src="<%=resource%>/oaf/common.js"></script>
<div class="settings-page">
<div class="settings-heading"><%:Settings%></div>
<section class="settings-panel">
<button type="button" class="settings-panel-header" aria-expanded="true" aria-controls="basic-settings-panel">
<span><%:Basic Settings%></span>
<span class="settings-chevron" aria-hidden="true"></span>
</button>
<div class="settings-panel-body" id="basic-settings-panel">
<div class="form-row">
<label class="form-label" for="work-mode"><%:Work Mode%></label>
<select id="work-mode">
<option value="0"><%:Gateway Mode%></option>
<option value="1"><%:Bypass Mode%></option>
</select>
</div>
<ol class="work-mode-warning" id="work-mode-warning">
<li><%:Bypass mode cannot count downstream traffic because traffic is directly forwarded to terminals at Layer 2 through the gateway.%></li>
<li><%:In bypass mode, it is recommended to disable IPv6 on terminals to prevent terminals from forwarding directly to the main router through IPv6.%></li>
</ol>
<div class="form-row">
<label class="form-label" for="lan-ifname"><%:LAN Interface%></label>
<div class="settings-field">
<input id="lan-ifname" type="text" placeholder="br-lan" class="input-short">
<div class="field-error" id="lan-ifname-error"></div>
<div class="form-desc-inline"><%:LAN interface name for terminal detection. Default is bridge interface (br-lan). If LAN port is changed to physical interface, please modify to the corresponding name, such as eth0.%></div>
</div>
</div>
<div class="form-row">
<label class="form-label" for="tcp-rst"><%:TCP Connection Reset%></label>
<div class="settings-field">
<label class="switch">
<input type="checkbox" id="tcp-rst">
<span class="slider"></span>
</label>
<div class="form-desc-inline"><%:After enabled, OAF sends TCP RST for filtered TCP connections.%></div>
</div>
</div>
<% if has_app_filter then %>
<div class="form-row">
<label class="form-label" for="app-filter-enable"><%:Enable App Filter%></label>
<label class="switch">
<input type="checkbox" id="app-filter-enable">
<span class="slider"></span>
</label>
</div>
<% end %>
<% if has_access_control then %>
<div class="form-row">
<label class="form-label" for="access-control-enable"><%:Enable Access Control%></label>
<label class="switch">
<input type="checkbox" id="access-control-enable">
<span class="slider"></span>
</label>
</div>
<% end %>
</div>
</section>
<section class="settings-panel">
<button type="button" class="settings-panel-header" aria-expanded="true" aria-controls="audit-settings-panel">
<span><%:Internet Audit%></span>
<span class="settings-chevron" aria-hidden="true"></span>
</button>
<div class="settings-panel-body" id="audit-settings-panel">
<div class="form-row">
<label class="form-label" for="rec-enable"><%:Enable%></label>
<label class="switch">
<input type="checkbox" id="rec-enable">
<span class="slider"></span>
</label>
</div>
<div class="form-row" id="data-size-group" style="display:none;">
<label class="form-label"><%:History Data Usage%></label>
<div class="data-size-field">
<span id="data-size-display"></span>
<button type="button" class="btn btn-secondary data-clean-btn" id="clean-history"><%:Clean%></button>
</div>
</div>
<div class="form-row">
<label class="form-label" for="rec-keep"><%:Record Time%></label>
<div class="settings-field">
<select id="rec-keep">
<option value="1">1 <%:days%></option>
<option value="2">2 <%:days%></option>
<option value="3">3 <%:days%></option>
<option value="4">4 <%:days%></option>
<option value="5">5 <%:days%></option>
<option value="6">6 <%:days%></option>
<option value="7">7 <%:days%></option>
<option value="10">10 <%:days%></option>
<option value="15">15 <%:days%></option>
<option value="30">30 <%:days%></option>
</select>
<div class="form-desc-inline"><%:Internet record retention time%></div>
</div>
</div>
<div class="form-row">
<label class="form-label" for="rec-valid"><%:App Valid Time%></label>
<div class="settings-field">
<select id="rec-valid">
<option value="1">1 <%:minutes%></option>
<option value="2">2 <%:minutes%></option>
<option value="3">3 <%:minutes%></option>
<option value="4">4 <%:minutes%></option>
<option value="5">5 <%:minutes%></option>
<option value="10">10 <%:minutes%></option>
<option value="15">15 <%:minutes%></option>
<option value="20">20 <%:minutes%></option>
<option value="30">30 <%:minutes%></option>
<option value="60">60 <%:minutes%></option>
</select>
<div class="form-desc-inline"><%:App records minimum duration%></div>
</div>
</div>
<div class="form-row">
<label class="form-label" for="rec-history-size"><%:History Data Size%></label>
<div class="settings-field">
<div class="size-field">
<input type="text" id="rec-history-size">
<span class="unit">MB</span>
</div>
<div class="form-desc-inline"><%:History data size limit%></div>
</div>
</div>
<div class="form-row">
<label class="form-label" for="rec-history-path"><%:History Data Path%></label>
<div class="settings-field">
<input type="text" id="rec-history-path">
<div class="form-desc-inline"><%:User's App record data will be stored in this directory.%></div>
</div>
</div>
<div class="form-row">
<label class="form-label" for="rec-base-path"><%:Base Data Path%></label>
<div class="settings-field">
<input type="text" id="rec-base-path">
<div class="form-desc-inline"><%:The user's basic data and real-time data are stored in this directory, such as online duration, traffic usage and other information. The default is a temporary directory and will not be retained after reboot. You can change it to a persistent directory, such as /etc/fwx.%></div>
</div>
</div>
</div>
</section>
<section class="settings-panel">
<button type="button" class="settings-panel-header" aria-expanded="true" aria-controls="advanced-settings-panel">
<span><%:Advanced Settings%></span>
<span class="settings-chevron" aria-hidden="true"></span>
</button>
<div class="settings-panel-body" id="advanced-settings-panel">
<div class="form-row">
<label class="form-label" for="disable-hnat"><%:Auto Disable Acceleration%></label>
<div class="settings-field">
<label class="switch">
<input type="checkbox" id="disable-hnat">
<span class="slider"></span>
</label>
<div class="form-desc-inline"><%:After enabled, related acceleration modules will be automatically disabled, including hardware acceleration, software acceleration and others, to prevent filtering failures caused by acceleration. It is recommended to restart the device after modification.%></div>
</div>
</div>
</div>
</section>
<div class="settings-actions">
<button class="btn btn-secondary" type="button" id="settings-refresh"><%:Refresh%></button>
<button class="btn" type="button" id="settings-save" disabled><%:Save%></button>
</div>
<div class="status-tip" id="settings-tip"></div>
</div>
<style>
.settings-page{max-width:860px;margin:0 auto 16px;padding:0 10px}.settings-heading{font-size:20px;font-weight:600;margin:8px 0 14px}.settings-panel{border:1px solid var(--border-color-low,#e5e7eb);border-radius:7px;margin-bottom:12px;background:var(--background-color-high,transparent);overflow:hidden}.settings-panel-header{width:100%;border:0;border-bottom:1px solid var(--border-color-low,#e5e7eb);background:var(--background-color-low,rgba(0,0,0,.025));color:inherit;display:flex;align-items:center;justify-content:space-between;padding:12px 16px;font-size:16px;font-weight:600;text-align:left;cursor:pointer}.settings-panel-header.collapsed{border-bottom-color:transparent}.settings-chevron{width:8px;height:8px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg);transition:transform .15s ease;margin:0 4px 4px 12px}.settings-panel-header.collapsed .settings-chevron{transform:rotate(-45deg);margin-bottom:0}.settings-panel-body{padding:16px}.settings-panel-body .form-row{align-items:flex-start;margin-bottom:16px}.settings-panel-body .switch{margin-top:2px;flex:none}.settings-field{flex:1;min-width:0}.settings-field>input,.settings-field>select{width:360px;max-width:100%}.form-desc-inline{margin-top:5px;font-size:12px;line-height:1.5;color:var(--text-color-medium,#6b7280)}.field-error{display:none;color:#f87171;font-size:12px;margin-top:4px}.work-mode-warning{display:none;margin:0 0 16px 150px;padding:10px 14px 10px 30px;border:1px solid rgba(239,68,68,.4);border-radius:6px;background:rgba(239,68,68,.08);color:#dc2626;font-size:13px;line-height:1.6}.work-mode-warning li{margin:2px 0}.data-size-field,.size-field{display:flex;align-items:center;gap:10px}.data-clean-btn{min-width:64px}.size-field input{width:320px!important}.size-field .unit{color:var(--text-color-medium,#6b7280)}.settings-actions{display:flex;justify-content:flex-end;gap:8px;padding-top:4px}.settings-page>.status-tip{text-align:right}.settings-panel-header:focus-visible{outline:2px solid #3b82f6;outline-offset:-2px}[data-darkmode="true"] .work-mode-warning{border-color:rgba(248,113,113,.55);background:rgba(127,29,29,.26);color:#fca5a5}
@media(max-width:600px){.settings-page{padding:0 4px}.settings-panel-header{padding:11px 12px}.settings-panel-body{padding:13px 12px}.settings-panel-body .form-row{display:block}.settings-panel-body .form-label{display:block;width:auto;margin-bottom:7px}.work-mode-warning{margin-left:0}.settings-field>input,.settings-field>select,.settings-panel-body>.form-row>select{width:100%}.size-field input{width:100%!important;min-width:0}.settings-actions{padding-right:2px}}
</style>
<script type="text/javascript">//<![CDATA[
(function() {
var text = {
saved: '<%:Saved successfully%>',
saveFailed: '<%:Save failed%>',
loading: '<%:Loading...%>',
loadFailed: '<%:Load failed%>',
invalidLan: '<%:Invalid LAN interface name%>',
invalidSize: '<%:History data size must be an integer between 1 and 1024 MB%>',
emptyHistoryPath: '<%:History data path cannot be empty or /%>',
longHistoryPath: '<%:History data path maximum length is 64 characters%>',
emptyBasePath: '<%:Base data path cannot be empty or /%>',
longBasePath: '<%:Base data path maximum length is 64 characters%>',
cleanConfirm: '<%:Are you sure you want to clean all history data? This action cannot be undone.%>',
cleanSuccess: '<%:Data cleaned successfully%>',
cleanFailed: '<%:Clean failed%>'
};
var api = {
workModeGet: '<%=url("admin/services/oaf/api/system/get_work_mode")%>',
workModeSet: '<%=url("admin/services/oaf/api/system/set_work_mode")%>',
systemGet: '<%=url("admin/services/oaf/api/system/get_system_info")%>',
systemSet: '<%=url("admin/services/oaf/api/system/set_system_info")%>',
tcpRstGet: '<%=url("admin/services/oaf/api/system/get_tcp_rst")%>',
tcpRstSet: '<%=url("admin/services/oaf/api/system/set_tcp_rst")%>',
appFilterGet: '<%=url("admin/services/oaf/api/app_filter/get_app_filter_adv")%>',
appFilterSet: '<%=url("admin/services/oaf/api/app_filter/set_app_filter_adv")%>',
accessControlGet: '<%=url("admin/services/oaf/api/mac_filter/get_mac_filter_adv")%>',
accessControlSet: '<%=url("admin/services/oaf/api/mac_filter/set_mac_filter_adv")%>',
advancedGet: '<%=url("admin/services/oaf/api/system/get_advanced_settings")%>',
advancedSet: '<%=url("admin/services/oaf/api/system/set_advanced_settings")%>',
recordGet: '<%=url("admin/services/oaf/api/internet_audit/get_record_base")%>',
recordSet: '<%=url("admin/services/oaf/api/internet_audit/set_record_base")%>',
recordAction: '<%=url("admin/services/oaf/api/internet_audit/record_action")%>'
};
var currentThemeMode = 0;
var settingsLoaded = false;
function isSuccess(resp) {
return !!resp && (resp.code === 0 || resp.code === 2000);
}
function showResult(message, success) {
if (typeof showCommonModal === 'function') {
showCommonModal(message || '', 2000, success ? 'success' : 'error');
}
}
function runTasks(tasks, done) {
var remaining = tasks.length;
var success = true;
if (!remaining) {
done(true);
return;
}
tasks.forEach(function(task) {
task(function(ok) {
success = success && !!ok;
remaining--;
if (!remaining) done(success);
});
});
}
function loadTask(url, applyData) {
return function(done) {
$.getJSON(url).done(function(resp) {
if (!isSuccess(resp) || !resp.data) {
done(false);
return;
}
done(applyData(resp.data) !== false);
}).fail(function() {
done(false);
});
};
}
function postTask(url, data) {
return function(done) {
$.ajax({url:url,method:'POST',dataType:'json',data:data}).done(function(resp) {
done(isSuccess(resp));
}).fail(function() {
done(false);
});
};
}
function updateWorkModeWarning(value) {
$('#work-mode-warning').toggle(parseInt(value, 10) === 1);
}
function applyRecordData(data) {
$('#rec-enable').prop('checked', data.enable == 1);
$('#rec-keep').val(data.record_time || 0);
$('#rec-valid').val(data.app_valid_time || 0);
$('#rec-history-size').val(data.history_data_size || '');
$('#rec-history-path').val(data.history_data_path || '');
$('#rec-base-path').val(data.base_data_path || data.terminal_data_path || '');
if (data.status && data.status.data_size !== undefined) {
var size = data.status.data_size || 0;
var unit = data.status.data_size_unit || 'KB';
var display = size + unit;
if (unit === 'KB' && size >= 1024) {
var mb = size / 1024;
display = (mb % 1 === 0 ? mb.toFixed(0) : mb.toFixed(1)) + 'MB';
}
$('#data-size-display').text(display);
$('#data-size-group').show();
} else {
$('#data-size-group').hide();
}
}
function refreshAll() {
var tasks = [];
settingsLoaded = false;
$('#settings-refresh,#settings-save').prop('disabled', true);
$('#settings-tip').text(text.loading);
tasks.push(loadTask(api.workModeGet, function(data) {
var value = parseInt(data.work_mode, 10) === 1 ? '1' : '0';
$('#work-mode').val(value);
updateWorkModeWarning(value);
}));
tasks.push(loadTask(api.systemGet, function(data) {
var fwx = data.fwx;
if (!fwx) return false;
$('#lan-ifname').val(fwx.lan_ifname || 'br-lan');
currentThemeMode = fwx.theme_mode == 1 ? 1 : 0;
}));
tasks.push(loadTask(api.tcpRstGet, function(data) {
$('#tcp-rst').prop('checked', data.tcp_rst != 0);
}));
<% if has_app_filter then %>
tasks.push(loadTask(api.appFilterGet, function(data) {
$('#app-filter-enable').prop('checked', data.enable == 1);
}));
<% end %>
<% if has_access_control then %>
tasks.push(loadTask(api.accessControlGet, function(data) {
$('#access-control-enable').prop('checked', data.enable == 1);
}));
<% end %>
tasks.push(loadTask(api.advancedGet, function(data) {
$('#disable-hnat').prop('checked', data.disable_hnat == 1);
}));
tasks.push(loadTask(api.recordGet, applyRecordData));
runTasks(tasks, function(success) {
settingsLoaded = success;
$('#settings-refresh').prop('disabled', false);
$('#settings-save').prop('disabled', !success);
$('#settings-tip').text(success ? '' : text.loadFailed);
});
}
function validateSettings() {
var lanIfname = $.trim($('#lan-ifname').val() || '');
var historySize = $.trim($('#rec-history-size').val() || '');
var historyPath = $.trim($('#rec-history-path').val() || '');
var basePath = $.trim($('#rec-base-path').val() || '');
$('#lan-ifname-error').hide();
$('#lan-ifname').css('border-color', '');
if (!/^[a-zA-Z0-9-]{2,16}$/.test(lanIfname)) {
$('#lan-ifname-error').text(text.invalidLan).show();
$('#lan-ifname').css('border-color', '#f87171');
return null;
}
if (historySize) {
var size = Number(historySize);
if (!/^\d+$/.test(historySize) || isNaN(size) || size < 1 || size > 1024) {
showResult(text.invalidSize, false);
return null;
}
}
if (!historyPath || historyPath === '/') {
showResult(text.emptyHistoryPath, false);
return null;
}
if (historyPath.length > 64) {
showResult(text.longHistoryPath, false);
return null;
}
if (!basePath || basePath === '/') {
showResult(text.emptyBasePath, false);
return null;
}
if (basePath.length > 64) {
showResult(text.longBasePath, false);
return null;
}
return {lanIfname:lanIfname,historySize:historySize,historyPath:historyPath,basePath:basePath};
}
function saveAll() {
var values;
var tasks = [];
if (!settingsLoaded) {
showResult(text.loadFailed, false);
return;
}
values = validateSettings();
if (!values) return;
tasks.push(postTask(api.workModeSet, {work_mode:$('#work-mode').val() || '0'}));
tasks.push(postTask(api.systemSet, {lan_ifname:values.lanIfname,theme_mode:currentThemeMode}));
tasks.push(postTask(api.tcpRstSet, {tcp_rst:$('#tcp-rst').is(':checked') ? 1 : 0}));
<% if has_app_filter then %>
tasks.push(postTask(api.appFilterSet, {enable:$('#app-filter-enable').is(':checked') ? 1 : 0}));
<% end %>
<% if has_access_control then %>
tasks.push(postTask(api.accessControlSet, {enable:$('#access-control-enable').is(':checked') ? 1 : 0}));
<% end %>
tasks.push(postTask(api.advancedSet, {disable_hnat:$('#disable-hnat').is(':checked') ? 1 : 0}));
tasks.push(postTask(api.recordSet, {
enable:$('#rec-enable').is(':checked') ? 1 : 0,
record_time:parseInt($('#rec-keep').val(), 10) || 0,
app_valid_time:parseInt($('#rec-valid').val(), 10) || 0,
history_data_size:values.historySize,
history_data_path:values.historyPath,
base_data_path:values.basePath
}));
$('#settings-refresh,#settings-save').prop('disabled', true);
runTasks(tasks, function(success) {
$('#settings-refresh,#settings-save').prop('disabled', false);
updateWorkModeWarning($('#work-mode').val());
showResult(success ? text.saved : text.saveFailed, success);
});
}
function cleanHistory() {
if (!window.confirm(text.cleanConfirm)) return;
$('#clean-history').prop('disabled', true);
$.ajax({url:api.recordAction,method:'POST',dataType:'json',data:{action:'clean_all_data'}}).done(function(resp) {
if (isSuccess(resp)) {
showResult(text.cleanSuccess, true);
loadTask(api.recordGet, applyRecordData)(function() {});
} else {
showResult(text.cleanFailed, false);
}
}).fail(function() {
showResult(text.cleanFailed, false);
}).always(function() {
$('#clean-history').prop('disabled', false);
});
}
$('.settings-panel-header').on('click', function() {
var header = $(this);
var panel = $('#' + header.attr('aria-controls'));
var expanded = header.attr('aria-expanded') === 'true';
header.attr('aria-expanded', expanded ? 'false' : 'true').toggleClass('collapsed', expanded);
panel.stop(true, true).slideToggle(120);
});
$('#work-mode').on('change', function() { updateWorkModeWarning(this.value); });
$('#settings-refresh').on('click', refreshAll);
$('#settings-save').on('click', saveAll);
$('#clean-history').on('click', cleanHistory);
$(refreshAll);
})();
//]]></script>
<%+footer%>
File diff suppressed because it is too large. Load diff
+437
View File
@@ -0,0 +1,437 @@
<%+header%>
<link rel="stylesheet" href="<%=resource%>/oaf/css/common.css">
<script src="<%=resource%>/oaf/jquery-3.7.1.min.js"></script>
<script src="<%=resource%>/oaf/oaf_icon.js"></script>
<div style="padding: 8px;">
<div class="cbi-section">
<ul class="tab-list">
<li class="tab-item active" data-tab="online-records"><%:Online Records%></li>
<li class="tab-item" data-tab="history-records"><%:History Records%></li>
</ul>
<div id="tab-online-records" class="tab-body active">
<table class="table cbi-section-table" id="online-record-table" style="table-layout: fixed; width: 100%;">
<colgroup>
<col class="record-app-col">
<col class="record-mac-col">
<col class="record-hostname-col">
<col class="record-nickname-col">
<col class="record-time-col">
<col class="record-time-col">
<col class="record-duration-col">
<col class="record-status-col">
<col class="record-status-col">
</colgroup>
<tr class="tr table-titles">
<th class="th"><%:App%></th>
<th class="th">MAC</th>
<th class="th"><%:Hostname%></th>
<th class="th"><%:Nickname%></th>
<th class="th"><%:First Visit%></th>
<th class="th"><%:Last Visit%></th>
<th class="th"><%:Duration%></th>
<th class="th"><%:Online%></th>
<th class="th"><%:Filter Status%></th>
</tr>
<tr class="tr">
<td class="td" colspan="9"><em><br /><%:Loading...%></em></td>
</tr>
</table>
<div id="online-pagination" style="margin-top: 12px;"></div>
</div>
<div id="tab-history-records" class="tab-body">
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:10px 0 12px 0;">
<input id="q-history-mac" class="cbi-input-text" type="text" placeholder="<%:Terminal MAC%>" style="width:200px;">
<input id="q-history-start" class="cbi-input-text" type="datetime-local" style="width:210px;">
<input id="q-history-end" class="cbi-input-text" type="datetime-local" style="width:210px;">
<button type="button" class="cbi-button cbi-button-action" id="btn-history-search"><%:Search%></button>
<button type="button" class="cbi-button" id="btn-history-reset"><%:Reset%></button>
</div>
<table class="table cbi-section-table" id="history-record-table" style="table-layout: fixed; width: 100%;">
<colgroup>
<col class="record-app-col">
<col class="record-mac-col">
<col class="record-hostname-col">
<col class="record-nickname-col">
<col class="record-time-col">
<col class="record-time-col">
<col class="record-duration-col">
<col class="record-status-col">
<col class="record-status-col">
</colgroup>
<tr class="tr table-titles">
<th class="th"><%:App%></th>
<th class="th">MAC</th>
<th class="th"><%:Hostname%></th>
<th class="th"><%:Nickname%></th>
<th class="th"><%:First Visit%></th>
<th class="th"><%:Last Visit%></th>
<th class="th"><%:Duration%></th>
<th class="th"><%:Online%></th>
<th class="th"><%:Filter Status%></th>
</tr>
<tr class="tr">
<td class="td" colspan="9"><em><br /><%:Loading...%></em></td>
</tr>
</table>
<div id="history-pagination" style="margin-top: 12px;"></div>
</div>
</div>
</div>
<style>
.tab-list {
display: flex;
list-style: none;
padding: 0;
margin: 0;
border-bottom: 1px solid var(--border-color-medium, #d1d5db);
}
.tab-item {
padding: 3px 16px;
cursor: pointer;
border: 1px solid var(--border-color-medium, #d1d5db);
border-bottom: none;
border-radius: 5px 5px 0 0;
margin-right: 2px;
background-color: var(--background-color-low, #f5f5f5);
color: var(--text-color-medium, #6b7280);
}
.tab-item.active {
font-weight: bold;
background-color: var(--background-color-high, #ffffff);
color: var(--text-color-high, #333);
}
.tab-item:hover {
background-color: var(--background-color-medium, #eeeeee);
}
.tab-body {
margin-top: 6px;
display: none;
}
.tab-body.active {
display: block;
}
#online-record-table .th, #history-record-table .th {
padding-top: 12px;
padding-bottom: 12px;
}
#online-record-table .td, #history-record-table .td {
padding-top: 12px;
padding-bottom: 12px;
line-height: 1.55;
}
#online-record-table,
#history-record-table {
--record-app-col-width: 138px;
--record-app-cell-padding: 14px;
--record-app-icon-size: 22px;
--record-app-icon-gap: 7px;
}
#online-record-table .record-app-col,
#history-record-table .record-app-col {
width: var(--record-app-col-width);
}
#online-record-table .record-mac-col,
#history-record-table .record-mac-col {
width: 15%;
}
#online-record-table .record-hostname-col,
#history-record-table .record-hostname-col,
#online-record-table .record-nickname-col,
#history-record-table .record-nickname-col {
width: 11%;
}
#online-record-table .record-time-col,
#history-record-table .record-time-col {
width: 15%;
}
#online-record-table .record-duration-col,
#history-record-table .record-duration-col {
width: 8%;
}
#online-record-table .record-status-col,
#history-record-table .record-status-col {
width: 6%;
}
#online-record-table .td:nth-child(5),
#online-record-table .td:nth-child(6),
#history-record-table .td:nth-child(5),
#history-record-table .td:nth-child(6) {
white-space: nowrap;
}
.record-app-cell {
display: flex;
align-items: center;
gap: var(--record-app-icon-gap);
min-width: 0;
width: calc(var(--record-app-col-width) - var(--record-app-cell-padding));
}
.record-app-cell .record-app-name {
min-width: 0;
max-width: calc(var(--record-app-col-width) - var(--record-app-cell-padding) - var(--record-app-icon-size) - var(--record-app-icon-gap));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
<script type="text/javascript">
//<![CDATA[
(function() {
var text = {
filtered: '<%:Filtered%>',
unfiltered: '<%:Unfiltered%>',
online: '<%:Online%>',
offline: '<%:Offline%>',
noData: '<%:No Data%>',
previous: '<%:Previous%>',
next: '<%:Next%>',
total: '<%:Total%>',
records: '<%:Records%>'
};
var online_state = { page: 1, page_size: 15, total_num: 0, total_page: 1 };
var history_state = { page: 1, page_size: 15, total_num: 0, total_page: 1 };
var history_loaded = false;
function pad2(n) {
n = parseInt(n || 0, 10);
return n < 10 ? ("0" + n) : ("" + n);
}
function format_time(ts) {
var t = parseInt(ts || 0, 10);
if (!t) return "--";
var d = new Date(t * 1000);
if (isNaN(d.getTime())) return "--";
return d.getFullYear() + "-" + pad2(d.getMonth() + 1) + "-" + pad2(d.getDate()) + " "
+ pad2(d.getHours()) + ":" + pad2(d.getMinutes());
}
function format_duration(sec) {
var s = parseInt(sec || 0, 10);
if (s < 0) s = 0;
var h = Math.floor(s / 3600);
var m = Math.floor((s % 3600) / 60);
var r = s % 60;
if (h > 0) return h + "h" + (m > 0 ? " " + m + "m" : "");
if (m > 0) return m + "m" + (r > 0 ? " " + r + "s" : "");
return r + "s";
}
function get_ts_from_input(selector) {
var v = $(selector).val();
if (!v) return 0;
var ms = new Date(v).getTime();
if (!ms || ms < 0) return 0;
return Math.floor(ms / 1000);
}
function render_filter_status(act) {
return parseInt(act || 0, 10) === 1
? '<span style="color:#F44336;">' + text.filtered + '</span>'
: '<span style="color:#22c55e;">' + text.unfiltered + '</span>';
}
function render_online_status(online) {
return parseInt(online || 0, 10) === 1
? '<span style="color:#22c55e;">' + text.online + '</span>'
: '<span style="color:#F44336;">' + text.offline + '</span>';
}
function app_display_name(app_id, app_name) {
var id = app_id === undefined || app_id === null ? "" : String(app_id);
var name = $.trim(app_name || "");
if (!name || name === "--" || name === "Unknown" || name === "Unknown App") {
return id && id !== "0" ? ("App" + id) : "--";
}
return name;
}
function render_app_cell($cell, app_id, app_name, app_icon) {
var id = app_id === undefined || app_id === null ? "" : String(app_id);
var name = app_display_name(id, app_name);
var $wrap = $('<div>').addClass('record-app-cell').attr('title', name);
var $icon = window.OAFIcon
? window.OAFIcon.createAppIcon(id, name, '<%=resource%>', { size: '22px', radius: '5px', icon: app_icon })
: $('<span>').text('');
$wrap.append($icon).append($('<span>').addClass('record-app-name').text(name));
$cell.empty().append($wrap);
}
function render_table(table_id, list) {
var $tb = $("#" + table_id);
$tb.find("tr:gt(0)").remove();
if (!Array.isArray(list) || list.length === 0) {
$tb.append('<tr class="tr"><td class="td" colspan="9"><em><br />' + text.noData + '</em></td></tr>');
return;
}
list.forEach(function(item) {
var app_id = item.id || item.appid || "";
var app_name = app_display_name(app_id, item.name || item.appname || "");
var mac = item.mac || "--";
var hostname = item.hostname || "--";
var nickname = item.nickname || "--";
var ft = item.ft || item.first_time || 0;
var lt = item.lt || item.latest_time || 0;
var tt = item.tt || item.total_time || 0;
var act = item.act || item.latest_action || 0;
var online = item.online || 0;
var $row = $('<tr>').addClass('tr');
var $appCell = $('<td>').addClass('td record-app-td');
render_app_cell($appCell, app_id, app_name, item.icon);
$row
.append($appCell)
.append($('<td>').addClass('td').attr('title', mac).text(mac))
.append($('<td>').addClass('td').attr('title', hostname).text(hostname))
.append($('<td>').addClass('td').attr('title', nickname).text(nickname))
.append($('<td>').addClass('td').text(format_time(ft)))
.append($('<td>').addClass('td').text(format_time(lt)))
.append($('<td>').addClass('td').text(format_duration(tt)))
.append($('<td>').addClass('td').html(render_online_status(online)))
.append($('<td>').addClass('td').html(render_filter_status(act)));
$tb.append($row);
});
}
function render_pagination(container_id, state, go_fn) {
var $p = $("#" + container_id);
if (state.total_page <= 1) {
$p.html("");
return;
}
var $wrap = $('<div>').css({
display: 'flex',
gap: '6px',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap'
});
var $prev = $('<button type="button">').addClass('cbi-button').text(text.previous).prop('disabled', state.page <= 1);
var $next = $('<button type="button">').addClass('cbi-button').text(text.next).prop('disabled', state.page >= state.total_page);
var $pageText = $('<span>').text(state.page + ' / ' + state.total_page);
var $jumpInput = $('<input type="number">')
.addClass('cbi-input-text')
.attr({ min: 1, max: state.total_page })
.css({ width: '72px', height: '28px', boxSizing: 'border-box' })
.val(state.page);
var $jumpBtn = $('<button type="button">').addClass('cbi-button').text('Go');
var $total = $('<span>').css('color', 'var(--text-color-medium, #999)')
.text(text.total + ' ' + state.total_num + ' ' + text.records);
function jump_to_input_page() {
var target = parseInt($jumpInput.val(), 10);
if (isNaN(target)) {
target = state.page;
}
target = Math.max(1, Math.min(state.total_page, target));
$jumpInput.val(target);
if (target !== state.page) {
go_fn(target);
}
}
$prev.on("click", function() {
if (state.page > 1) go_fn(state.page - 1);
});
$next.on("click", function() {
if (state.page < state.total_page) go_fn(state.page + 1);
});
$jumpBtn.on("click", jump_to_input_page);
$jumpInput.on("keydown", function(e) {
if (e.key === "Enter") {
e.preventDefault();
jump_to_input_page();
}
});
$wrap.append($prev, $pageText, $next, $jumpInput, $jumpBtn, $total);
$p.empty().append($wrap);
}
function load_online_records() {
new XHR().get("<%=url('admin/services/oaf/api/app_record/get_active_app_records')%>", {
page: online_state.page,
page_size: online_state.page_size
}, function(x, data) {
var resp = data || {};
online_state.total_num = parseInt(resp.total_num || 0, 10);
online_state.total_page = parseInt(resp.total_page || 1, 10);
online_state.page = parseInt(resp.page || online_state.page, 10);
online_state.page_size = parseInt(resp.page_size || online_state.page_size, 10);
render_table("online-record-table", resp.list || []);
render_pagination("online-pagination", online_state, function(p) {
online_state.page = p;
load_online_records();
});
});
}
function load_history_records() {
new XHR().get("<%=url('admin/services/oaf/api/app_record/get_app_history_records')%>", {
mac: $("#q-history-mac").val() || "",
start_time: get_ts_from_input("#q-history-start"),
end_time: get_ts_from_input("#q-history-end"),
page: history_state.page,
page_size: history_state.page_size
}, function(x, data) {
var resp = data || {};
history_state.total_num = parseInt(resp.total_num || 0, 10);
history_state.total_page = parseInt(resp.total_page || 1, 10);
history_state.page = parseInt(resp.page || history_state.page, 10);
history_state.page_size = parseInt(resp.page_size || history_state.page_size, 10);
render_table("history-record-table", resp.list || []);
render_pagination("history-pagination", history_state, function(p) {
history_state.page = p;
load_history_records();
});
});
}
function switch_tab(tab_id) {
$(".tab-item").removeClass("active");
$('.tab-item[data-tab="' + tab_id + '"]').addClass("active");
$(".tab-body").removeClass("active");
$("#tab-" + tab_id).addClass("active");
if (tab_id === "history-records" && !history_loaded) {
history_loaded = true;
load_history_records();
}
}
$(".tab-item").on("click", function() {
var tab_id = $(this).attr("data-tab");
if (!tab_id) return;
switch_tab(tab_id);
});
$("#btn-history-search").on("click", function() {
history_state.page = 1;
load_history_records();
});
$("#btn-history-reset").on("click", function() {
$("#q-history-mac").val("");
$("#q-history-start").val("");
$("#q-history-end").val("");
history_state.page = 1;
load_history_records();
});
var urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('tab') === 'history') {
switch_tab('history-records');
}
load_online_records();
})();
//]]>
</script>
<%+footer%>
File diff suppressed because it is too large. Load diff
+295
View File
@@ -0,0 +1,295 @@
<%+header%>
<link rel="stylesheet" href="<%=resource%>/oaf/css/common.css">
<script type="text/javascript" src="<%=resource%>/oaf/jquery-3.7.1.min.js"></script>
<script type="text/javascript" src="<%=resource%>/oaf/common.js"></script>
<script type="text/javascript" src="<%=resource%>/oaf/oaf_icon.js"></script>
<div class="feature-container">
<div class="feature-page-heading">
<div class="feature-page-title"><%:App Feature Library%></div>
<div class="feature-page-desc"><%:The feature library is used to describe app features, and determines the effectiveness of app filtering and internet usage records%></div>
</div>
<ul class="tab-list cbi-tabmenu">
<li class="tab-item cbi-tab active" data-tab="tab-basic"><%:Basic Info%></li>
<li class="tab-item cbi-tab" data-tab="tab-online"><%:Online Update%></li>
<li class="tab-item cbi-tab" data-tab="tab-custom"><%:Custom Applications%></li>
</ul>
<div class="cbi-tabcontainer">
<div id="tab-basic" class="tab-body active cbi-tab">
<div id="feature-info" class="feature-info-card">
<div class="feature-info-grid">
<div class="feature-info-item">
<span class="feature-info-label"><%:Current version%></span>
<span class="feature-info-value" id="info-version">--</span>
</div>
<div class="feature-info-item">
<span class="feature-info-label"><%:Feature Library Type%></span>
<span class="feature-info-value" id="info-free">--</span>
</div>
<div class="feature-info-item">
<span class="feature-info-label"><%:Feature format%></span>
<span class="feature-info-value" id="info-format">--</span>
</div>
<div class="feature-info-item">
<span class="feature-info-label"><%:App number%></span>
<span class="feature-info-value" id="info-appnum">--</span>
</div>
<div class="feature-info-item">
<span class="feature-info-label"><%:Official Website%></span>
<span class="feature-info-value"><a href="http://www.openappfilter.com" target="_blank">www.openappfilter.com</a></span>
</div>
</div>
</div>
<div class="feature-app-panel">
<div class="custom-title"><%:App List%></div>
<div id="app-list" class="app-list"></div>
</div>
</div>
<div id="tab-online" class="tab-body cbi-tab">
<div class="online-panel">
<div class="custom-title"><%:Online Feature Library Update%></div>
<div class="feature-hint"><%:Visit%> <a href="http://www.openappfilter.com" target="_blank">www.openappfilter.com</a> <%:to obtain the advanced feature library update Key.%></div>
<div class="online-control-stack">
<div class="online-control-row">
<label for="online-token"><%:Subscription Key%></label>
<div class="online-token-input">
<input type="password" id="online-token" autocomplete="off">
<button type="button" class="online-token-eye" id="online-token-toggle" title="<%:Show%>" aria-label="<%:Show%>"></button>
</div>
<div class="online-actions">
<button type="button" class="cbi-button cbi-button-apply" id="online-save-token"><%:Save%></button>
</div>
</div>
<div class="feature-hint"><%:When the Key is empty, the free version can be used. After setting the Key, the premium version can be used. The premium version supports more applications, is updated regularly, and the latest supported application list can be viewed on the official website.%></div>
<div class="online-control-row">
<label for="online-lang"><%:Language%></label>
<select id="online-lang" class="online-lang-select">
<option value="en"><%:English%></option>
<option value="cn"><%:Chinese%></option>
</select>
<div class="online-actions">
<button type="button" class="cbi-button online-fetch-button" id="online-fetch-list"><%:Update List%></button>
</div>
</div>
</div>
<div class="online-device-info">
<span><%:Current Version Number%>: <strong id="online-current-version">--</strong></span>
<span><%:Feature Library Type%>: <strong id="online-current-free">--</strong></span>
<span><%:Feature format%>: <strong id="online-version">v4.0</strong></span>
</div>
<div id="online-status" class="feature-status" style="display:none;"></div>
<div id="online-announcement" class="online-announcement" style="display:none;">
<span class="announcement-icon" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 10v4a1 1 0 0 0 1 1h3l5 4V5L8 9H5a1 1 0 0 0-1 1Z"/><path d="M16 9.5a4 4 0 0 1 0 5"/><path d="M19 7a8 8 0 0 1 0 10"/></svg>
</span>
<span id="online-announcement-text"></span>
</div>
<div id="online-list-empty" class="custom-empty"><%:Click Update List to load available feature libraries%></div>
<div id="online-list-wrap" class="online-list-wrap" style="display:none;">
<table class="common-table online-table">
<thead><tr>
<th><%:Feature Library%></th>
<th><%:Type%></th>
<th><%:Application Count%></th>
<th><%:Language%></th>
<th><%:Release Date%></th>
<th><%:Actions%></th>
</tr></thead>
<tbody id="online-list-body"></tbody>
</table>
<div id="online-card-list" class="online-card-list"></div>
</div>
</div>
</div>
<div id="tab-custom" class="tab-body cbi-tab">
<div class="custom-panel">
<div class="custom-toolbar">
<div class="custom-title"><%:Custom Applications%></div>
<button type="button" class="cbi-button cbi-button-add" id="custom-add"><%:Add Application%></button>
</div>
<div id="custom-status" class="feature-hint"><%:Loading...%></div>
<div id="custom-empty" class="custom-empty" style="display:none;"><%:No custom applications%></div>
<div class="custom-table-wrap" id="custom-table-wrap" style="display:none;">
<table class="common-table custom-table">
<thead><tr>
<th><%:Application Name%></th>
<th><%:Category%></th>
<th><%:Feature Count%></th>
<th><%:Actions%></th>
</tr></thead>
<tbody id="custom-table-body"></tbody>
</table>
<div id="custom-card-list" class="custom-card-list"></div>
</div>
</div>
</div>
</div>
</div>
<div id="custom-editor" class="custom-editor-mask" style="display:none;">
<div class="custom-editor-dialog">
<div class="custom-editor-header">
<h3 id="custom-editor-title"><%:Add Application%></h3>
<button type="button" class="editor-close" id="custom-editor-close" title="<%:Close%>">&times;</button>
</div>
<div class="custom-editor-body">
<div class="editor-basic-grid">
<div class="editor-field">
<label for="custom-name"><%:Application Name%> *</label>
<input type="text" id="custom-name" maxlength="63">
</div>
<div class="editor-field">
<label for="custom-class"><%:Category%> *</label>
<select id="custom-class"></select>
</div>
</div>
<div class="feature-editor-toolbar">
<span><%:Application Features%></span>
<button type="button" class="cbi-button cbi-button-add" id="add-feature"><%:Add Feature%></button>
</div>
<div id="feature-editors"></div>
<div id="custom-editor-error" class="editor-error" style="display:none;"></div>
</div>
<div class="custom-editor-footer">
<button type="button" class="cbi-button" id="custom-cancel"><%:Cancel%></button>
<button type="button" class="cbi-button cbi-button-apply" id="custom-save"><%:Save%></button>
</div>
</div>
</div>
<style>
.feature-container{padding:10px}.feature-page-heading{margin:2px 0 14px}.feature-page-title{font-size:22px;font-weight:700;color:var(--text-color-high,#333);line-height:1.35}.feature-page-desc{margin-top:4px;color:var(--text-color-medium,#6b7280);font-size:13px;line-height:1.5}.feature-container .tab-list{display:flex;list-style:none;padding:0;margin:10px 0 15px;border-bottom:1px solid var(--border-color-medium,#d1d5db);overflow-x:auto}.feature-container .tab-item{padding:3px 16px;cursor:pointer;border:1px solid var(--border-color-medium,#d1d5db)!important;border-bottom:none!important;margin-right:6px;border-radius:5px 5px 0 0;background:var(--background-color-low,#f5f5f5)!important;color:var(--text-color-medium,#6b7280)!important;white-space:nowrap}.feature-container .tab-item.active{background:var(--background-color-high,#fff)!important;color:var(--text-color-high,#333)!important;font-weight:bold}.feature-container .tab-item:hover{background:var(--background-color-medium,#eee)!important}.tab-body{display:none}.tab-body.active{display:block}.feature-info-card,.custom-panel{padding:16px;border:1px solid var(--border-color-low,#e5e7eb);border-radius:8px;background:var(--background-color-high,#fff);margin-bottom:12px}.feature-info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}.feature-info-item{padding:10px 12px;border:1px solid var(--border-color-low,#e5e7eb);border-radius:6px;background:var(--background-color-low,#f5f5f5)}.feature-info-label{display:block;font-size:13px;color:var(--text-color-medium,#6b7280);margin-bottom:4px}.feature-info-value{font-weight:600;font-size:15px;color:var(--text-color-high,#333)}.feature-card{background:var(--background-color-high,#fff);border:1px solid var(--border-color-low,#e5e7eb);border-radius:8px;padding:16px;margin-top:12px}.feature-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.feature-label{min-width:120px;font-weight:600}.feature-inputs{display:flex;align-items:center;gap:10px;flex:1}.feature-file{width:100%;max-width:360px}.feature-btn{min-width:110px}.feature-hint{margin-top:10px;color:var(--text-color-medium,#6b7280);font-size:13px;line-height:1.5}.feature-status{margin-top:8px;font-size:14px;font-weight:600}.feature-status.success{color:#34d399}.feature-status.error,.editor-error{color:#f87171}.feature-modal{display:none;position:fixed;inset:0;background-color:rgba(0,0,0,.5);z-index:1000;justify-content:center;align-items:center}.feature-modal-content{background:var(--background-color-high,#fff);padding:16px 20px;border-radius:6px;color:var(--text-color-high,#333);min-width:200px;text-align:center;box-shadow:0 4px 12px rgba(0,0,0,.4);border:1px solid var(--border-color-low,#e5e7eb)}.app-list{margin-top:10px}.class-card{margin-bottom:16px;border:1px solid var(--border-color-low,#e5e7eb);border-radius:8px;background:var(--background-color-high,#fff)}.class-header{padding:12px 16px;font-weight:600;font-size:15px;border-bottom:1px solid var(--border-color-low,#e5e7eb);color:var(--text-color-high,#333)}.app-items{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;padding:12px 16px}.app-item{display:flex;align-items:center;gap:8px;padding:8px 12px;border:1px solid var(--border-color-medium,#d1d5db);border-radius:6px;background:var(--background-color-low,#f5f5f5);font-size:13px;cursor:default}.app-item img,.app-icon-letter{width:20px;height:20px;border-radius:4px;flex-shrink:0}.app-item img{object-fit:contain}.app-icon-letter{display:inline-flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#fff}.app-item .app-name{color:var(--text-color-medium,#6b7280);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.custom-toolbar{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.custom-title{font-size:17px;font-weight:600;color:var(--text-color-high,#333)}.custom-empty{text-align:center;padding:34px 12px;color:var(--text-color-medium,#6b7280);border:1px dashed var(--border-color-medium,#d1d5db);border-radius:6px}.custom-table-wrap{overflow-x:auto}.custom-table th:last-child,.custom-table td:last-child{text-align:right;width:110px}.custom-actions{display:flex;justify-content:flex-end;gap:6px}.icon-action{display:inline-flex;align-items:center;justify-content:center;width:32px;height:32px;padding:0;border:1px solid var(--border-color-medium,#d1d5db);border-radius:5px;background:transparent;color:var(--text-color-medium,#6b7280);cursor:pointer}.icon-action:hover{background:var(--background-color-low,#f5f5f5);color:var(--text-color-high,#333)}.icon-action.delete:hover{color:#ef4444;border-color:#ef4444}.icon-action svg{width:17px;height:17px}.custom-card-list{display:none}.custom-editor-mask{position:fixed;inset:0;z-index:1100;background:rgba(0,0,0,.55);align-items:center;justify-content:center;padding:16px}.custom-editor-dialog{display:flex;flex-direction:column;width:min(820px,100%);max-height:calc(100vh - 32px);background:var(--background-coloLine truncated
.online-panel{padding:16px;border:1px solid var(--border-color-low,#e5e7eb);border-radius:8px;background:var(--background-color-high,#fff)}.online-token-row{display:grid;grid-template-columns:140px minmax(240px,1fr) auto;align-items:center;gap:12px;margin-top:16px}.online-token-row>label{font-weight:600}.online-token-input,.online-actions{display:flex;align-items:center;gap:8px}.online-token-input{position:relative}.online-token-input input{min-width:0;flex:1;padding-right:38px}.online-token-eye{position:absolute;right:4px;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;width:30px;height:30px;padding:0;border:0;background:transparent;color:var(--text-color-medium,#6b7280);cursor:pointer}.online-token-eye:hover{color:var(--text-color-high,#333)}.online-token-eye svg{width:18px;height:18px}.online-device-info{display:flex;flex-wrap:wrap;gap:10px 24px;margin:14px 0;color:var(--text-color-medium,#6b7280);font-size:13px}.online-device-info strong{color:var(--text-color-high,#333);font-weight:500}.online-announcement{display:flex;align-items:flex-start;gap:8px;margin:10px 0 12px;padding:9px 12px;border:1px solid var(--border-color-low,#e5e7eb);border-radius:6px;background:var(--background-color-low,#f5f5f5);color:var(--text-color-high,#333);font-size:13px;line-height:1.5}.announcement-icon{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;margin-top:1px;color:#f59e0b;flex-shrink:0}.announcement-icon svg{width:18px;height:18px}.online-list-wrap{overflow-x:auto;margin-top:14px}.online-table th:last-child,.online-table td:last-child{text-align:left;width:106px}.online-table tr.current-version td,.online-card.current-version{background:var(--background-color-low,#f5f5f5)}.current-version-label{color:#22c55e;font-weight:600}.online-use-version:disabled{opacity:.55;cursor:not-allowed}.online-card-list{display:none}.online-card{padding:12px;border:1px solid var(--border-color-low,#e5e7eb);border-radius:6px;background:var(--background-color-low,#f5f5f5);margin-bottom:10px}.online-card-title{font-weight:600;overflow-wrap:anywhere}.online-card-meta{margin-top:6px;color:var(--text-color-medium,#6b7280);font-size:13px}.online-card-action{margin-top:10px;text-align:left}
.online-control-stack{display:flex;flex-direction:column;gap:12px;margin-top:16px}.online-control-row{display:grid;grid-template-columns:140px minmax(240px,1fr) auto;align-items:center;gap:12px}.online-control-row>label{font-weight:600}.online-lang-select{width:180px;max-width:100%}.online-confirm-mask{position:fixed;inset:0;z-index:1200;display:flex;align-items:center;justify-content:center;padding:16px;background:rgba(0,0,0,.45)}.online-confirm-dialog{width:min(420px,calc(100vw - 32px));min-height:150px;box-sizing:border-box;padding:22px 24px;border:1px solid var(--border-color-low,#e5e7eb);border-radius:8px;background:var(--background-color-high,#fff);color:var(--text-color-high,#333);box-shadow:0 12px 30px rgba(0,0,0,.28)}.online-confirm-message{min-height:56px;line-height:1.6;text-align:center;font-size:14px;color:var(--text-color-high,#333)}.online-confirm-actions{display:flex;align-items:center;justify-content:center;gap:10px;margin-top:18px}.online-confirm-actions .cbi-button{min-width:68px;height:30px;padding:0 14px;font-size:13px;line-height:28px;border-radius:4px}.online-confirm-cancel{background:var(--background-color-low,#f5f5f5)!important;color:var(--text-color-high,#333)!important;border:1px solid var(--border-color-medium,#d1d5db)!important}.online-confirm-ok{background:#2885e8!important;color:#fff!important;border:1px solid #2885e8!important}
.common-modal-content{width:auto!important;min-width:150px!important;height:auto!important;min-height:86px!important;padding:18px 24px!important;box-sizing:border-box;font-size:14px;line-height:1.5}
.online-progress-mask{position:fixed;inset:0;z-index:1210;display:none;align-items:center;justify-content:center;padding:16px;background:rgba(0,0,0,.45)}.online-progress-mask.show{display:flex}.online-progress-modal{width:320px;max-width:90vw;min-height:120px;border-radius:10px;padding:18px 16px;background:var(--background-color-high,#fff);color:var(--text-color-high,#333);display:flex;align-items:center;justify-content:center;flex-direction:column;text-align:center;box-sizing:border-box;gap:12px;box-shadow:0 12px 30px rgba(0,0,0,.28);border:1px solid var(--border-color-low,#e5e7eb)}.online-progress-spinner{width:30px;height:30px;border:3px solid rgba(25,118,210,.2);border-top-color:var(--primary-color-high,#1976d2);border-radius:50%;animation:online-progress-spin .8s linear infinite}.online-progress-text{font-size:13px;line-height:1.5;color:var(--text-color-high,#374151)}[data-darkmode="true"] .online-progress-modal{background:#2d2d2d;color:#f5f5f5}[data-darkmode="true"] .online-progress-spinner{border-color:rgba(59,130,246,.2);border-top-color:#3b82f6}[data-darkmode="true"] .online-progress-text{color:rgba(255,255,255,.88)}@keyframes online-progress-spin{to{transform:rotate(360deg)}}
.online-premium-text,.online-premium-cell,.online-card.online-premium-row .online-card-title,.online-card.online-premium-row .online-card-meta{color:#2885e8}[data-darkmode="true"] .online-premium-text,[data-darkmode="true"] .online-premium-cell,[data-darkmode="true"] .online-card.online-premium-row .online-card-title,[data-darkmode="true"] .online-card.online-premium-row .online-card-meta{color:#58a6ff}
.online-fetch-button{background:#2885e8!important;border-color:#2885e8!important;color:#fff!important}.online-fetch-button:hover{background:#1d74d8!important;border-color:#1d74d8!important;color:#fff!important}.online-fetch-button:disabled{opacity:.6;color:#fff!important}
.feature-app-panel{padding:16px;border:1px solid var(--border-color-low,#e5e7eb);border-radius:8px;background:var(--background-color-high,#fff);margin-top:12px}.class-header{display:flex;align-items:center;justify-content:space-between;gap:10px;width:100%;box-sizing:border-box;background:transparent;border:0;cursor:pointer;text-align:left}.class-title{font-weight:600;color:var(--text-color-high,#333);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.class-count{display:inline-flex;align-items:center;justify-content:center;min-width:32px;height:22px;padding:0 8px;border-radius:999px;background:var(--background-color-low,#f5f5f5);color:var(--text-color-medium,#6b7280);font-size:12px}.class-chevron{width:8px;height:8px;border-right:2px solid currentColor;border-bottom:2px solid currentColor;transform:rotate(45deg);transition:transform .2s;color:var(--text-color-medium,#6b7280);flex-shrink:0}.class-card.collapsed .class-chevron{transform:rotate(-45deg)}.class-card.collapsed .app-items{display:none}
.feature-app-panel .class-card{margin-bottom:5px;padding:10px;border:1px solid var(--border-color-low,#e5e7eb);border-radius:6px;background:var(--background-color-low,#f5f5f5)}.feature-app-panel .class-header{padding:8px;border:0!important;border-radius:4px;background:var(--background-color-medium,#eee);margin:0;color:var(--text-color-high,#333)}.feature-app-panel .class-title{font-size:14px}.feature-app-panel .app-items{display:flex;flex-wrap:wrap;gap:15px;padding:10px 8px 4px}.feature-app-panel .app-item{width:calc(20% - 12px);min-width:120px;max-width:180px;padding:5px 0;border:0;background:transparent;box-sizing:border-box}.feature-app-panel .app-name{color:var(--text-color-medium,#6b7280)}
@media(min-width:800px){.app-items{grid-template-columns:repeat(6,1fr)}}@media(min-width:1200px){.app-items{grid-template-columns:repeat(8,1fr)}}
@media(max-width:640px){.feature-container{padding:4px}.feature-info-card,.custom-panel,.online-panel{padding:10px}.feature-inputs{align-items:stretch;flex-direction:column}.feature-btn{width:100%}.custom-table,.online-table{display:none}.custom-card-list,.online-card-list{display:block}.custom-toolbar{align-items:stretch;flex-direction:column}.custom-toolbar .cbi-button{width:100%}.editor-basic-grid,.feature-field-grid{grid-template-columns:1fr}.custom-editor-mask{padding:8px}.custom-editor-dialog{max-height:calc(100vh - 16px)}.custom-editor-footer .cbi-button{flex:1}.online-token-row{grid-template-columns:1fr}.online-actions{align-items:stretch;flex-direction:column}.online-actions .cbi-button{width:100%}.online-token-input{width:100%}}
@media(max-width:640px){.online-control-row{grid-template-columns:1fr}.online-lang-select{width:100%}}
</style>
<script>
(function($){
var apiClassList='<%=url("admin/services/oaf/feature/class_list")%>';
var apiInfo='<%=url("admin/services/oaf/feature/info")%>';
var apiOnlineConfig='<%=url("admin/services/oaf/feature/online_config")%>';
var apiOnlineSave='<%=url("admin/services/oaf/feature/online_save")%>';
var apiOnlineList='<%=url("admin/services/oaf/feature/online_list")%>';
var apiOnlineStart='<%=url("admin/services/oaf/feature/online_start")%>';
var apiOnlineStatus='<%=url("admin/services/oaf/feature/online_status")%>';
var apiCustomList='<%=url("admin/services/oaf/feature/custom_list")%>';
var apiCustomClasses='<%=url("admin/services/oaf/feature/custom_class_list")%>';
var apiCustomSave='<%=url("admin/services/oaf/feature/custom_save")%>';
var txt={loadFailed:'<%:Load failed%>',saveFailed:'<%:Save failed%>',saved:'<%:Saved successfully%>',loading:'<%:Loading...%>',noData:'<%:No data%>',noApps:'<%:No apps%>',unknown:'<%:Unknown%>',add:'<%:Add Application%>',edit:'<%:Edit Application%>',feature:'<%:Feature%>',position:'<%:Position%>',value:'<%:Value%>',deleteConfirm:'<%:Are you sure you want to delete this application?%>',invalidName:'<%:Invalid application name%>',invalidClass:'<%:Please select a category%>',invalidPort:'<%:Destination port must be a single port or port range between 1 and 65535%>',invalidDomain:'<%:Invalid domain%>',invalidUri:'<%:Invalid URI%>',invalidPayload:'<%:Invalid payload match format%>',invalidPosition:'<%:Payload position must be a signed integer%>',emptyFeature:'<%:At least one match field must be configured for each feature%>',maxApps:'<%:A maximum of 64 custom applications is allowed%>',maxFeatures:'<%:A maximum of 16 features is allowed%>',maxPayloads:'<%:A maximum of 15 payload matches is allowed%>'};
var onlineTxt={useVersion:'<%:Switch%>',confirm:'<%:Use this feature library version now?%>',confirmAction:'<%:Confirm%>',cancel:'<%:Cancel%>',success:'<%:Feature library updated successfully%>',failed:'<%:Feature library update failed%>',timeout:'<%:Feature library update timeout%>',invalidToken:'<%:Invalid subscription key%>',vipRequired:'<%:A valid subscription is required%>',rateLimited:'<%:Too many requests. Please try again later.%>',deviceLimited:'<%:The subscription device limit has been reached%>',serviceUnavailable:'<%:Subscription service is unavailable%>',formatError:'<%:Feature library format error. Please use the feature library for this version.%>',fileTooLarge:'<%:The downloaded file is too large%>',checksumError:'<%:Feature library checksum verification failed%>',updating:'<%:Updating, please wait...%>',updatingList:'<%:Updating list, please wait...%>',tokenSaved:'<%:Saved successfully%>',updated:'<%:Updated successfully%>',switchSuccess:'<%:Switched successfully%>',show:'<%:Show%>',hide:'<%:Hide%>',downloading:'<%:Downloading%>',extracting:'<%:Extracting%>',validating:'<%:Validating%>',applying:'<%:Applying%>',lite:'<%:Lite%>',plus:'<%:Plus%>',applicationCount:'<%:Application Count%>',chinese:'<%:Chinese%>',english:'<%:English%>',currentVersion:'<%:Current Version%>',updateListHint:'<%:Click Update List to get the latest feature libraries%>',freeVersion:'<%:Free%>',premiumVersion:'<%:Premium%>',keyRequired:'<%:Please enter a valid Key. You can apply for a Key on the official website.%>',keyRequiredVersion:'<%:Enter the Key to use this feature library version%>',invalidKeyPermission:'<%:Invalid Key. Please get the correct Key from the official website and enable the advanced feature library permission.%>'};
var customApps=[];
var customClasses=[];
var customLoaded=false;
var editingIndex=-1;
var originalClassId=0;
var iconStatusCache={};
var onlineLoaded=false;
var onlinePolling=false;
var onlinePollStartedAt=0;
var onlinePollTimeout=180000;
var currentVersionNumber=$.trim($('#online-current-version').text()||$('#info-version').text()||'');
var currentFeatureType=0;
var currentFeatureFree=0;
var currentOnlineFiles=[];
function clone(value){return JSON.parse(JSON.stringify(value));}
function utf8Length(value){return unescape(encodeURIComponent(value || '')).length;}
function showMessage(message,ok){if(window.showCommonModal){showCommonModal(message,2500,ok?'success':'error');}}
function showOnlineToast(message,ok){if(window.showCommonModal)showCommonModal(message,2000,ok?'success':'error');else{setOnlineStatus(message,ok);setTimeout(function(){setOnlineStatus('',true);},2000);}}
function showOnlineProgress(message){var mask=$('.online-progress-mask');if(!mask.length){mask=$('<div>').addClass('online-progress-mask');var modal=$('<div>').addClass('online-progress-modal');modal.append($('<div>').addClass('online-progress-spinner'),$('<div>').addClass('online-progress-text'));mask.append(modal).appendTo(document.body);}mask.find('.online-progress-text').text(message||txt.loading);mask.addClass('show');}
function updateOnlineProgress(message){var mask=$('.online-progress-mask');if(mask.length)mask.find('.online-progress-text').text(message||txt.loading);else showOnlineProgress(message);}
function closeOnlineProgress(){$('.online-progress-mask').removeClass('show').remove();}
function browserRawLanguage(){var langs=navigator.languages&&navigator.languages.length?navigator.languages:[navigator.language||navigator.userLanguage||''];return String(langs[0]||'');}
function browserDefaultOnlineLang(){var lang=browserRawLanguage().toLowerCase();if(lang.indexOf('zh')===0)return 'cn';if(lang.indexOf('en')===0)return 'en';return 'en';}
function showOnlineSwitchConfirm(onConfirm){$('.online-confirm-mask').remove();var mask=$('<div>').addClass('online-confirm-mask');var dialog=$('<div>').addClass('online-confirm-dialog');var message=$('<div>').addClass('online-confirm-message').text(onlineTxt.confirm);var actions=$('<div>').addClass('online-confirm-actions');var cancel=$('<button>').attr('type','button').addClass('cbi-button online-confirm-cancel').text(onlineTxt.cancel);var ok=$('<button>').attr('type','button').addClass('cbi-button cbi-button-apply online-confirm-ok').text(onlineTxt.confirmAction);cancel.on('click',function(){mask.remove();});ok.on('click',function(){mask.remove();if(onConfirm)onConfirm();});actions.append(cancel,ok);dialog.append(message,actions);mask.append(dialog).appendTo(document.body);}
function setEditorError(message){$('#custom-editor-error').text(message||'').toggle(!!message);}
function className(classId){for(var i=0;i<customClasses.length;i++){if(Number(customClasses[i].class_id)===Number(classId))return customClasses[i].class_name;}return '--';}
function appLetterColor(name){return window.OAFIcon?window.OAFIcon.hashColor(name):'#3b82f6';}
function editIcon(){return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L8 18l-4 1 1-4Z"/></svg>';}
function deleteIcon(){return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M8 6V4h8v2"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v5M14 11v5"/></svg>';}
function eyeIcon(visible){return visible?'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 3l18 18"/><path d="M10.6 10.6a2 2 0 0 0 2.8 2.8"/><path d="M9.9 4.2A10.7 10.7 0 0 1 12 4c5 0 9 5 10 8a14.5 14.5 0 0 1-3.1 4.8"/><path d="M6.6 6.6A14.5 14.5 0 0 0 2 12c1 3 5 8 10 8 1.5 0 2.9-.4 4.1-1.1"/></svg>':'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>';}
function onlineError(data){var code=Number(data&&data.status_code)||400;var message=data&&data.message?String(data.message):'';if(code===40000||code===40001||code===40010)return onlineTxt.invalidKeyPermission;if(code===40031)return onlineTxt.deviceLimited;if(code===40029)return onlineTxt.rateLimited;if(code===401)return onlineTxt.formatError;if(code===402)return onlineTxt.fileTooLarge;if(code===403)return onlineTxt.checksumError;return message?message:onlineTxt.failed;}
function setOnlineStatus(message,ok){var box=$('#online-status');box.text(message||'').toggle(!!message).removeClass('success error');if(message&&ok===true)box.addClass('success');else if(message&&ok===false)box.addClass('error');}
function setOnlineBusy(busy){$('#online-save-token,#online-fetch-list,.online-use-version,#online-lang').prop('disabled',!!busy);}
function formatOnlineSize(bytes){var value=Number(bytes)||0;var units=['B','KB','MB','GB'];var index=0;while(value>=1024&&index<units.length-1){value=value/1024;index++;}if(index===0)return Math.floor(value)+units[index];return (Math.floor(value*10)/10).toFixed(1).replace(/\.0$/,'')+units[index];}
function onlineProgressMessage(data,stageText){var now=Number(data&&data.download_now)||0;var total=Number(data&&data.download_total)||0;if(data&&data.stage==='downloading'&&(now>0||total>0)){var text=now>0?formatOnlineSize(now):'0B';if(total>0){var percent=Math.min(100,Math.max(0,Math.floor(now*100/total)));text+=' / '+formatOnlineSize(total)+' ('+percent+'%)';}return onlineTxt.updating+' ('+stageText+' '+text+')';}return onlineTxt.updating+' ('+stageText+')';}
function featureVersionDisplay(version,type){var text=String(version||'').trim();return text?'v'+text.replace(/^[vV]/,'')+'('+onlineTypeLabel(type)+')':'--';}
function refreshFeatureInfo(done){$.get(apiInfo,function(resp){var data=resp&&resp.code===2000?resp.data:null;if(!data)return;currentVersionNumber=data.version||'';currentFeatureType=data.type;currentFeatureFree=data.free;$('#info-version').text(featureVersionDisplay(data.version,data.type));$('#info-free').text(onlineFreeLabel(data.free));$('#online-current-version').text(featureVersionDisplay(data.version,data.type));$('#online-current-free').text(onlineFreeLabel(data.free));$('#info-format').text(data.format||'--');$('#info-appnum').text(data.app_count===undefined?'--':data.app_count);}).always(function(){if(done)done();});}
function loadOnlineConfig(){setOnlineStatus(txt.loading);$.get(apiOnlineConfig,function(resp){if(!resp||resp.code!==2000||!resp.data){setOnlineStatus(onlineError(resp&&resp.data),false);return;}$('#online-token').val(resp.data.token||'');$('#online-version').text(resp.data.version||'v4.0');$('#online-current-version').text(featureVersionDisplay(currentVersionNumber,currentFeatureType));$('#online-current-free').text(onlineFreeLabel(currentFeatureFree));onlineLoaded=true;setOnlineStatus('',true);loadOnlineList(false);}).fail(function(){setOnlineStatus(txt.loadFailed,false);});}
function saveOnlineToken(){var token=$.trim($('#online-token').val()||'');$('#online-save-token').prop('disabled',true);$.post(apiOnlineSave,{token:token},function(resp){if(resp&&resp.code===2000){setOnlineStatus('',true);showOnlineToast(onlineTxt.tokenSaved,true);}else{setOnlineStatus(onlineError(resp&&resp.data),false);}},'json').fail(function(){setOnlineStatus(txt.saveFailed,false);}).always(function(){$('#online-save-token').prop('disabled',false);});}
function normalizeFeatureVersion(version){return String(version||'').trim().replace(/^[vV]/,'').toLowerCase();}
function onlineHasKey(){return !!$.trim($('#online-token').val()||'');}
function updateOnlineUseButtons(){var hasKey=onlineHasKey();$('.online-use-version').each(function(){var btn=$(this);var current=String(btn.attr('data-current')||'0')==='1';var premium=String(btn.attr('data-free')||'0')!=='1';var blocked=premium&&!hasKey;btn.prop('disabled',current||blocked).toggleClass('disabled',current||blocked).attr('title',blocked?onlineTxt.keyRequiredVersion:'');});}
function onlineUseButton(id,disabled,lang,md5,free){var blocked=Number(free)!==1&&!onlineHasKey();return $('<button>').attr({type:'button','data-id':id,'data-lang':lang||'cn','data-md5':md5||'','data-free':Number(free)===1?1:0,'data-current':disabled?1:0,title:blocked?onlineTxt.keyRequiredVersion:''}).prop('disabled',!!disabled||blocked).toggleClass('disabled',!!disabled||blocked).addClass('cbi-button cbi-button-apply online-use-version').text(onlineTxt.useVersion);}
function onlineTypeLabel(type){return Number(type)===1?onlineTxt.plus:onlineTxt.lite;}
function onlineFreeLabel(value){return Number(value)===1?onlineTxt.freeVersion:onlineTxt.premiumVersion;}
function onlineLangLabel(lang){return String(lang||'cn').toLowerCase()==='en'?onlineTxt.english:onlineTxt.chinese;}
function isCurrentOnlineVersion(file){var current=normalizeFeatureVersion(currentVersionNumber);var target=normalizeFeatureVersion(file&&file.version);return !!current&&!!target&&current===target&&Number(currentFeatureType)===Number(file&&file.type)&&Number(currentFeatureFree)===Number(file&&file.free);}
function onlineVersionCell(file){var wrap=$('<span>').text(featureVersionDisplay(file&&file.version,file&&file.type));if(isCurrentOnlineVersion(file))wrap.append(document.createTextNode(' - '),$('<span>').addClass('current-version-label').text(onlineTxt.currentVersion));return wrap;}
function renderOnlineList(data,refresh){currentOnlineFiles=(data&&data.files?data.files:[]).slice().sort(function(a,b){return (Number(a&&a.free)===1?1:0)-(Number(b&&b.free)===1?1:0);});var body=$('#online-list-body').empty();var cards=$('#online-card-list').empty();var announcement=$.trim(data&&data.announcement?data.announcement:'');if(!currentOnlineFiles.length){$('#online-announcement').hide();$('#online-list-wrap').hide();$('#online-list-empty').text(refresh?txt.noData:onlineTxt.updateListHint).show();return;}if(announcement){$('#online-announcement-text').text(announcement);$('#online-announcement').css('display','flex');}else{$('#online-announcement').hide();}$('#online-list-empty').hide();$('#online-list-wrap').show();currentOnlineFiles.forEach(function(file){var count=Math.max(0,Number(file.count)||0);var current=isCurrentOnlineVersion(file);var isFree=Number(file&&file.free)===1;var freeText=onlineFreeLabel(file.free);var premiumClass=isFree?'':'online-premium-cell';var freeCell=$('<span>').text(freeText).toggleClass('online-premium-text',!isFree);var row=$('<tr>').toggleClass('current-version',current).toggleClass('online-premium-row',!isFree);row.append($('<td>').append(onlineVersionCell(file).addClass(premiumClass)),$('<td>').append(freeCell),$('<td>').append($('<span>').addClass(premiumClass).text(count)),$('<td>').append($('<span>').addClass(premiumClass).text(onlineLangLabel(file.lang))),$('<td>').append($('<span>').addClass(premiumClass).text(file.date||'')),$('<td>').append(onlineUseButton(file.id,current,file.lang,file.md5,file.free)));body.append(row);var card=$('<div>').addClass('online-card').toggleClass('current-version',current).toggleClass('online-premium-row',!isFree);card.append($('<div>').addClass('online-card-title').append(onlineVersionCell(file)),$('<div>').addClass('online-card-meta').append($('<span>').text(freeText).toggleClass('online-premium-text',!isFree),document.createTextNode(' / '+onlineTxt.applicationCount+': '+count+' / '+onlineLangLabel(file.lang))),$('<div>').addClass('online-card-meta').text(file.date||''),$('<div>').addClass('online-card-action').append(onlineUseButton(file.id,current,file.lang,file.md5,file.free)));cards.append(card);});}
function loadOnlineList(refresh){if(refresh)showOnlineProgress(onlineTxt.updatingList);else setOnlineStatus(txt.loading);$('#online-fetch-list').prop('disabled',true);$.get(apiOnlineList,{lang:$('#online-lang').val()||'cn',device_lang:browserRawLanguage(),refresh:refresh?1:0},function(resp){if(resp&&resp.code===2000&&resp.data){renderOnlineList(resp.data,refresh);setOnlineStatus('',true);if(refresh){closeOnlineProgress();showOnlineToast(onlineTxt.updated,true);}}else{if(refresh)closeOnlineProgress();setOnlineStatus(onlineError(resp&&resp.data),false);}}).fail(function(){if(refresh)closeOnlineProgress();setOnlineStatus(txt.loadFailed,false);}).always(function(){$('#online-fetch-list').prop('disabled',false);});}
function fetchOnlineList(){loadOnlineList(true);}
function handleOnlineStatus(data){var state=data&&data.state?data.state:'idle';var stages={downloading:onlineTxt.downloading,extracting:onlineTxt.extracting,validating:onlineTxt.validating,applying:onlineTxt.applying};var stageText=stages[data&&data.stage]||data&&data.stage||'';if(state==='running'){if(!onlinePollStartedAt)onlinePollStartedAt=Date.now();setOnlineBusy(true);updateOnlineProgress(onlineProgressMessage(data,stageText));return true;}setOnlineBusy(false);if(!onlinePollStartedAt)return false;closeOnlineProgress();if(state==='success'){setOnlineStatus('',true);showOnlineToast(onlineTxt.switchSuccess,true);setTimeout(function(){refreshFeatureInfo(function(){loadOnlineList(false);});},2000);}else if(state==='failed'){setOnlineStatus(onlineError(data),false);}return false;}
function pollOnlineStatus(){if(onlinePolling)return;onlinePolling=true;var request=function(){if(onlinePollStartedAt&&Date.now()-onlinePollStartedAt>onlinePollTimeout){onlinePolling=false;onlinePollStartedAt=0;setOnlineBusy(false);closeOnlineProgress();setOnlineStatus(onlineTxt.timeout,false);return;}$.ajax({url:apiOnlineStatus,dataType:'json',timeout:5000}).done(function(resp){var running=resp&&resp.code===2000&&handleOnlineStatus(resp.data||{});if(running)setTimeout(request,1000);else{onlinePolling=false;onlinePollStartedAt=0;}}).fail(function(){onlinePolling=false;onlinePollStartedAt=0;setOnlineBusy(false);closeOnlineProgress();setOnlineStatus(txt.loadFailed,false);});};request();}
function startOnlineUpdate(id,lang,md5){var run=function(){setOnlineBusy(true);onlinePollStartedAt=Date.now();setOnlineStatus('',true);showOnlineProgress(onlineTxt.updating);$.ajax({url:apiOnlineStart,type:'POST',data:{id:id,lang:lang||'cn',md5:md5||''},dataType:'json',timeout:15000}).done(function(resp){if(resp&&resp.code===2000){onlinePolling=false;pollOnlineStatus();}else{onlinePollStartedAt=0;setOnlineBusy(false);closeOnlineProgress();setOnlineStatus(onlineError(resp&&resp.data),false);}}).fail(function(){onlinePollStartedAt=0;setOnlineBusy(false);closeOnlineProgress();setOnlineStatus(onlineTxt.failed,false);});};showOnlineSwitchConfirm(run);}
function loadClassList(){$.get(apiClassList,function(data){renderClassList(data&&data.class_list?data.class_list:[]);}).fail(function(){$('#app-list').empty().append($('<div>').addClass('feature-hint').text(txt.loadFailed));});}
function renderClassList(list){var container=$('#app-list').empty();if(!list.length){container.append($('<div>').addClass('feature-hint').text(txt.noData));return;}list.forEach(function(category,index){var collapsed=index!==0;var card=$('<div>').addClass('class-card').toggleClass('collapsed',collapsed);var appList=category.app_list||[];var header=$('<button>').attr({type:'button','aria-expanded':collapsed?'false':'true','data-index':index}).addClass('class-header');header.append($('<span>').addClass('class-title').text((category.name||txt.unknown)+'('+appList.length+')'),$('<span>').addClass('class-chevron').attr('aria-hidden','true'));var apps=$('<div>').addClass('app-items');if(!appList.length){apps.append($('<div>').addClass('feature-hint').text(txt.noApps));}else{appList.forEach(function(raw){var parts=raw.split(',');var appId=parts[0]||'';var appName=parts[1]||txt.unknown;var hasIcon=parts[2]!=='0';var item=$('<div>').addClass('app-item');var icon=$('<span>').addClass('app-icon-letter').css('background',appLetterColor(appName)).text(appName.charAt(0).toUpperCase());var src='<%=resource%>/oaf/app_icons/'+encodeURIComponent(appId)+'.png';if(hasIcon&&appId&&iconStatusCache[appId]!=='failed'){var loader=new Image();loader.onload=function(){iconStatusCache[appId]='loaded';icon.replaceWith($('<img>').attr({src:src,alt:appName}));};loader.onerror=function(){iconStatusCache[appId]='failed';};loader.src=src;}item.append(icon).append($('<span>').addClass('app-name').text(appName));apps.append(item);});}card.append(header,apps);container.append(card);});}
function loadCustomData(){customLoaded=false;$('#custom-status').text(txt.loading).show();$('#custom-empty,#custom-table-wrap').hide();$.when($.get(apiCustomClasses),$.get(apiCustomList)).done(function(classResult,appResult){var classResp=classResult[0]||{};var appResp=appResult[0]||{};if(classResp.code!==2000||appResp.code!==2000){renderCustomLoadError();return;}customClasses=classResp.data&&classResp.data.class_list?classResp.data.class_list:[];customApps=appResp.data&&appResp.data.app_list?appResp.data.app_list:[];customLoaded=true;renderCustomList();}).fail(renderCustomLoadError);}
function renderCustomLoadError(){$('#custom-status').text(txt.loadFailed).show();$('#custom-empty,#custom-table-wrap').hide();}
function actionButtons(index){var wrap=$('<div>').addClass('custom-actions');wrap.append($('<button>').attr({type:'button',title:'<%:Edit%>','data-index':index}).addClass('icon-action custom-edit').html(editIcon()));wrap.append($('<button>').attr({type:'button',title:'<%:Delete%>','data-index':index}).addClass('icon-action delete custom-delete').html(deleteIcon()));return wrap;}
function renderCustomList(){var body=$('#custom-table-body').empty();var cards=$('#custom-card-list').empty();$('#custom-status').hide();$('#custom-add').prop('disabled',customApps.length>=64);if(!customApps.length){$('#custom-empty').show();$('#custom-table-wrap').hide();return;}$('#custom-empty').hide();$('#custom-table-wrap').show();customApps.forEach(function(app,index){var row=$('<tr>');row.append($('<td>').text(app.name||''),$('<td>').text(className(app.class_id)),$('<td>').text((app.features||[]).length),$('<td>').append(actionButtons(index)));body.append(row);var card=$('<div>').addClass('custom-card');var head=$('<div>').addClass('custom-card-head').append($('<span>').text(app.name||''),actionButtons(index));card.append(head,$('<div>').addClass('custom-card-meta').text(className(app.class_id)+' · '+(app.features||[]).length+' <%:Features%>'));cards.append(card);});}
function emptyFeature(){return {protocol:'tcp',dest_port:'',domain:'',uri:'',payload:[]};}
function populateClassSelect(selected){var select=$('#custom-class').empty();customClasses.forEach(function(item){select.append($('<option>').val(item.class_id).text(item.class_name));});if(selected)select.val(String(selected));}
function editorField(label,input,help){var field=$('<div>').addClass('editor-field').append($('<label>').text(label),input);if(help)field.append($('<span>').addClass('field-help').text(help));return field;}
function validDestPort(value){var match=/^(\d{1,5})(?:-(\d{1,5}))?$/.exec(value);if(!match)return false;var start=Number(match[1]);var end=match[2]!==undefined?Number(match[2]):start;return start>=1&&start<=65535&&end>=start&&end<=65535;}
function payloadToText(payload){return (payload||[]).map(function(item){var position=Number(item.position);var positionText=position>=0&&position<10?'0'+position:String(position);return positionText+':'+String(item.value||'').toLowerCase();}).join(',');}
function parsePayloadText(text){if(!text)return[];var items=text.split(',');if(items.length>15)throw txt.maxPayloads;return items.map(function(item){var match=/^(-?\d+):([0-9a-f]{2})$/i.exec($.trim(item));if(!match)throw txt.invalidPayload;var position=Number(match[1]);if(position<-2147483648||position>2147483647)throw txt.invalidPosition;return {position:position,value:match[2].toLowerCase()};});}
function addFeatureEditor(feature){var container=$('#feature-editors');if(container.children('.feature-editor').length>=16){setEditorError(txt.maxFeatures);return;}feature=feature||emptyFeature();var editor=$('<div>').addClass('feature-editor');var header=$('<div>').addClass('feature-editor-header').append($('<span>').addClass('feature-title').text(txt.feature),$('<button>').attr({type:'button',title:'<%:Remove%>'}).addClass('feature-remove').html('&times;'));var body=$('<div>').addClass('feature-editor-body');var generalHelp=$('<div>').addClass('field-help feature-general-help').text('<%:Leave blank for no restriction%>');var grid=$('<div>').addClass('feature-field-grid');var protocol=$('<select>').addClass('feature-protocol').append($('<option>').val('tcp').text('TCP'),$('<option>').val('udp').text('UDP')).val(feature.protocol||'tcp');grid.append(editorField('<%:Protocol%> *',protocol,'<%:Select TCP or UDP.%>'),editorField('<%:Destination Port%>',$('<input>').attr({type:'text',maxlength:'11'}).addClass('feature-port').val(feature.dest_port||''),'<%:Supports a single destination port or port range, for example 80 or 80-443.%>'),editorField('<%:Domain%>',$('<input>').attr({type:'text',maxlength:'31'}).addClass('feature-domain').val(feature.domain||'')),editorField('URI',$('<input>').attr({type:'text',maxlength:'63'}).addClass('feature-uri').val(feature.uri||''),'<%:The HTTP request URI. HTTPS does not support URI matching.%>'));
var payloadInput=$('<input>').attr({type:'text',maxlength:'255'}).addClass('feature-payload').val(payloadToText(feature.payload));var payloadField=editorField('<%:Payload Match%>',payloadInput,'<%:Matches hexadecimal Layer 7 payload content at specified positions. Format: pos:value,pos:value, for example 00:0a,02:ab. Position 0 is the first byte.%>');payloadField.css('margin-top','12px');body.append(generalHelp,grid,payloadField);editor.append(header,body);container.append(editor);}
function openEditor(index){if(!customLoaded||!customClasses.length){showMessage(txt.loadFailed,false);return;}editingIndex=typeof index==='number'?index:-1;if(editingIndex<0&&customApps.length>=64){showMessage(txt.maxApps,false);return;}var app=editingIndex>=0?clone(customApps[editingIndex]):{appid:0,class_id:customClasses[0].class_id,name:'',features:[emptyFeature()]};originalClassId=Number(app.class_id)||0;$('#custom-editor-title').text(editingIndex>=0?txt.edit:txt.add);$('#custom-name').val(app.name||'');populateClassSelect(app.class_id);$('#feature-editors').empty();(app.features&&app.features.length?app.features:[emptyFeature()]).forEach(addFeatureEditor);setEditorError('');$('#custom-editor').css('display','flex');}
function closeEditor(){$('#custom-editor').hide();setEditorError('');}
function collectEditor(){var name=$.trim($('#custom-name').val()||'');var classId=Number($('#custom-class').val());if(!name||utf8Length(name)>63||/[~\:\[\];,]/.test(name))throw txt.invalidName;if(!classId)throw txt.invalidClass;var features=[];$('#feature-editors .feature-editor').each(function(){var editor=$(this);var port=$.trim(editor.find('.feature-port').val()||'');var domain=$.trim(editor.find('.feature-domain').val()||'');var uri=$.trim(editor.find('.feature-uri').val()||'');var payload=parsePayloadText($.trim(editor.find('.feature-payload').val()||''));if(port&&!validDestPort(port))throw txt.invalidPort;if(utf8Length(domain)>31||/[\s;,\[\]]/.test(domain))throw txt.invalidDomain;if(utf8Length(uri)>63||/[\s;,\[\]]/.test(uri))throw txt.invalidUri;if(!port&&!domain&&!uri&&!payload.length)throw txt.emptyFeature;features.push({protocol:editor.find('.feature-protocol').val(),dest_port:port,domain:domain,uri:uri,payload:payload});});if(!features.length)throw txt.emptyFeature;var old=editingIndex>=0?customApps[editingIndex]:null;return {appid:old&&Number(old.class_id)===classId?old.appid:0,class_id:classId,name:name,features:features};}
function saveAppList(list,onSuccess,onFailure){$('#custom-save').prop('disabled',true);$.ajax({url:apiCustomSave,type:'POST',dataType:'json',data:{data:JSON.stringify({app_list:list})}}).done(function(resp){if(resp&&resp.code===2000&&resp.data&&resp.data.app_list){customApps=resp.data.app_list;renderCustomList();if(onSuccess)onSuccess();showMessage(txt.saved,true);}else{var error=resp&&resp.data&&resp.data.error?resp.data.error:txt.saveFailed;if(onFailure)onFailure(error);else showMessage(error,false);}}).fail(function(){if(onFailure)onFailure(txt.saveFailed);else showMessage(txt.saveFailed,false);}).always(function(){$('#custom-save').prop('disabled',false);});}
function saveEditor(){setEditorError('');if(editingIndex<0&&customApps.length>=64){setEditorError(txt.maxApps);return;}var app;try{app=collectEditor();}catch(error){setEditorError(String(error));return;}var next=clone(customApps);if(editingIndex>=0)next[editingIndex]=app;else next.push(app);saveAppList(next,function(){closeEditor();},setEditorError);}
function deleteApplication(index){var remove=function(){var next=clone(customApps);next.splice(index,1);saveAppList(next);};if(window.showConfirmModal)showConfirmModal(txt.deleteConfirm,remove,null,'<%:Delete%>','<%:Cancel%>');else if(window.confirm(txt.deleteConfirm))remove();}
$(document).on('click','.tab-item',function(){var tabId=$(this).data('tab');$('.tab-item').removeClass('active');$(this).addClass('active');$('.tab-body').removeClass('active');$('#'+tabId).addClass('active');if(tabId==='tab-basic'){refreshFeatureInfo();loadClassList();}else if(tabId==='tab-online'){refreshFeatureInfo(function(){loadOnlineConfig();});pollOnlineStatus();}else if(tabId==='tab-custom'){loadCustomData();}});
$(document).on('click','.class-header',function(){var card=$(this).closest('.class-card');var collapsed=!card.hasClass('collapsed');card.toggleClass('collapsed',collapsed);$(this).attr('aria-expanded',collapsed?'false':'true');});
$('#online-token-toggle').html(eyeIcon(false));
$(document).on('click','#online-token-toggle',function(){var input=$('#online-token');var show=input.attr('type')==='password';input.attr('type',show?'text':'password');$(this).html(eyeIcon(show)).attr({title:show?onlineTxt.hide:onlineTxt.show,'aria-label':show?onlineTxt.hide:onlineTxt.show});});
$(document).on('input','#online-token',updateOnlineUseButtons);
$(document).on('click','#online-save-token',saveOnlineToken);
$(document).on('click','#online-fetch-list',fetchOnlineList);
$(document).on('click','.online-use-version',function(){if($(this).prop('disabled'))return;if(String($(this).attr('data-free')||'0')!=='1'&&!$.trim($('#online-token').val()||'')){showOnlineToast(onlineTxt.keyRequired,false);return;}startOnlineUpdate(String($(this).attr('data-id')||''),String($(this).attr('data-lang')||'cn'),String($(this).attr('data-md5')||''));});
$(document).on('click','#custom-add',function(){openEditor();});
$(document).on('click','.custom-edit',function(){openEditor(Number($(this).data('index')));});
$(document).on('click','.custom-delete',function(){deleteApplication(Number($(this).data('index')));});
$(document).on('click','#custom-editor-close,#custom-cancel',closeEditor);
$(document).on('click','#custom-save',saveEditor);
$(document).on('click','#add-feature',function(){addFeatureEditor(emptyFeature());});
$(document).on('click','.feature-remove',function(){if($('#feature-editors .feature-editor').length<=1){setEditorError(txt.emptyFeature);return;}$(this).closest('.feature-editor').remove();});
$('#custom-editor').on('click',function(event){if(event.target===this)closeEditor();});
$(function(){var defaultLang=browserDefaultOnlineLang();$('#online-lang').val(defaultLang);refreshFeatureInfo();pollOnlineStatus();loadClassList();});
})(jQuery);
</script>
<%+footer%>
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,495 @@
<link rel="stylesheet" href="<%=resource%>/oaf/css/common.css">
<style>
.user-list {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
.user-list th, .user-list td {
padding: 8px;
border: 1px solid #ddd;
text-align: left;
}
/* Add user button */
.add-user-btn {
margin-top: 10px;
padding: 5px 15px;
background-color: #113861;
color: white;
border: none;
border-radius: 3px;
cursor: pointer;
}
.mode-selection {
margin-bottom: 10px;
margin-top: 10px;
}
/* Modal styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
}
.modal-content {
background-color: white;
margin: 15% auto;
padding: 20px;
border-radius: 5px;
width: 60%;
max-width: 600px;
}
.user-selection {
max-height: 300px;
overflow-y: auto;
margin: 15px 0;
}
.user-item {
padding: 8px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
}
.whitelist-section {
margin-top: 30px;
display: none;
}
.whitelist-section.show {
display: block;
}
.whitelist-title {
font-size: 16px;
font-weight: bold;
margin-bottom: 15px;
}
</style>
<script type="text/javascript">//<![CDATA[
let userData = {
users: [],
mode: 1
};
let allUsers = [
];
function initData() {
userData.list = [
];
getAppFilterUserData();
getAllUsersData();
getWhitelistData();
}
function getAllUsersData() {
new XHR().get('<%=url('admin/services/oaf/api/get_all_users')%>', {flag: 2, page: 0},
function (x, data) {
if (Array.isArray(data.data.list))
allUsers = data.data.list;
renderAutoUserData();
}
);
}
function getAppFilterUserData() {
new XHR().get('<%=url('admin/services/oaf/api/mac_filter/get_mac_filter_user')%>', null,
function (x, data) {
userData = data.data;
if (!Array.isArray(userData.list))
userData.list=[]
renderUserData();
render_mode_description(userData.mode);
}
);
}
function renderUserData() {
const table = document.getElementById('user_table');
const rows = table.querySelectorAll('.tr:not(.table-titles)');
rows.forEach(row => row.remove());
const modeRadios = document.querySelectorAll('input[name="mode"]');
console.log("userData.mode = " + userData.mode);
modeRadios.forEach(radio => {
console.log("radio.value = " + radio.value);
radio.checked = parseInt(radio.value) === userData.mode;
});
if (userData.mode === 1) {
document.getElementById('user_table').style.display = 'table';
document.getElementById('auto_user_table').style.display = 'none';
} else {
document.getElementById('user_table').style.display = 'none';
document.getElementById('auto_user_table').style.display = 'table';
}
const addButton = document.querySelector('.cbi-button-add');
const actionsHeader = document.querySelector('.table-titles .th:last-child');
if (addButton) {
addButton.style.display = userData.mode === 1 ? 'inline-block' : 'none';
}
if (actionsHeader) {
actionsHeader.style.display = userData.mode === 1 ? '' : 'none';
}
const displayList = userData.mode === 1 ? userData.list : allUsers;
displayList.forEach((user, index) => {
const hostname = user.hostname || '--';
const nickname = user.nickname || '--';
const row = document.createElement('div');
row.className = 'tr';
row.innerHTML = `
<div class="td">${user.mac}</div>
<div class="td">${hostname}</div>
<div class="td">${nickname}</div>
${userData.mode === 1 ? `<div class="td"><button type="button" class="btn-delete" onclick="removeUser(${index})"><%:Delete%></button></div>` : ''}
`;
table.appendChild(row);
});
}
function renderAutoUserData() {
const table = document.getElementById('auto_user_table');
const rows = table.querySelectorAll('.tr:not(.table-titles)');
rows.forEach(row => row.remove());
const displayList = allUsers.filter(user =>
!whitelistData.list.some(wUser => wUser.mac === user.mac)
);
displayList.forEach((user, index) => {
const hostname = user.hostname || '--';
const nickname = user.nickname || '--';
const row = document.createElement('div');
row.className = 'tr';
row.innerHTML = `
<div class="td">${user.mac}</div>
<div class="td">${hostname}</div>
<div class="td">${nickname}</div>
<div class="td">
<button type="button" class="add-user-btn" style="padding: 3px 8px;" onclick="addToWhitelist('${user.mac}')"><%:Add to whitelist%></button>
</div>
`;
table.appendChild(row);
});
}
function showSuccessMessage(message = '<%:Setting success%>') {
const modal = document.getElementById('modal');
const messageElement = modal.querySelector('p');
messageElement.textContent = message;
modal.style.display = 'flex';
setTimeout(() => {
modal.style.display = 'none';
}, 1000);
}
function removeUser(index) {
const userToRemove = userData.list[index];
new XHR().post('<%=url('admin/services/oaf/api/mac_filter/del_mac_filter_user')%>',
{ mac: userToRemove.mac },
function(x, data) {
getAppFilterUserData();
showSuccessMessage('<%:Delete success%>');
}
);
}
function showUserModal() {
const modal = document.getElementById('userModal');
modal.style.display = 'flex';
const userList = document.getElementById('userList');
userList.innerHTML = '';
allUsers.forEach(user => {
const userElement = document.createElement('div');
userElement.style.padding = '8px';
userElement.style.borderBottom = '1px solid #eee';
const displayName = user.nickname || user.hostname || '';
userElement.innerHTML = `
<label style="display: flex; align-items: center;">
<input type="checkbox" value="${user.mac}" style="margin-right: 8px;">
<span>${user.mac}</span>
<span style="color: #666; margin-left: 8px;">${displayName ? `(${displayName})` : ''}</span>
</label>
`;
userList.appendChild(userElement);
});
}
function closeUserModal() {
const modal = document.getElementById('userModal');
modal.style.display = 'none';
}
function addSelectedUsers() {
const userList = document.getElementById('userList');
const checkboxes = userList.querySelectorAll('input[type="checkbox"]:checked');
const selectedMacs = Array.from(checkboxes).map(checkbox => checkbox.value);
var data = {
mac_list: selectedMacs
}
var data_str = JSON.stringify(data);
console.log(selectedMacs);
new XHR().post('<%=url('admin/services/oaf/api/mac_filter/add_mac_filter_user')%>',
{ data: data_str },
function(x, data) {
getAppFilterUserData();
closeUserModal();
showSuccessMessage('<%:Add success%>');
}
);
}
function render_mode_description(newMode) {
const modeDescription = document.getElementById('modeDescription');
if (newMode === 0) {
modeDescription.textContent = '<%:In auto mode, all new devices will be managed, including those with random MAC addresses%>';
} else {
modeDescription.textContent = '<%:In manual mode, only specified devices will be managed%>';
}
const whitelistSection = document.getElementById('whitelist_section');
if (newMode === 0) {
whitelistSection.classList.add('show');
} else {
whitelistSection.classList.remove('show');
}
}
function switchMode(newMode) {
userData.mode = newMode;
renderUserData();
render_mode_description(newMode);
new XHR().post('<%=url('admin/services/oaf/api/mac_filter/set_mac_filter_user')%>',
{ mode: newMode },
function(x, data) {
console.log('Mode updated successfully');
showSuccessMessage('<%:Mode switched successfully%>');
});
}
let whitelistData = {
list: []
};
function getWhitelistData() {
new XHR().get('<%=url('admin/services/oaf/api/mac_filter/get_mac_filter_whitelist')%>', null,
function (x, data) {
whitelistData = data.data;
if (!Array.isArray(whitelistData.list))
whitelistData.list = [];
renderWhitelistData();
renderAutoUserData();
}
);
}
function renderWhitelistData() {
const table = document.getElementById('whitelist_table');
const rows = table.querySelectorAll('.tr:not(.table-titles)');
rows.forEach(row => row.remove());
whitelistData.list.forEach((user, index) => {
const hostname = user.hostname || '--';
const nickname = user.nickname || '--';
const row = document.createElement('div');
row.className = 'tr';
row.innerHTML = `
<div class="td">${user.mac}</div>
<div class="td">${hostname}</div>
<div class="td">${nickname}</div>
<div class="td"><button type="button" class="btn-delete" onclick="removeWhitelistUser(${index})"><%:Delete%></button></div>
`;
table.appendChild(row);
});
}
function showWhitelistModal() {
const modal = document.getElementById('whitelistModal');
modal.style.display = 'flex';
const userList = document.getElementById('whitelistUserList');
userList.innerHTML = '';
allUsers.forEach(user => {
const userElement = document.createElement('div');
userElement.style.padding = '8px';
userElement.style.borderBottom = '1px solid #eee';
const displayName = user.nickname || user.hostname || '';
userElement.innerHTML = `
<label style="display: flex; align-items: center;">
<input type="checkbox" value="${user.mac}" style="margin-right: 8px;">
<span>${user.mac}</span>
<span style="color: #666; margin-left: 8px;">${displayName ? `(${displayName})` : ''}</span>
</label>
`;
userList.appendChild(userElement);
});
}
function closeWhitelistModal() {
const modal = document.getElementById('whitelistModal');
modal.style.display = 'none';
}
function addSelectedWhitelistUsers() {
const userList = document.getElementById('whitelistUserList');
const checkboxes = userList.querySelectorAll('input[type="checkbox"]:checked');
const selectedMacs = Array.from(checkboxes).map(checkbox => checkbox.value);
var data = {
mac_list: selectedMacs
};
new XHR().post('<%=url('admin/services/oaf/api/mac_filter/add_mac_filter_whitelist')%>',
{ data: JSON.stringify(data) },
function(x, res) {
closeWhitelistModal();
getWhitelistData();
showSuccessMessage('<%:Add success%>');
}
);
}
function removeWhitelistUser(index) {
const userToRemove = whitelistData.list[index];
new XHR().post('<%=url('admin/services/oaf/api/mac_filter/del_mac_filter_whitelist')%>',
{ mac: userToRemove.mac },
function(x, data) {
getWhitelistData();
showSuccessMessage('<%:Delete success%>');
}
);
}
function addToWhitelist(mac) {
var data = {
mac_list: [mac]
};
new XHR().post('<%=url('admin/services/oaf/api/mac_filter/add_mac_filter_whitelist')%>',
{ data: JSON.stringify(data) },
function(x, res) {
getWhitelistData();
renderAutoUserData();
showSuccessMessage('<%:Add success%>');
}
);
}
window.onload = initData;
//]]></script>
<div class="cbi-section cbi-tblsection">
<div style="max-width: 1000px; margin-left: 5px;padding: 5px;">
<div class="mode-selection">
<input type="radio" name="mode" value="0" onclick="switchMode(0)"> <%:Auto mode%>
<input type="radio" name="mode" value="1" checked onclick="switchMode(1)"> <%:Manual mode%>
</div>
<div id="modeDescription" style="margin-bottom: 15px;">
<%:In manual mode, only specified devices will be managed%>
</div>
<div class="table" id="user_table" style="display: none;">
<div class="tr table-titles">
<div class="th"><%:MAC%></div>
<div class="th"><%:Hostname%></div>
<div class="th"><%:Remark%></div>
<div class="th"><%:Actions%></div>
</div>
</div>
<div class="table" id="auto_user_table" style="display: none;">
<div class="tr table-titles">
<div class="th"><%:MAC%></div>
<div class="th"><%:Hostname%></div>
<div class="th"><%:Device nickname%></div>
<div class="th"><%:Operation%></div>
</div>
</div>
<div class="cbi-section-create">
<input type="button" class="cbi-button cbi-button-add" value="<%:Add user%>" onclick="showUserModal()" />
</div>
</div>
</div>
<div id="userModal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1000; justify-content: center; align-items: center;">
<div class="modal-content" style="background-color: #2d2d2d; color: #ffffff;">
<h3 style="margin: 0 0 15px 0;"><%:Select device%></h3>
<div id="userList" style="flex: 1; overflow-y: auto;">
</div>
<div style="margin-top: 15px; text-align: right;">
<button type="button" onclick="addSelectedUsers()" style="margin-right: 10px; padding: 5px 15px;"><%:OK%></button>
<button type="button" onclick="closeUserModal()" style="padding: 5px 15px;"><%:Cancel%></button>
</div>
</div>
</div>
<div id="modal" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.5); z-index: 1000; justify-content: center; align-items: center;">
<div style="background-color: rgba(0, 0, 0, 0.5); padding: 10px; border-radius: 5px; text-align: center; width: 130px; height: 70px; color: white; display: flex; justify-content: center; align-items: center;">
<p style="margin: 0;color: white;"><%:Setting success%></p>
</div>
</div>
<div class="whitelist-section" id="whitelist_section">
<div class="whitelist-title"><%:Whitelist%></div>
<div class="table" id="whitelist_table">
<div class="tr table-titles">
<div class="th"><%:MAC%></div>
<div class="th"><%:Hostname%></div>
<div class="th"><%:Device nickname%></div>
<div class="th"><%:Operation%></div>
</div>
</div>
</div>
<div class="modal" id="whitelistModal">
<div class="modal-content">
<h3><%:Add to whitelist%></h3>
<div class="user-selection" id="whitelistUserList"></div>
<div style="text-align: right; margin-top: 15px;">
<button type="button" class="add-user-btn" onclick="addSelectedWhitelistUsers()"><%:OK%></button>
<button type="button" onclick="closeWhitelistModal()" style="margin-left: 10px;"><%:Cancel%></button>
</div>
</div>
</div>
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+672
View File
@@ -0,0 +1,672 @@
<%
local dsp = require "luci.dispatcher"
local nfs = require "nixio.fs"
local has_app_filter = nfs.access("/etc/config/appfilter")
local has_access_control = nfs.access("/etc/config/macfilter")
local has_record = nfs.access("/etc/config/fwx_record")
%>
<link rel="stylesheet" href="<%=resource%>/oaf/css/common.css">
<script src="<%=resource%>/oaf/jquery-3.7.1.min.js"></script>
<script src="<%=resource%>/oaf/common.js"></script>
<style>
.oaf-whitelist-tip {
margin: 8px 0 14px;
padding: 10px 14px;
border: 1px solid var(--border-color-low, #e5e7eb);
border-radius: 6px;
background: var(--background-color-low, #f5f5f5);
color: var(--text-color-medium, #6b7280);
font-size: 13px;
line-height: 1.6;
}
.oaf-whitelist-tabs {
display: flex;
list-style: none;
gap: 0;
padding: 0;
margin: 10px 0 15px;
border-bottom: 1px solid var(--border-color-medium, #d1d5db);
overflow-x: auto;
}
.oaf-whitelist-tab {
padding: 3px 16px;
border: 1px solid var(--border-color-medium, #d1d5db) !important;
border-bottom: none !important;
border-radius: 5px 5px 0 0;
margin-right: 6px;
background: var(--background-color-low, #f5f5f5) !important;
color: var(--text-color-medium, #6b7280) !important;
cursor: pointer;
font-size: 14px;
line-height: 1.6;
white-space: nowrap;
}
.oaf-whitelist-tab:hover {
background: var(--background-color-medium, #eee) !important;
color: var(--text-color-high, #333) !important;
}
.oaf-whitelist-tab.active {
background: var(--background-color-high, #fff) !important;
color: var(--text-color-high, #333) !important;
font-weight: bold;
}
.oaf-whitelist-panel {
display: none;
}
.oaf-whitelist-panel.active {
display: block;
}
.oaf-whitelist-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.oaf-whitelist-panel-header h3 {
min-width: 0;
}
.oaf-whitelist-table-wrap {
width: 100%;
overflow-x: auto;
}
.oaf-whitelist-table {
width: 100%;
min-width: 680px;
}
.oaf-whitelist-add-btn {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: auto;
min-width: max-content;
white-space: nowrap;
line-height: 1.4;
margin: 0;
padding: 9px 18px;
background-color: #2885e8;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
}
.oaf-whitelist-add-btn:hover {
background-color: #1e6bb8;
}
.oaf-whitelist-delete-btn {
padding: 5px 12px;
background-color: #e5484d;
color: #fff;
border: none;
border-radius: 3px;
cursor: pointer;
font-size: 13px;
}
.oaf-whitelist-delete-btn:hover {
background-color: #c9373c;
}
.oaf-whitelist-modal,
.oaf-whitelist-message {
display: none;
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
justify-content: center;
align-items: center;
}
.oaf-whitelist-modal {
z-index: 10000;
}
.oaf-whitelist-modal-content {
width: calc(100vw - 32px);
max-width: 700px;
max-height: calc(100vh - 64px);
display: flex;
flex-direction: column;
overflow: hidden;
box-sizing: border-box;
border-radius: 8px;
border: 1px solid var(--border-color-low, #d1d5db);
background: var(--background-color-high, #fff);
color: var(--text-color-high, #1f2937);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.22);
}
.oaf-whitelist-modal-header {
padding: 20px 24px 14px;
border-bottom: 1px solid var(--border-color-low, #d1d5db);
color: var(--text-color-high, #1f2937);
font-size: 20px;
font-weight: 600;
}
.oaf-whitelist-selection-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
padding: 16px 24px 10px;
}
.oaf-whitelist-selection-stat {
color: var(--text-color-medium, #6b7280);
font-size: 13px;
white-space: nowrap;
}
.oaf-whitelist-user-list {
min-height: 180px;
margin: 0 24px;
overflow-y: auto;
border: 1px solid var(--border-color-low, #d1d5db);
border-radius: 4px;
background: var(--background-color-low, #f5f5f5);
}
.oaf-whitelist-user-item {
display: flex;
align-items: center;
padding: 12px;
border-bottom: 1px solid var(--border-color-low, #d1d5db);
background: var(--background-color-high, #fff);
color: var(--text-color-high, #1f2937);
cursor: pointer;
}
.oaf-whitelist-user-item:last-child {
border-bottom: none;
}
.oaf-whitelist-user-item:hover {
background: var(--background-color-medium, #eee);
}
.oaf-whitelist-user-item .oaf-check-item {
width: 100%;
display: flex;
align-items: center;
gap: 0;
}
.oaf-whitelist-user-item .oaf-check-item > input[type="checkbox"] {
margin: 0 12px 0 0;
flex: 0 0 auto;
}
.oaf-whitelist-user-item .oaf-check-item > label {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
margin: 0;
line-height: 1.4;
}
.oaf-whitelist-user-mac {
color: var(--text-color-high, #1f2937);
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.oaf-whitelist-empty-state {
padding: 40px;
color: var(--text-color-medium, #6b7280);
text-align: center;
}
.oaf-whitelist-modal-buttons {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 16px 24px;
border-top: 1px solid var(--border-color-low, #d1d5db);
}
.oaf-whitelist-modal-btn {
min-width: 84px;
padding: 8px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
line-height: 20px;
}
.oaf-whitelist-modal-btn-primary {
border: none;
background-color: #2885e8;
color: #fff;
}
.oaf-whitelist-modal-btn-secondary {
border: 1px solid var(--border-color-medium, #d1d5db);
background: transparent;
color: var(--text-color-high, #1f2937);
}
[data-darkmode="true"] .oaf-whitelist-user-item:hover {
background: var(--background-color-medium, rgba(255, 255, 255, 0.08));
}
.oaf-whitelist-message {
z-index: 10001;
}
.oaf-whitelist-message-content {
padding: 20px 30px;
border-radius: 5px;
background-color: rgba(0, 0, 0, 0.75);
color: #fff;
text-align: center;
}
@media (max-width: 600px) {
.oaf-whitelist-tabs {
overflow-x: auto;
}
.oaf-whitelist-tab {
min-width: max-content;
}
.oaf-whitelist-panel-header,
.oaf-whitelist-selection-toolbar {
align-items: stretch;
flex-direction: column;
}
.oaf-whitelist-modal-content {
width: calc(100vw - 20px);
max-height: calc(100vh - 24px);
}
.oaf-whitelist-modal-buttons .oaf-whitelist-modal-btn {
flex: 1;
}
}
</style>
<script type="text/javascript">//<![CDATA[
var whitelistText = {
noEntries: '<%:No whitelist entries yet%>',
loading: '<%:Loading...%>',
deleteLabel: '<%:Delete%>',
noDevices: '<%:No available devices to add%>',
selectDevice: '<%:Please select at least one device%>',
addSuccess: '<%:Add success%>',
addFailed: '<%:Add failed%>',
confirmRemove: '<%:Are you sure you want to remove this device from whitelist?%>',
deleteSuccess: '<%:Delete success%>',
deleteFailed: '<%:Delete failed%>',
selected: '<%:Selected%>'
};
var whitelistTypes = {};
<% if has_app_filter then %>
whitelistTypes.app_filter = {
panelId: 'appFilterWhitelistPanel',
tableBodyId: 'appFilterWhitelistTableBody',
getUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "app_filter", "get_appfilter_whitelist")%>',
addUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "app_filter", "add_appfilter_whitelist")%>',
delUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "app_filter", "del_appfilter_whitelist")%>',
usersUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "app_filter", "get_all_users")%>',
list: [],
users: [],
usersLoaded: false
};
<% end %>
<% if has_access_control then %>
whitelistTypes.access_control = {
panelId: 'accessControlWhitelistPanel',
tableBodyId: 'accessControlWhitelistTableBody',
getUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "mac_filter", "get_mac_filter_whitelist")%>',
addUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "mac_filter", "add_mac_filter_whitelist")%>',
delUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "mac_filter", "del_mac_filter_whitelist")%>',
usersUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "get_all_users")%>',
list: [],
users: [],
usersLoaded: false
};
<% end %>
<% if has_record then %>
whitelistTypes.record = {
panelId: 'recordWhitelistPanel',
tableBodyId: 'recordWhitelistTableBody',
getUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "record_whitelist", "get_record_whitelist")%>',
addUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "record_whitelist", "add_record_whitelist")%>',
delUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "record_whitelist", "del_record_whitelist")%>',
usersUrl: '<%=dsp.build_url("admin", "services", "oaf", "api", "record_whitelist", "get_all_users")%>',
list: [],
users: [],
usersLoaded: false
};
<% end %>
var activeWhitelistType = null;
var modalWhitelistType = null;
function escapeWhitelistHtml(value) {
return $('<div>').text(value == null ? '' : String(value)).html();
}
function normalizeWhitelistMac(mac) {
return (mac || '').replace(/^\s+|\s+$/g, '').toUpperCase();
}
function switchWhitelistTab(type) {
if (!whitelistTypes[type]) {
return;
}
activeWhitelistType = type;
$('.oaf-whitelist-tab').removeClass('active').attr('aria-selected', 'false');
$('.oaf-whitelist-tab[data-type="' + type + '"]').addClass('active').attr('aria-selected', 'true');
$('.oaf-whitelist-panel').removeClass('active');
$('#' + whitelistTypes[type].panelId).addClass('active');
}
function loadWhitelist(type) {
var config = whitelistTypes[type];
new XHR().get(config.getUrl, null, function(x, data) {
config.list = data && data.data && Array.isArray(data.data.list) ? data.data.list : [];
renderWhitelistTable(type);
}, function() {
config.list = [];
renderWhitelistTable(type);
});
}
function loadWhitelistUsers(type, callback) {
var config = whitelistTypes[type];
new XHR().get(config.usersUrl, {flag: 2, page: 0}, function(x, data) {
config.users = data && data.data && Array.isArray(data.data.list) ? data.data.list : [];
config.usersLoaded = true;
if (callback) {
callback();
}
}, function() {
config.users = [];
config.usersLoaded = true;
if (callback) {
callback();
}
});
}
function renderWhitelistTable(type) {
var config = whitelistTypes[type];
var tableBody = document.getElementById(config.tableBodyId);
tableBody.innerHTML = '';
if (config.list.length === 0) {
tableBody.innerHTML = '<tr><td colspan="5" style="text-align:center;padding:30px;color:#999;">' + whitelistText.noEntries + '</td></tr>';
return;
}
config.list.forEach(function(user, index) {
var row = document.createElement('tr');
var mac = normalizeWhitelistMac(user.mac || '');
row.innerHTML = '<td>' + (index + 1) + '</td>' +
'<td style="font-family:monospace;font-weight:bold;">' + escapeWhitelistHtml(mac) + '</td>' +
'<td>' + escapeWhitelistHtml(user.hostname || '--') + '</td>' +
'<td>' + escapeWhitelistHtml(user.nickname || '--') + '</td>' +
'<td><button type="button" class="oaf-whitelist-delete-btn" onclick="removeWhitelistUser(\'' + type + '\',\'' + mac + '\')">' + whitelistText.deleteLabel + '</button></td>';
tableBody.appendChild(row);
});
}
function showWhitelistModal(type) {
modalWhitelistType = type;
$('#oafWhitelistModal').css('display', 'flex');
if (!whitelistTypes[type].usersLoaded) {
$('#oafWhitelistUserList').html('<div class="oaf-whitelist-empty-state">' + whitelistText.loading + '</div>');
updateWhitelistSelectionStats();
loadWhitelistUsers(type, function() {
if (modalWhitelistType === type) {
renderAvailableWhitelistUsers(type);
}
});
return;
}
renderAvailableWhitelistUsers(type);
}
function renderAvailableWhitelistUsers(type) {
var config = whitelistTypes[type];
var whitelistMacs = config.list.map(function(user) { return normalizeWhitelistMac(user.mac || ''); });
var availableUsers = config.users.filter(function(user) { return whitelistMacs.indexOf(normalizeWhitelistMac(user.mac || '')) === -1; });
var userList = $('#oafWhitelistUserList').empty();
if (availableUsers.length === 0) {
userList.html('<div class="oaf-whitelist-empty-state">' + whitelistText.noDevices + '</div>');
updateWhitelistSelectionStats();
return;
}
availableUsers.forEach(function(user) {
var mac = normalizeWhitelistMac(user.mac || '');
var extraInfo = user.nickname || ((user.hostname && user.hostname !== '*') ? user.hostname : user.ip) || '';
var displayText = extraInfo ? mac + ' (' + extraInfo + ')' : mac;
var item = $('<div class="oaf-whitelist-user-item"></div>');
var checkId = 'oaf-whitelist-user-' + modalWhitelistType + '-' + mac.replace(/[^a-zA-Z0-9_-]/g, '-');
var checkWrap = $('<div class="oaf-check-item"></div>');
var checkbox = $('<input type="checkbox">')
.attr('id', checkId)
.val(mac)
.on('change', updateWhitelistSelectionStats);
var label = $('<label class="oaf-whitelist-user-mac"></label>')
.attr('for', checkId)
.text(displayText);
checkWrap.append(checkbox).append(label);
item.append(checkWrap);
item.on('click', function(event) {
if (!$(event.target).closest('.oaf-check-item').length) {
checkbox.prop('checked', !checkbox.prop('checked')).trigger('change');
}
});
userList.append(item);
});
updateWhitelistSelectionStats();
}
function updateWhitelistSelectionStats() {
var allCount = $('#oafWhitelistUserList input[type="checkbox"]').length;
var selectedCount = $('#oafWhitelistUserList input[type="checkbox"]:checked').length;
$('#oafWhitelistSelectionStats').text(whitelistText.selected + ': ' + selectedCount + ' / ' + allCount);
}
function toggleAllWhitelistUsers() {
var checkboxes = $('#oafWhitelistUserList input[type="checkbox"]');
var selectedCount = checkboxes.filter(':checked').length;
checkboxes.prop('checked', checkboxes.length > 0 && selectedCount !== checkboxes.length);
updateWhitelistSelectionStats();
}
function closeWhitelistModal() {
$('#oafWhitelistModal').hide();
modalWhitelistType = null;
}
function addWhitelistUsers() {
var config = whitelistTypes[modalWhitelistType];
var selectedMacs = $('#oafWhitelistUserList input[type="checkbox"]:checked').map(function() {
return this.value;
}).get();
if (selectedMacs.length === 0) {
showWhitelistMessage(whitelistText.selectDevice);
return;
}
new XHR().post(config.addUrl, {data: JSON.stringify({mac_list: selectedMacs})}, function(x, data) {
if (data && data.code == 2000) {
var type = modalWhitelistType;
closeWhitelistModal();
loadWhitelist(type);
showWhitelistMessage(whitelistText.addSuccess);
} else {
showWhitelistMessage(whitelistText.addFailed);
}
}, function() {
showWhitelistMessage(whitelistText.addFailed);
});
}
function removeWhitelistUser(type, mac) {
var config = whitelistTypes[type];
if (!confirm(whitelistText.confirmRemove)) {
return;
}
new XHR().post(config.delUrl, {mac: mac}, function(x, data) {
if (data && data.code == 2000) {
loadWhitelist(type);
showWhitelistMessage(whitelistText.deleteSuccess);
} else {
showWhitelistMessage(whitelistText.deleteFailed);
}
}, function() {
showWhitelistMessage(whitelistText.deleteFailed);
});
}
function showWhitelistMessage(message) {
$('#oafWhitelistMessageText').text(message);
$('#oafWhitelistMessage').css('display', 'flex');
setTimeout(function() {
$('#oafWhitelistMessage').hide();
}, 1500);
}
$(function() {
var types = Object.keys(whitelistTypes);
if (types.length === 0) {
return;
}
switchWhitelistTab(types[0]);
types.forEach(function(type) {
loadWhitelist(type);
loadWhitelistUsers(type);
});
$('#oafWhitelistModal').on('click', function(event) {
if (event.target === this) {
closeWhitelistModal();
}
});
});
//]]></script>
<div class="cbi-section cbi-tblsection">
<div class="oaf-whitelist-tip">
<%:Users in the whitelist are not subject to control. For terminals using random MAC addresses, you can use the whitelist mechanism together with filtering rules: select All Users in a filtering rule, and the rule will apply to all new users.%>
</div>
<div class="oaf-whitelist-tabs" role="tablist">
<% if has_app_filter then %>
<button type="button" class="oaf-whitelist-tab" data-type="app_filter" role="tab" aria-selected="false" onclick="switchWhitelistTab('app_filter')"><%:App Filter Whitelist%></button>
<% end %>
<% if has_access_control then %>
<button type="button" class="oaf-whitelist-tab" data-type="access_control" role="tab" aria-selected="false" onclick="switchWhitelistTab('access_control')"><%:Access Control Whitelist%></button>
<% end %>
<% if has_record then %>
<button type="button" class="oaf-whitelist-tab" data-type="record" role="tab" aria-selected="false" onclick="switchWhitelistTab('record')"><%:Record Whitelist%></button>
<% end %>
</div>
<% if has_app_filter then %>
<div id="appFilterWhitelistPanel" class="oaf-whitelist-panel" role="tabpanel">
<div class="oaf-whitelist-panel-header">
<h3 style="margin:0;"><%:App Filter Whitelist%></h3>
<button type="button" class="oaf-whitelist-add-btn" onclick="showWhitelistModal('app_filter')">+ <%:Add to Whitelist%></button>
</div>
<div class="oaf-whitelist-table-wrap">
<table class="common-table oaf-whitelist-table">
<thead><tr><th><%:No.%></th><th><%:MAC Address%></th><th><%:Hostname%></th><th><%:Nickname%></th><th><%:Operation%></th></tr></thead>
<tbody id="appFilterWhitelistTableBody"><tr><td colspan="5" style="text-align:center;padding:30px;color:#999;"><%:Loading...%></td></tr></tbody>
</table>
</div>
</div>
<% end %>
<% if has_access_control then %>
<div id="accessControlWhitelistPanel" class="oaf-whitelist-panel" role="tabpanel">
<div class="oaf-whitelist-panel-header">
<h3 style="margin:0;"><%:Access Control Whitelist%></h3>
<button type="button" class="oaf-whitelist-add-btn" onclick="showWhitelistModal('access_control')">+ <%:Add to Whitelist%></button>
</div>
<div class="oaf-whitelist-table-wrap">
<table class="common-table oaf-whitelist-table">
<thead><tr><th><%:No.%></th><th><%:MAC Address%></th><th><%:Hostname%></th><th><%:Nickname%></th><th><%:Operation%></th></tr></thead>
<tbody id="accessControlWhitelistTableBody"><tr><td colspan="5" style="text-align:center;padding:30px;color:#999;"><%:Loading...%></td></tr></tbody>
</table>
</div>
</div>
<% end %>
<% if has_record then %>
<div id="recordWhitelistPanel" class="oaf-whitelist-panel" role="tabpanel">
<div class="oaf-whitelist-panel-header">
<h3 style="margin:0;"><%:Record Whitelist%></h3>
<button type="button" class="oaf-whitelist-add-btn" onclick="showWhitelistModal('record')">+ <%:Add to Whitelist%></button>
</div>
<div class="oaf-whitelist-table-wrap">
<table class="common-table oaf-whitelist-table">
<thead><tr><th><%:No.%></th><th><%:MAC Address%></th><th><%:Hostname%></th><th><%:Nickname%></th><th><%:Operation%></th></tr></thead>
<tbody id="recordWhitelistTableBody"><tr><td colspan="5" style="text-align:center;padding:30px;color:#999;"><%:Loading...%></td></tr></tbody>
</table>
</div>
</div>
<% end %>
</div>
<div id="oafWhitelistModal" class="oaf-whitelist-modal">
<div class="oaf-whitelist-modal-content">
<div class="oaf-whitelist-modal-header"><%:Add to Whitelist%></div>
<div class="oaf-whitelist-selection-toolbar">
<button type="button" class="oaf-whitelist-modal-btn oaf-whitelist-modal-btn-secondary" onclick="toggleAllWhitelistUsers()"><%:Select All%></button>
<span id="oafWhitelistSelectionStats" class="oaf-whitelist-selection-stat"><%:Selected%>: 0 / 0</span>
</div>
<div id="oafWhitelistUserList" class="oaf-whitelist-user-list"></div>
<div class="oaf-whitelist-modal-buttons">
<button type="button" class="oaf-whitelist-modal-btn oaf-whitelist-modal-btn-primary" onclick="addWhitelistUsers()"><%:OK%></button>
<button type="button" class="oaf-whitelist-modal-btn oaf-whitelist-modal-btn-secondary" onclick="closeWhitelistModal()"><%:Cancel%></button>
</div>
</div>
</div>
<div id="oafWhitelistMessage" class="oaf-whitelist-message">
<div class="oaf-whitelist-message-content">
<p id="oafWhitelistMessageText" style="margin:0;"></p>
</div>
</div>