summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base-ucode/luasrc
diff options
context:
space:
mode:
Diffstat (limited to 'modules/luci-base-ucode/luasrc')
-rw-r--r--modules/luci-base-ucode/luasrc/ucodebridge/luci/dispatcher.lua457
-rw-r--r--modules/luci-base-ucode/luasrc/ucodebridge/luci/http.lua144
-rw-r--r--modules/luci-base-ucode/luasrc/ucodebridge/luci/template.lua184
-rw-r--r--modules/luci-base-ucode/luasrc/ucodebridge/luci/ucodebridge.lua52
-rw-r--r--modules/luci-base-ucode/luasrc/ucodebridge/luci/util.lua786
5 files changed, 1623 insertions, 0 deletions
diff --git a/modules/luci-base-ucode/luasrc/ucodebridge/luci/dispatcher.lua b/modules/luci-base-ucode/luasrc/ucodebridge/luci/dispatcher.lua
new file mode 100644
index 0000000000..8889853b98
--- /dev/null
+++ b/modules/luci-base-ucode/luasrc/ucodebridge/luci/dispatcher.lua
@@ -0,0 +1,457 @@
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
+-- Licensed to the public under the Apache License 2.0.
+
+module("luci.dispatcher", package.seeall)
+
+local http = _G.L.http
+
+context = setmetatable({
+ request = _G.L.ctx.request_path;
+ requested = _G.L.node;
+ dispatched = _G.L.node;
+}, {
+ __index = function(t, k)
+ if k == "requestpath" then
+ return _G.L.ctx.request_path
+ elseif k == "requestargs" then
+ return _G.L.ctx.request_args
+ else
+ return _G.L.ctx[k]
+ end
+ end
+})
+
+uci = require "luci.model.uci"
+uci:set_session_id(_G.L.ctx.authsession)
+
+i18n = require "luci.i18n"
+i18n.setlanguage(_G.L.dispatcher.lang)
+
+build_url = _G.L.dispatcher.build_url
+menu_json = _G.L.dispatcher.menu_json
+error404 = _G.L.dispatcher.error404
+error500 = _G.L.dispatcher.error500
+
+function is_authenticated(auth)
+ local session = _G.L.dispatcher.is_authenticated(auth)
+ if session then
+ return session.sid, session.data, session.acls
+ end
+end
+
+function assign(path, clone, title, order)
+ local obj = node(unpack(path))
+
+ obj.title = title
+ obj.order = order
+
+ setmetatable(obj, {__index = node(unpack(clone))})
+
+ return obj
+end
+
+function entry(path, target, title, order)
+ local c = node(unpack(path))
+
+ c.title = title
+ c.order = order
+ c.action = target
+
+ return c
+end
+
+-- enabling the node.
+function get(...)
+ return node(...)
+end
+
+function node(...)
+ local p = table.concat({ ... }, "/")
+
+ if not __entries[p] then
+ __entries[p] = {}
+ end
+
+ return __entries[p]
+end
+
+function lookup(...)
+ local i, path = nil, {}
+ for i = 1, select('#', ...) do
+ local name, arg = nil, tostring(select(i, ...))
+ for name in arg:gmatch("[^/]+") do
+ path[#path+1] = name
+ end
+ end
+
+ local node = menu_json()
+ for i = 1, #path do
+ node = node.children[path[i]]
+
+ if not node then
+ return nil
+ elseif node.leaf then
+ break
+ end
+ end
+
+ return node, build_url(unpack(path))
+end
+
+
+function process_lua_controller(path)
+ local base = "/usr/lib/lua/luci/controller/"
+ local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
+ local mod = require(modname)
+ assert(mod ~= true,
+ "Invalid controller file found\n" ..
+ "The file '" .. path .. "' contains an invalid module line.\n" ..
+ "Please verify whether the module name is set to '" .. modname ..
+ "' - It must correspond to the file path!")
+
+ local idx = mod.index
+ if type(idx) ~= "function" then
+ return nil
+ end
+
+ local entries = {}
+
+ __entries = entries
+ __controller = modname
+
+ setfenv(idx, setmetatable({}, { __index = luci.dispatcher }))()
+
+ __entries = nil
+ __controller = nil
+
+ -- fixup gathered node specs
+ for path, entry in pairs(entries) do
+ if entry.leaf then
+ entry.wildcard = true
+ end
+
+ if type(entry.file_depends) == "table" then
+ for _, v in ipairs(entry.file_depends) do
+ entry.depends = entry.depends or {}
+ entry.depends.fs = entry.depends.fs or {}
+
+ local ft = fs.stat(v, "type")
+ if ft == "dir" then
+ entry.depends.fs[v] = "directory"
+ elseif v:match("/s?bin/") then
+ entry.depends.fs[v] = "executable"
+ else
+ entry.depends.fs[v] = "file"
+ end
+ end
+ end
+
+ if type(entry.uci_depends) == "table" then
+ for k, v in pairs(entry.uci_depends) do
+ entry.depends = entry.depends or {}
+ entry.depends.uci = entry.depends.uci or {}
+ entry.depends.uci[k] = v
+ end
+ end
+
+ if type(entry.acl_depends) == "table" then
+ for _, acl in ipairs(entry.acl_depends) do
+ entry.depends = entry.depends or {}
+ entry.depends.acl = entry.depends.acl or {}
+ entry.depends.acl[#entry.depends.acl + 1] = acl
+ end
+ end
+
+ if (entry.sysauth_authenticator ~= nil) or
+ (entry.sysauth ~= nil and entry.sysauth ~= false)
+ then
+ if entry.sysauth_authenticator == "htmlauth" then
+ entry.auth = {
+ login = true,
+ methods = { "cookie:sysauth_https", "cookie:sysauth_http" }
+ }
+ elseif subname == "rpc" and entry.module == "luci.controller.rpc" then
+ entry.auth = {
+ login = false,
+ methods = { "query:auth", "cookie:sysauth_https", "cookie:sysauth_http" }
+ }
+ elseif entry.module == "luci.controller.admin.uci" then
+ entry.auth = {
+ login = false,
+ methods = { "param:sid" }
+ }
+ end
+ elseif entry.sysauth == false then
+ entry.auth = {}
+ end
+
+ entry.leaf = nil
+
+ entry.file_depends = nil
+ entry.uci_depends = nil
+ entry.acl_depends = nil
+
+ entry.sysauth = nil
+ entry.sysauth_authenticator = nil
+ end
+
+ return entries
+end
+
+function invoke_cbi_action(model, config, ...)
+ local cbi = require "luci.cbi"
+ local tpl = require "luci.template"
+ local util = require "luci.util"
+
+ if not config then
+ config = {}
+ end
+
+ local maps = cbi.load(model, ...)
+
+ local state = nil
+
+ local function has_uci_access(config, level)
+ local rv = util.ubus("session", "access", {
+ ubus_rpc_session = context.authsession,
+ scope = "uci", object = config,
+ ["function"] = level
+ })
+
+ return (type(rv) == "table" and rv.access == true) or false
+ end
+
+ local i, res
+ for i, res in ipairs(maps) do
+ if util.instanceof(res, cbi.SimpleForm) then
+ io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n"
+ % model)
+
+ io.stderr:write("please change %s to use the form() action instead.\n"
+ % table.concat(context.request, "/"))
+ end
+
+ res.flow = config
+ local cstate = res:parse()
+ if cstate and (not state or cstate < state) then
+ state = cstate
+ end
+ end
+
+ local function _resolve_path(path)
+ return type(path) == "table" and build_url(unpack(path)) or path
+ end
+
+ if config.on_valid_to and state and state > 0 and state < 2 then
+ http:redirect(_resolve_path(config.on_valid_to))
+ return
+ end
+
+ if config.on_changed_to and state and state > 1 then
+ http:redirect(_resolve_path(config.on_changed_to))
+ return
+ end
+
+ if config.on_success_to and state and state > 0 then
+ http:redirect(_resolve_path(config.on_success_to))
+ return
+ end
+
+ if config.state_handler then
+ if not config.state_handler(state, maps) then
+ return
+ end
+ end
+
+ http:header("X-CBI-State", state or 0)
+
+ if not config.noheader then
+ tpl.render("cbi/header", {state = state})
+ end
+
+ local redirect
+ local messages
+ local applymap = false
+ local pageaction = true
+ local parsechain = { }
+ local writable = false
+
+ for i, res in ipairs(maps) do
+ if res.apply_needed and res.parsechain then
+ local c
+ for _, c in ipairs(res.parsechain) do
+ parsechain[#parsechain+1] = c
+ end
+ applymap = true
+ end
+
+ if res.redirect then
+ redirect = redirect or res.redirect
+ end
+
+ if res.pageaction == false then
+ pageaction = false
+ end
+
+ if res.message then
+ messages = messages or { }
+ messages[#messages+1] = res.message
+ end
+ end
+
+ for i, res in ipairs(maps) do
+ local is_readable_map = has_uci_access(res.config, "read")
+ local is_writable_map = has_uci_access(res.config, "write")
+
+ writable = writable or is_writable_map
+
+ res:render({
+ firstmap = (i == 1),
+ redirect = redirect,
+ messages = messages,
+ pageaction = pageaction,
+ parsechain = parsechain,
+ readable = is_readable_map,
+ writable = is_writable_map
+ })
+ end
+
+ if not config.nofooter then
+ tpl.render("cbi/footer", {
+ flow = config,
+ pageaction = pageaction,
+ redirect = redirect,
+ state = state,
+ autoapply = config.autoapply,
+ trigger_apply = applymap,
+ writable = writable
+ })
+ end
+end
+
+function invoke_form_action(model, ...)
+ local cbi = require "luci.cbi"
+ local tpl = require "luci.template"
+
+ local maps = luci.cbi.load(model, ...)
+ local state = nil
+
+ local i, res
+ for i, res in ipairs(maps) do
+ local cstate = res:parse()
+ if cstate and (not state or cstate < state) then
+ state = cstate
+ end
+ end
+
+ http:header("X-CBI-State", state or 0)
+ tpl.render("header")
+ for i, res in ipairs(maps) do
+ res:render()
+ end
+ tpl.render("footer")
+end
+
+
+function call(name, ...)
+ return {
+ ["type"] = "call",
+ ["module"] = __controller,
+ ["function"] = name,
+ ["parameters"] = select('#', ...) > 0 and {...} or nil
+ }
+end
+
+function post(name, ...)
+ return {
+ ["type"] = "call",
+ ["module"] = __controller,
+ ["function"] = name,
+ ["parameters"] = select('#', ...) > 0 and {...} or nil,
+ ["post"] = true
+ }
+end
+
+function view(name)
+ return {
+ ["type"] = "view",
+ ["path"] = name
+ }
+end
+
+function template(name)
+ return {
+ ["type"] = "template",
+ ["path"] = name
+ }
+end
+
+function cbi(model, config)
+ return {
+ ["type"] = "call",
+ ["module"] = "luci.dispatcher",
+ ["function"] = "invoke_cbi_action",
+ ["parameters"] = { model, config },
+ ["post"] = {
+ ["cbi.submit"] = true
+ }
+ }
+end
+
+function form(model)
+ return {
+ ["type"] = "call",
+ ["module"] = "luci.dispatcher",
+ ["function"] = "invoke_form_action",
+ ["parameters"] = { model },
+ ["post"] = {
+ ["cbi.submit"] = true
+ }
+ }
+end
+
+function firstchild()
+ return {
+ ["type"] = "firstchild"
+ }
+end
+
+function firstnode()
+ return {
+ ["type"] = "firstchild",
+ ["recurse"] = true
+ }
+end
+
+function arcombine(trg1, trg2)
+ return {
+ ["type"] = "arcombine",
+ ["targets"] = { trg1, trg2 } --,
+ --env = getfenv(),
+ }
+end
+
+function alias(...)
+ return {
+ ["type"] = "alias",
+ ["path"] = table.concat({ ... }, "/")
+ }
+end
+
+function rewrite(n, ...)
+ return {
+ ["type"] = "rewrite",
+ ["path"] = table.concat({ ... }, "/"),
+ ["remove"] = n
+ }
+end
+
+
+translate = i18n.translate
+
+-- This function does not actually translate the given argument but
+-- is used by build/i18n-scan.pl to find translatable entries.
+function _(text)
+ return text
+end
diff --git a/modules/luci-base-ucode/luasrc/ucodebridge/luci/http.lua b/modules/luci-base-ucode/luasrc/ucodebridge/luci/http.lua
new file mode 100644
index 0000000000..06547ae2ce
--- /dev/null
+++ b/modules/luci-base-ucode/luasrc/ucodebridge/luci/http.lua
@@ -0,0 +1,144 @@
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Copyright 2010-2018 Jo-Philipp Wich <jo@mein.io>
+-- Licensed to the public under the Apache License 2.0.
+
+local util = require "luci.util"
+local coroutine = require "coroutine"
+local table = require "table"
+local lhttp = require "lucihttp"
+
+local L, table, ipairs, pairs, type, error = _G.L, table, ipairs, pairs, type, error
+
+module "luci.http"
+
+HTTP_MAX_CONTENT = 1024*100 -- 100 kB maximum content size
+
+function close()
+ L.http:close()
+end
+
+function content()
+ return L.http:content()
+end
+
+function formvalue(name, noparse)
+ return L.http:formvalue(name, noparse)
+end
+
+function formvaluetable(prefix)
+ return L.http:formvaluetable(prefix)
+end
+
+function getcookie(name)
+ return L.http:getcookie(name)
+end
+
+-- or the environment table itself.
+function getenv(name)
+ return L.http:getenv(name)
+end
+
+function setfilehandler(callback)
+ return L.http:setfilehandler(callback)
+end
+
+function header(key, value)
+ L.http:header(key, value)
+end
+
+function prepare_content(mime)
+ L.http:prepare_content(mime)
+end
+
+function source()
+ return L.http.input
+end
+
+function status(code, message)
+ L.http:status(code, message)
+end
+
+-- This function is as a valid LTN12 sink.
+-- If the content chunk is nil this function will automatically invoke close.
+function write(content, src_err)
+ if src_err then
+ error(src_err)
+ end
+
+ return L.print(content)
+end
+
+function splice(fd, size)
+ coroutine.yield(6, fd, size)
+end
+
+function redirect(url)
+ L.http:redirect(url)
+end
+
+function build_querystring(q)
+ local s, n, k, v = {}, 1, nil, nil
+
+ for k, v in pairs(q) do
+ s[n+0] = (n == 1) and "?" or "&"
+ s[n+1] = util.urlencode(k)
+ s[n+2] = "="
+ s[n+3] = util.urlencode(v)
+ n = n + 4
+ end
+
+ return table.concat(s, "")
+end
+
+urldecode = util.urldecode
+
+urlencode = util.urlencode
+
+function write_json(x)
+ L.printf('%J', x)
+end
+
+-- separated by "&". Tables are encoded as parameters with multiple values by
+-- repeating the parameter name with each value.
+function urlencode_params(tbl)
+ local k, v
+ local n, enc = 1, {}
+ for k, v in pairs(tbl) do
+ if type(v) == "table" then
+ local i, v2
+ for i, v2 in ipairs(v) do
+ if enc[1] then
+ enc[n] = "&"
+ n = n + 1
+ end
+
+ enc[n+0] = lhttp.urlencode(k)
+ enc[n+1] = "="
+ enc[n+2] = lhttp.urlencode(v2)
+ n = n + 3
+ end
+ else
+ if enc[1] then
+ enc[n] = "&"
+ n = n + 1
+ end
+
+ enc[n+0] = lhttp.urlencode(k)
+ enc[n+1] = "="
+ enc[n+2] = lhttp.urlencode(v)
+ n = n + 3
+ end
+ end
+
+ return table.concat(enc, "")
+end
+
+context = {
+ request = {
+ formvalue = function(self, ...) return formvalue(...) end;
+ formvaluetable = function(self, ...) return formvaluetable(...) end;
+ content = function(self, ...) return content(...) end;
+ getcookie = function(self, ...) return getcookie(...) end;
+ setfilehandler = function(self, ...) return setfilehandler(...) end;
+ }
+}
diff --git a/modules/luci-base-ucode/luasrc/ucodebridge/luci/template.lua b/modules/luci-base-ucode/luasrc/ucodebridge/luci/template.lua
new file mode 100644
index 0000000000..b7cc56e1cc
--- /dev/null
+++ b/modules/luci-base-ucode/luasrc/ucodebridge/luci/template.lua
@@ -0,0 +1,184 @@
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Licensed to the public under the Apache License 2.0.
+
+local util = require "luci.util"
+local config = require "luci.config"
+local tparser = require "luci.template.parser"
+
+local tostring, pairs, loadstring = tostring, pairs, loadstring
+local setmetatable, loadfile = setmetatable, loadfile
+local getfenv, setfenv, rawget = getfenv, setfenv, rawget
+local assert, type, error = assert, type, error
+local table, string, unpack = table, string, unpack
+
+
+---
+--- bootstrap
+---
+local _G = _G
+local L = _G.L
+
+local http = _G.L.http
+
+local disp = require "luci.dispatcher"
+local i18n = require "luci.i18n"
+local xml = require "luci.xml"
+local fs = require "nixio.fs"
+
+
+--- LuCI template library.
+module "luci.template"
+
+config.template = config.template or {}
+viewdir = config.template.viewdir or util.libpath() .. "/view"
+
+
+-- Define the namespace for template modules
+context = {} --util.threadlocal()
+
+--- Render a certain template.
+-- @param name Template name
+-- @param scope Scope to assign to template (optional)
+function render(name, scope)
+ return Template(name):render(scope or getfenv(2))
+end
+
+--- Render a template from a string.
+-- @param template Template string
+-- @param scope Scope to assign to template (optional)
+function render_string(template, scope)
+ return Template(nil, template):render(scope or getfenv(2))
+end
+
+
+-- Template class
+Template = util.class()
+
+-- Shared template cache to store templates in to avoid unnecessary reloading
+Template.cache = setmetatable({}, {__mode = "v"})
+
+
+
+local function _ifattr(cond, key, val, noescape)
+ if cond then
+ local env = getfenv(3)
+ local scope = (type(env.self) == "table") and env.self
+ if type(val) == "table" then
+ if not next(val) then
+ return ''
+ else
+ val = util.serialize_json(val)
+ end
+ end
+
+ val = tostring(val or
+ (type(env[key]) ~= "function" and env[key]) or
+ (scope and type(scope[key]) ~= "function" and scope[key]) or "")
+
+ if noescape ~= true then
+ val = xml.pcdata(val)
+ end
+
+ return string.format(' %s="%s"', tostring(key), val)
+ else
+ return ''
+ end
+end
+
+context.viewns = setmetatable({
+ include = function(name)
+ if fs.access(viewdir .. "/" .. name .. ".htm") then
+ Template(name):render(getfenv(2))
+ else
+ L.include(name, getfenv(2))
+ end
+ end;
+ translate = i18n.translate;
+ translatef = i18n.translatef;
+ export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
+ striptags = xml.striptags;
+ pcdata = xml.pcdata;
+ ifattr = function(...) return _ifattr(...) end;
+ attr = function(...) return _ifattr(true, ...) end;
+ url = disp.build_url;
+}, {__index=function(tbl, key)
+ if key == "controller" then
+ return disp.build_url()
+ elseif key == "REQUEST_URI" then
+ return disp.build_url(unpack(disp.context.requestpath))
+ elseif key == "FULL_REQUEST_URI" then
+ local url = { http:getenv("SCRIPT_NAME") or "", http:getenv("PATH_INFO") }
+ local query = http:getenv("QUERY_STRING")
+ if query and #query > 0 then
+ url[#url+1] = "?"
+ url[#url+1] = query
+ end
+ return table.concat(url, "")
+ elseif key == "token" then
+ return disp.context.authtoken
+ elseif key == "theme" then
+ return L.media and fs.basename(L.media) or tostring(L)
+ elseif key == "resource" then
+ return L.config.main.resourcebase
+ else
+ return rawget(tbl, key) or _G[key] or L[key]
+ end
+end})
+
+
+-- Constructor - Reads and compiles the template on-demand
+function Template.__init__(self, name, template)
+ if name then
+ self.template = self.cache[name]
+ self.name = name
+ else
+ self.name = "[string]"
+ end
+
+ -- Create a new namespace for this template
+ self.viewns = context.viewns
+
+ -- If we have a cached template, skip compiling and loading
+ if not self.template then
+
+ -- Compile template
+ local err
+ local sourcefile
+
+ if name then
+ sourcefile = viewdir .. "/" .. name .. ".htm"
+ self.template, _, err = tparser.parse(sourcefile)
+ else
+ sourcefile = "[string]"
+ self.template, _, err = tparser.parse_string(template)
+ end
+
+ -- If we have no valid template throw error, otherwise cache the template
+ if not self.template then
+ error("Failed to load template '" .. self.name .. "'.\n" ..
+ "Error while parsing template '" .. sourcefile .. "':\n" ..
+ (err or "Unknown syntax error"))
+ elseif name then
+ self.cache[name] = self.template
+ end
+ end
+end
+
+
+-- Renders a template
+function Template.render(self, scope)
+ scope = scope or getfenv(2)
+
+ -- Put our predefined objects in the scope of the template
+ setfenv(self.template, setmetatable({}, {__index =
+ function(tbl, key)
+ return rawget(tbl, key) or self.viewns[key] or scope[key]
+ end}))
+
+ -- Now finally render the thing
+ local stat, err = util.copcall(self.template)
+ if not stat then
+ error("Failed to execute template '" .. self.name .. "'.\n" ..
+ "A runtime error occurred: " .. tostring(err or "(nil)"))
+ end
+end
diff --git a/modules/luci-base-ucode/luasrc/ucodebridge/luci/ucodebridge.lua b/modules/luci-base-ucode/luasrc/ucodebridge/luci/ucodebridge.lua
new file mode 100644
index 0000000000..fa4943dc99
--- /dev/null
+++ b/modules/luci-base-ucode/luasrc/ucodebridge/luci/ucodebridge.lua
@@ -0,0 +1,52 @@
+-- Copyright 2022 Jo-Philipp Wich <jo@mein.io>
+-- Licensed to the public under the Apache License 2.0.
+
+local coroutine, assert, error, type, require = coroutine, assert, error, type, require
+local tmpl = require "luci.template"
+local util = require "luci.util"
+local http = require "luci.http"
+
+
+--- LuCI ucode bridge library.
+module "luci.ucodebridge"
+
+local function run(fn, ...)
+ local co = coroutine.create(fn)
+ local ok, ret
+
+ while coroutine.status(co) ~= "dead" do
+ ok, ret = coroutine.resume(co, ...)
+
+ if not ok then
+ error(ret)
+ end
+ end
+
+ return ret
+end
+
+function compile(path)
+ run(function(path)
+ return tmpl.Template(path)
+ end, path)
+end
+
+function render(path, scope)
+ run(tmpl.render, path, scope)
+end
+
+function call(modname, method, ...)
+ return run(function(module, method, ...)
+ local mod = require(modname)
+ local func = mod[method]
+
+ assert(func ~= nil,
+ 'Cannot resolve function "' .. method .. '". Is it misspelled or local?')
+
+ assert(type(func) == "function",
+ 'The symbol "' .. method .. '" does not refer to a function but data ' ..
+ 'of type "' .. type(func) .. '".')
+
+ return func(...)
+ end, modname, method, ...)
+end
diff --git a/modules/luci-base-ucode/luasrc/ucodebridge/luci/util.lua b/modules/luci-base-ucode/luasrc/ucodebridge/luci/util.lua
new file mode 100644
index 0000000000..d50eb1dd9c
--- /dev/null
+++ b/modules/luci-base-ucode/luasrc/ucodebridge/luci/util.lua
@@ -0,0 +1,786 @@
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Licensed to the public under the Apache License 2.0.
+
+local io = require "io"
+local math = require "math"
+local table = require "table"
+local debug = require "debug"
+local ldebug = require "luci.debug"
+local string = require "string"
+local coroutine = require "coroutine"
+local tparser = require "luci.template.parser"
+local json = require "luci.jsonc"
+local lhttp = require "lucihttp"
+
+local _ubus = require "ubus"
+local _ubus_connection = nil
+
+local getmetatable, setmetatable = getmetatable, setmetatable
+local rawget, rawset, unpack, select = rawget, rawset, unpack, select
+local tostring, type, assert, error = tostring, type, assert, error
+local ipairs, pairs, next, loadstring = ipairs, pairs, next, loadstring
+local require, pcall, xpcall = require, pcall, xpcall
+local collectgarbage, get_memory_limit = collectgarbage, get_memory_limit
+
+local L = _G.L
+
+module "luci.util"
+
+--
+-- Pythonic string formatting extension
+--
+getmetatable("").__mod = function(a, b)
+ local ok, res
+
+ if not b then
+ return a
+ elseif type(b) == "table" then
+ local k, _
+ for k, _ in pairs(b) do if type(b[k]) == "userdata" then b[k] = tostring(b[k]) end end
+
+ ok, res = pcall(a.format, a, unpack(b))
+ if not ok then
+ error(res, 2)
+ end
+ return res
+ else
+ if type(b) == "userdata" then b = tostring(b) end
+
+ ok, res = pcall(a.format, a, b)
+ if not ok then
+ error(res, 2)
+ end
+ return res
+ end
+end
+
+
+--
+-- Class helper routines
+--
+
+-- Instantiates a class
+local function _instantiate(class, ...)
+ local inst = setmetatable({}, {__index = class})
+
+ if inst.__init__ then
+ inst:__init__(...)
+ end
+
+ return inst
+end
+
+-- The class object can be instantiated by calling itself.
+-- Any class functions or shared parameters can be attached to this object.
+-- Attaching a table to the class object makes this table shared between
+-- all instances of this class. For object parameters use the __init__ function.
+-- Classes can inherit member functions and values from a base class.
+-- Class can be instantiated by calling them. All parameters will be passed
+-- to the __init__ function of this class - if such a function exists.
+-- The __init__ function must be used to set any object parameters that are not shared
+-- with other objects of this class. Any return values will be ignored.
+function class(base)
+ return setmetatable({}, {
+ __call = _instantiate,
+ __index = base
+ })
+end
+
+function instanceof(object, class)
+ local meta = getmetatable(object)
+ while meta and meta.__index do
+ if meta.__index == class then
+ return true
+ end
+ meta = getmetatable(meta.__index)
+ end
+ return false
+end
+
+
+--
+-- Scope manipulation routines
+--
+
+coxpt = setmetatable({}, { __mode = "kv" })
+
+local tl_meta = {
+ __mode = "k",
+
+ __index = function(self, key)
+ local t = rawget(self, coxpt[coroutine.running()]
+ or coroutine.running() or 0)
+ L.http:write("<!-- __index(%s/%s, %s): %s -->\n" %{ tostring(self), tostring(coxpt[coroutine.running()] or coroutine.running() or 0), key, tostring(t and t[key]) })
+ return t and t[key]
+ end,
+
+ __newindex = function(self, key, value)
+ L.http:write("<!-- __newindex(%s/%s, %s, %s) -->\n" %{ tostring(self), tostring(coxpt[coroutine.running()] or coroutine.running() or 0), key, tostring(value) })
+ local c = coxpt[coroutine.running()] or coroutine.running() or 0
+ local r = rawget(self, c)
+ if not r then
+ rawset(self, c, { [key] = value })
+ else
+ r[key] = value
+ end
+ end
+}
+
+-- the current active coroutine. A thread local store is private a table object
+-- whose values can't be accessed from outside of the running coroutine.
+function threadlocal(tbl)
+ return tbl or {} --setmetatable(tbl or {}, tl_meta)
+end
+
+
+--
+-- Debugging routines
+--
+
+function perror(obj)
+ return io.stderr:write(tostring(obj) .. "\n")
+end
+
+function dumptable(t, maxdepth, i, seen)
+ i = i or 0
+ seen = seen or setmetatable({}, {__mode="k"})
+
+ for k,v in pairs(t) do
+ perror(string.rep("\t", i) .. tostring(k) .. "\t" .. tostring(v))
+ if type(v) == "table" and (not maxdepth or i < maxdepth) then
+ if not seen[v] then
+ seen[v] = true
+ dumptable(v, maxdepth, i+1, seen)
+ else
+ perror(string.rep("\t", i) .. "*** RECURSION ***")
+ end
+ end
+ end
+end
+
+
+--
+-- String and data manipulation routines
+--
+
+-- compatibility wrapper for xml.pcdata
+function pcdata(value)
+ local xml = require "luci.xml"
+
+ perror("luci.util.pcdata() has been replaced by luci.xml.pcdata() - Please update your code.")
+ return xml.pcdata(value)
+end
+
+function urlencode(value)
+ if value ~= nil then
+ local str = tostring(value)
+ return lhttp.urlencode(str, lhttp.ENCODE_IF_NEEDED + lhttp.ENCODE_FULL)
+ or str
+ end
+ return nil
+end
+
+function urldecode(value, decode_plus)
+ if value ~= nil then
+ local flag = decode_plus and lhttp.DECODE_PLUS or 0
+ local str = tostring(value)
+ return lhttp.urldecode(str, lhttp.DECODE_IF_NEEDED + flag)
+ or str
+ end
+ return nil
+end
+
+-- compatibility wrapper for xml.striptags
+function striptags(value)
+ local xml = require "luci.xml"
+
+ perror("luci.util.striptags() has been replaced by luci.xml.striptags() - Please update your code.")
+ return xml.striptags(value)
+end
+
+function shellquote(value)
+ return string.format("'%s'", string.gsub(value or "", "'", "'\\''"))
+end
+
+-- for bash, ash and similar shells single-quoted strings are taken
+-- literally except for single quotes (which terminate the string)
+-- (and the exception noted below for dash (-) at the start of a
+-- command line parameter).
+function shellsqescape(value)
+ local res
+ res, _ = string.gsub(value, "'", "'\\''")
+ return res
+end
+
+-- bash, ash and other similar shells interpret a dash (-) at the start
+-- of a command-line parameters as an option indicator regardless of
+-- whether it is inside a single-quoted string. It must be backlash
+-- escaped to resolve this. This requires in some funky special-case
+-- handling. It may actually be a property of the getopt function
+-- rather than the shell proper.
+function shellstartsqescape(value)
+ res, _ = string.gsub(value, "^%-", "\\-")
+ return shellsqescape(res)
+end
+
+-- containing the resulting substrings. The optional max parameter specifies
+-- the number of bytes to process, regardless of the actual length of the given
+-- string. The optional last parameter, regex, specifies whether the separator
+-- sequence is interpreted as regular expression.
+-- pattern as regular expression (optional, default is false)
+function split(str, pat, max, regex)
+ pat = pat or "\n"
+ max = max or #str
+
+ local t = {}
+ local c = 1
+
+ if #str == 0 then
+ return {""}
+ end
+
+ if #pat == 0 then
+ return nil
+ end
+
+ if max == 0 then
+ return str
+ end
+
+ repeat
+ local s, e = str:find(pat, c, not regex)
+ max = max - 1
+ if s and max < 0 then
+ t[#t+1] = str:sub(c)
+ else
+ t[#t+1] = str:sub(c, s and s - 1)
+ end
+ c = e and e + 1 or #str + 1
+ until not s or max < 0
+
+ return t
+end
+
+function trim(str)
+ return (str:gsub("^%s*(.-)%s*$", "%1"))
+end
+
+function cmatch(str, pat)
+ local count = 0
+ for _ in str:gmatch(pat) do count = count + 1 end
+ return count
+end
+
+-- one token per invocation, the tokens are separated by whitespace. If the
+-- input value is a table, it is transformed into a string first. A nil value
+-- will result in a valid iterator which aborts with the first invocation.
+function imatch(v)
+ if type(v) == "table" then
+ local k = nil
+ return function()
+ k = next(v, k)
+ return v[k]
+ end
+
+ elseif type(v) == "number" or type(v) == "boolean" then
+ local x = true
+ return function()
+ if x then
+ x = false
+ return tostring(v)
+ end
+ end
+
+ elseif type(v) == "userdata" or type(v) == "string" then
+ return tostring(v):gmatch("%S+")
+ end
+
+ return function() end
+end
+
+-- value or 0 if the unit is unknown. Upper- or lower case is irrelevant.
+-- Recognized units are:
+-- o "y" - one year (60*60*24*366)
+-- o "m" - one month (60*60*24*31)
+-- o "w" - one week (60*60*24*7)
+-- o "d" - one day (60*60*24)
+-- o "h" - one hour (60*60)
+-- o "min" - one minute (60)
+-- o "kb" - one kilobyte (1024)
+-- o "mb" - one megabyte (1024*1024)
+-- o "gb" - one gigabyte (1024*1024*1024)
+-- o "kib" - one si kilobyte (1000)
+-- o "mib" - one si megabyte (1000*1000)
+-- o "gib" - one si gigabyte (1000*1000*1000)
+function parse_units(ustr)
+
+ local val = 0
+
+ -- unit map
+ local map = {
+ -- date stuff
+ y = 60 * 60 * 24 * 366,
+ m = 60 * 60 * 24 * 31,
+ w = 60 * 60 * 24 * 7,
+ d = 60 * 60 * 24,
+ h = 60 * 60,
+ min = 60,
+
+ -- storage sizes
+ kb = 1024,
+ mb = 1024 * 1024,
+ gb = 1024 * 1024 * 1024,
+
+ -- storage sizes (si)
+ kib = 1000,
+ mib = 1000 * 1000,
+ gib = 1000 * 1000 * 1000
+ }
+
+ -- parse input string
+ for spec in ustr:lower():gmatch("[0-9%.]+[a-zA-Z]*") do
+
+ local num = spec:gsub("[^0-9%.]+$","")
+ local spn = spec:gsub("^[0-9%.]+", "")
+
+ if map[spn] or map[spn:sub(1,1)] then
+ val = val + num * ( map[spn] or map[spn:sub(1,1)] )
+ else
+ val = val + num
+ end
+ end
+
+
+ return val
+end
+
+-- also register functions above in the central string class for convenience
+string.split = split
+string.trim = trim
+string.cmatch = cmatch
+string.parse_units = parse_units
+
+
+function append(src, ...)
+ for i, a in ipairs({...}) do
+ if type(a) == "table" then
+ for j, v in ipairs(a) do
+ src[#src+1] = v
+ end
+ else
+ src[#src+1] = a
+ end
+ end
+ return src
+end
+
+function combine(...)
+ return append({}, ...)
+end
+
+function contains(table, value)
+ for k, v in pairs(table) do
+ if value == v then
+ return k
+ end
+ end
+ return false
+end
+
+-- Both table are - in fact - merged together.
+function update(t, updates)
+ for k, v in pairs(updates) do
+ t[k] = v
+ end
+end
+
+function keys(t)
+ local keys = { }
+ if t then
+ for k, _ in kspairs(t) do
+ keys[#keys+1] = k
+ end
+ end
+ return keys
+end
+
+function clone(object, deep)
+ local copy = {}
+
+ for k, v in pairs(object) do
+ if deep and type(v) == "table" then
+ v = clone(v, deep)
+ end
+ copy[k] = v
+ end
+
+ return setmetatable(copy, getmetatable(object))
+end
+
+
+-- Serialize the contents of a table value.
+function _serialize_table(t, seen)
+ assert(not seen[t], "Recursion detected.")
+ seen[t] = true
+
+ local data = ""
+ local idata = ""
+ local ilen = 0
+
+ for k, v in pairs(t) do
+ if type(k) ~= "number" or k < 1 or math.floor(k) ~= k or ( k - #t ) > 3 then
+ k = serialize_data(k, seen)
+ v = serialize_data(v, seen)
+ data = data .. ( #data > 0 and ", " or "" ) ..
+ '[' .. k .. '] = ' .. v
+ elseif k > ilen then
+ ilen = k
+ end
+ end
+
+ for i = 1, ilen do
+ local v = serialize_data(t[i], seen)
+ idata = idata .. ( #idata > 0 and ", " or "" ) .. v
+ end
+
+ return idata .. ( #data > 0 and #idata > 0 and ", " or "" ) .. data
+end
+
+-- with loadstring().
+function serialize_data(val, seen)
+ seen = seen or setmetatable({}, {__mode="k"})
+
+ if val == nil then
+ return "nil"
+ elseif type(val) == "number" then
+ return val
+ elseif type(val) == "string" then
+ return "%q" % val
+ elseif type(val) == "boolean" then
+ return val and "true" or "false"
+ elseif type(val) == "function" then
+ return "loadstring(%q)" % get_bytecode(val)
+ elseif type(val) == "table" then
+ return "{ " .. _serialize_table(val, seen) .. " }"
+ else
+ return '"[unhandled data type:' .. type(val) .. ']"'
+ end
+end
+
+function restore_data(str)
+ return loadstring("return " .. str)()
+end
+
+
+--
+-- Byte code manipulation routines
+--
+
+-- will be stripped before it is returned.
+function get_bytecode(val)
+ local code
+
+ if type(val) == "function" then
+ code = string.dump(val)
+ else
+ code = string.dump( loadstring( "return " .. serialize_data(val) ) )
+ end
+
+ return code -- and strip_bytecode(code)
+end
+
+-- numbers and debugging numbers will be discarded. Original version by
+-- Peter Cawley (http://lua-users.org/lists/lua-l/2008-02/msg01158.html)
+function strip_bytecode(code)
+ local version, format, endian, int, size, ins, num, lnum = code:byte(5, 12)
+ local subint
+ if endian == 1 then
+ subint = function(code, i, l)
+ local val = 0
+ for n = l, 1, -1 do
+ val = val * 256 + code:byte(i + n - 1)
+ end
+ return val, i + l
+ end
+ else
+ subint = function(code, i, l)
+ local val = 0
+ for n = 1, l, 1 do
+ val = val * 256 + code:byte(i + n - 1)
+ end
+ return val, i + l
+ end
+ end
+
+ local function strip_function(code)
+ local count, offset = subint(code, 1, size)
+ local stripped = { string.rep("\0", size) }
+ local dirty = offset + count
+ offset = offset + count + int * 2 + 4
+ offset = offset + int + subint(code, offset, int) * ins
+ count, offset = subint(code, offset, int)
+ for n = 1, count do
+ local t
+ t, offset = subint(code, offset, 1)
+ if t == 1 then
+ offset = offset + 1
+ elseif t == 4 then
+ offset = offset + size + subint(code, offset, size)
+ elseif t == 3 then
+ offset = offset + num
+ elseif t == 254 or t == 9 then
+ offset = offset + lnum
+ end
+ end
+ count, offset = subint(code, offset, int)
+ stripped[#stripped+1] = code:sub(dirty, offset - 1)
+ for n = 1, count do
+ local proto, off = strip_function(code:sub(offset, -1))
+ stripped[#stripped+1] = proto
+ offset = offset + off - 1
+ end
+ offset = offset + subint(code, offset, int) * int + int
+ count, offset = subint(code, offset, int)
+ for n = 1, count do
+ offset = offset + subint(code, offset, size) + size + int * 2
+ end
+ count, offset = subint(code, offset, int)
+ for n = 1, count do
+ offset = offset + subint(code, offset, size) + size
+ end
+ stripped[#stripped+1] = string.rep("\0", int * 3)
+ return table.concat(stripped), offset
+ end
+
+ return code:sub(1,12) .. strip_function(code:sub(13,-1))
+end
+
+
+--
+-- Sorting iterator functions
+--
+
+function _sortiter( t, f )
+ local keys = { }
+
+ local k, v
+ for k, v in pairs(t) do
+ keys[#keys+1] = k
+ end
+
+ local _pos = 0
+
+ table.sort( keys, f )
+
+ return function()
+ _pos = _pos + 1
+ if _pos <= #keys then
+ return keys[_pos], t[keys[_pos]], _pos
+ end
+ end
+end
+
+-- the provided callback function.
+function spairs(t,f)
+ return _sortiter( t, f )
+end
+
+-- The table pairs are sorted by key.
+function kspairs(t)
+ return _sortiter( t )
+end
+
+-- The table pairs are sorted by value.
+function vspairs(t)
+ return _sortiter( t, function (a,b) return t[a] < t[b] end )
+end
+
+
+--
+-- System utility functions
+--
+
+function bigendian()
+ return string.byte(string.dump(function() end), 7) == 0
+end
+
+function exec(command)
+ local pp = io.popen(command)
+ local data = pp:read("*a")
+ pp:close()
+
+ return data
+end
+
+function execi(command)
+ local pp = io.popen(command)
+
+ return pp and function()
+ local line = pp:read()
+
+ if not line then
+ pp:close()
+ end
+
+ return line
+ end
+end
+
+-- Deprecated
+function execl(command)
+ local pp = io.popen(command)
+ local line = ""
+ local data = {}
+
+ while true do
+ line = pp:read()
+ if (line == nil) then break end
+ data[#data+1] = line
+ end
+ pp:close()
+
+ return data
+end
+
+
+local ubus_codes = {
+ "INVALID_COMMAND",
+ "INVALID_ARGUMENT",
+ "METHOD_NOT_FOUND",
+ "NOT_FOUND",
+ "NO_DATA",
+ "PERMISSION_DENIED",
+ "TIMEOUT",
+ "NOT_SUPPORTED",
+ "UNKNOWN_ERROR",
+ "CONNECTION_FAILED"
+}
+
+local function ubus_return(...)
+ if select('#', ...) == 2 then
+ local rv, err = select(1, ...), select(2, ...)
+ if rv == nil and type(err) == "number" then
+ return nil, err, ubus_codes[err]
+ end
+ end
+
+ return ...
+end
+
+function ubus(object, method, data, path, timeout)
+ if not _ubus_connection then
+ _ubus_connection = _ubus.connect(path, timeout)
+ assert(_ubus_connection, "Unable to establish ubus connection")
+ end
+
+ if object and method then
+ if type(data) ~= "table" then
+ data = { }
+ end
+ return ubus_return(_ubus_connection:call(object, method, data))
+ elseif object then
+ return _ubus_connection:signatures(object)
+ else
+ return _ubus_connection:objects()
+ end
+end
+
+function serialize_json(x, cb)
+ local js = json.stringify(x)
+ if type(cb) == "function" then
+ cb(js)
+ else
+ return js
+ end
+end
+
+
+function libpath()
+ return require "nixio.fs".dirname(ldebug.__file__)
+end
+
+function checklib(fullpathexe, wantedlib)
+ local fs = require "nixio.fs"
+ local haveldd = fs.access('/usr/bin/ldd')
+ local haveexe = fs.access(fullpathexe)
+ if not haveldd or not haveexe then
+ return false
+ end
+ local libs = exec(string.format("/usr/bin/ldd %s", shellquote(fullpathexe)))
+ if not libs then
+ return false
+ end
+ for k, v in ipairs(split(libs)) do
+ if v:find(wantedlib) then
+ return true
+ end
+ end
+ return false
+end
+
+-------------------------------------------------------------------------------
+-- Coroutine safe xpcall and pcall versions
+--
+-- Encapsulates the protected calls with a coroutine based loop, so errors can
+-- be dealed without the usual Lua 5.x pcall/xpcall issues with coroutines
+-- yielding inside the call to pcall or xpcall.
+--
+-- Authors: Roberto Ierusalimschy and Andre Carregal
+-- Contributors: Thomas Harning Jr., Ignacio BurgueƱo, Fabio Mascarenhas
+--
+-- Copyright 2005 - Kepler Project
+--
+-- $Id: coxpcall.lua,v 1.13 2008/05/19 19:20:02 mascarenhas Exp $
+-------------------------------------------------------------------------------
+
+-------------------------------------------------------------------------------
+-- Implements xpcall with coroutines
+-------------------------------------------------------------------------------
+local coromap = setmetatable({}, { __mode = "k" })
+
+local function handleReturnValue(err, co, status, ...)
+ if not status then
+ return false, err(debug.traceback(co, (...)), ...)
+ end
+ if coroutine.status(co) == 'suspended' then
+ return performResume(err, co, coroutine.yield(...))
+ else
+ return true, ...
+ end
+end
+
+function performResume(err, co, ...)
+ return handleReturnValue(err, co, coroutine.resume(co, ...))
+end
+
+local function id(trace, ...)
+ return trace
+end
+
+function coxpcall(f, err, ...)
+ local current = coroutine.running()
+ if not current then
+ if err == id then
+ return pcall(f, ...)
+ else
+ if select("#", ...) > 0 then
+ local oldf, params = f, { ... }
+ f = function() return oldf(unpack(params)) end
+ end
+ return xpcall(f, err)
+ end
+ else
+ local res, co = pcall(coroutine.create, f)
+ if not res then
+ local newf = function(...) return f(...) end
+ co = coroutine.create(newf)
+ end
+ coromap[co] = current
+ coxpt[co] = coxpt[current] or current or 0
+ return performResume(err, co, ...)
+ end
+end
+
+function copcall(f, ...)
+ return coxpcall(f, id, ...)
+end