-- -- This file is part of the Doodle3D project (http://doodle3d.com). -- -- @copyright 2013, Doodle3D -- @license This software is licensed under the terms of the GNU GPL v2 or later. -- See file LICENSE.txt or visit http://www.gnu.org/licenses/gpl.html for full license details. --- -- The settings interface reads and writes configuration keys using [UCI](http://wiki.openwrt.org/doc/uci). -- All keys have pre-defined defaults in @{conf_defaults} which will be used -- if no value is stored in the UCI config. The UCI config file is `/etc/config/wifibox`. -- The default values guarantee there will always be a set of reasonable settings -- to use and provide a clear overview of all existing configuration keys as well. -- uci api: http://wiki.openwrt.org/doc/techref/uci, http://luci.subsignal.org/api/luci/modules/luci.model.uci.html local uci = require('uci').cursor() local utils = require('util.utils') local baseconfig = require('conf_defaults') local utils = require('util.utils') local log = require('util.logger') local MOD_ABBR = "USET" local M = {} --- UCI config name (i.e., file under `/etc/config`) local UCI_CONFIG_NAME = 'wifibox' --- Absolute path to the UCI config file local UCI_CONFIG_FILE = '/etc/config/' .. UCI_CONFIG_NAME --- [Section type](http://wiki.openwrt.org/doc/techref/uci#about.uci.structure) that will be used in @{UCI_CONFIG_FILE} local UCI_CONFIG_TYPE = 'settings' --- [Section name](http://wiki.openwrt.org/doc/techref/uci#about.uci.structure) that will be used for 'public' settings (as predefined in conf_defaults.lua) in @{UCI_CONFIG_FILE} local UCI_CONFIG_SECTION = 'general' --- [Section name](http://wiki.openwrt.org/doc/techref/uci#about.uci.structure) that will be used for 'firmware-local' settings in @{UCI_CONFIG_FILE} local UCI_CONFIG_SYSTEM_SECTION = 'system' local ERR_NO_SUCH_KEY = "key does not exist" --- Returns a key with all periods ('.') replaced by underscores ('_'). -- @string key The key for which to substitute dots. -- @return The substituted key, or the key parameter itself if it is not of type 'string'. local function replaceDots(key) if type(key) ~= 'string' then return key end local r = key:gsub('%.', '_') return r end --- Returns a key with all underscores ('_') replaced by periods ('.'). -- @string key The key for which to substitute underscores. -- @return The substituted key, or the key parameter itself if it is not of type 'string'. local function replaceUnderscores(key) if type(key) ~= 'string' then return key end local r = key:gsub('_', '%.') return r end --- Converts a lua value to equivalent representation for UCI. -- Boolean values are converted to '1' and '0', everything else is converted to a string. -- -- @p v The value to convert. -- @p vType The type of the given value. -- @return A value usable to write to UCI. local function toUciValue(v, vType) if vType == 'bool' then return v and '1' or '0' end if(vType == 'string') then v = v:gsub('[\n\r]', '\\n') end return tostring(v) end --- Converts a value read from UCI to a correctly typed lua value. -- For boolean, '1' is converted to true and everything else to false. Floats -- and ints are converted to numbers and everything else will be returned as is. -- -- @p v The value to convert. -- @p vType The type of the given value. -- @return A lua value typed correctly with regard to the vType parameter. local function fromUciValue(v, vType) if v == nil then return nil end if vType == 'bool' then return (v == '1') and true or false elseif vType == 'float' or vType == 'int' then return tonumber(v) elseif vType == 'string' then v = v:gsub('\\n', '\n') return v else return v end end --- Reports whether a value is valid given the constraints specified in a base table. -- @p value The value to test. -- @tab baseTable The base table to use constraint data from (min,max,regex,isValid). -- @treturn bool Returns true if the value is valid, false if it is not. local function isValid(value, baseTable) local varType, min, max, regex, isValid = baseTable.type, baseTable.min, baseTable.max, baseTable.regex, baseTable.isValid if isValid then local ok,msg = isValid(value) if msg == nil then msg = "invalid value" end return ok or nil,msg end if varType == 'bool' then return type(value) == 'boolean' or nil,"invalid bool value" elseif varType == 'int' or varType == 'float' then local numValue = tonumber(value) if numValue == nil then return nil, "invalid number" elseif varType == 'int' and math.floor(numValue) ~= numValue then return nil, "invalid int" elseif min and numValue < min then return nil, "too low" elseif max and numValue > max then return nil, "too high" end elseif varType == 'string' then local ok = true if min and value:len() < min then return nil,"too short" elseif max and value:len() > max then return nil,"too long" elseif regex and value:match(regex) == nil then return nil,"invalid value" end end return true end --- Looks up the table in @{conf_defaults}.lua corresponding to a key. -- @string key The key for which to return the base table. -- @treturn table The base table for key, or nil if it does not exist. local function getBaseKeyTable(key) local base = baseconfig[key] return type(base) == 'table' and base.default ~= nil and base or nil end --- Looks up the table in @{subconf_defaults}.lua corresponding to a key. -- @string key The key for which to return the base table. -- @treturn table The base table for key, or nil if it does not exist. --[[local function getSubBaseKeyTable(key) local base = subconfig[key] return type(base) == 'table' and base.default ~= nil and base or nil end]]-- --- Returns the value of the requested key if it exists. -- @p key The key to return the associated value for. -- @return The associated value, beware (!) that this may be boolean false for keys of 'bool' type, or nil if the key could not be read because of a UCI error. -- @treturn string Message in case of error. function M.get(key) --log:info(MOD_ABBR, "settings:get: "..utils.dump(key)) key = replaceDots(key) -- retrieve settings's base settings from conf_defaults.lua local base = getBaseKeyTable(key) if not base then return nil,ERR_NO_SUCH_KEY end -- check which uci section to read. -- By default it will read from the general section, but if a base setting contains a subSection it will check that subSection local section = UCI_CONFIG_SECTION; if base.subSection ~= nil then section = M.get(base.subSection) end -- get setting from uci local uciV,msg = uci:get(UCI_CONFIG_NAME, section, key) if not uciV and msg ~= nil then local errorMSG = "Issue reading setting '"..utils.dump(key).."': "..utils.dump(msg); log:warning(MOD_ABBR, errorMSG) return nil, errorMSG; end -- convert uci value into proper lua value local uciV = fromUciValue(uciV, base.type) if uciV ~= nil then -- returning value from uci return uciV elseif base.subSection ~= nil then local subDefault = base["default_"..section] if subDefault ~= nil then -- returning subsection default value return subDefault end end -- returning default value return base.default end --- Returns all configuration keys with their current values. -- @return A table containing a key/value pair for each configuration key, or nil if a UCI error occured. -- @return string Message in case of error. function M.getAll() local result = {} for k,_ in pairs(baseconfig) do if not k:match('^[A-Z_]*$') then --TEMP: skip 'constants', which should be moved anyway local key = replaceUnderscores(k) local v, msg = M.get(key) if not v and msg ~= nil then return nil, msg else result[key] = v end end end return result end --- Reports whether or not a key exists. -- @string key The key to find. -- @treturn bool True if the key exists, false if not. function M.exists(key) key = replaceDots(key) return getBaseKeyTable(key) ~= nil end --- Reports whether or not a key is at its default value. -- 'Default' in this regard means that no UCI value is defined. This means that -- if for instance, the default is 'abc', and UCI contains a configured value of -- 'abc' as well, that key is _not_ a default value. -- -- @string key The key to report about. -- @treturn bool True if the key is currently at its default value, false if not. function M.isDefault(key) key = replaceDots(key) if not M.exists(key) then return nil,ERR_NO_SUCH_KEY end return uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, key) == nil end --- Sets a key to a new value or reverts it to the default value. -- @string key The key to set. -- @p[opt=nil] value The value or set, or nil to revert key to its default value. -- @p[opt=nil] noCommit If true, do not commit the uci configuration; this is more efficient when setting multiple values -- @treturn bool|nil True if everything went well, false if validation error, nil in case of error. -- @treturn ?string Error message in case first return value is nil (invalid key). function M.set(key, value, noCommit) --log:info(MOD_ABBR, "settings:set: "..utils.dump(key).." to: "..utils.dump(value)) key = replaceDots(key) local r = utils.create(UCI_CONFIG_FILE) local rv, msg = uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SECTION, UCI_CONFIG_TYPE) if not rv and msg ~= nil then local errorMSG = "Issue creating section '"..utils.dump(UCI_CONFIG_SECTION).."': "..utils.dump(msg); log:warning(MOD_ABBR, errorMSG) return nil, errorMSG; end local base = getBaseKeyTable(key) if not base then return false,ERR_NO_SUCH_KEY end --log:info(MOD_ABBR, " base.type: "..utils.dump(base.type)) if base.type == 'bool' then if value ~= "" then value = utils.toboolean(value) else value = nil end elseif base.type == 'int' or base.type == 'float' then value = tonumber(value) if(value == nil) then return false,"Value isn't a valid int or float" end end local valid,m = isValid(value, base) if not valid then return false,m end local section = UCI_CONFIG_SECTION; if base.subSection ~= nil then section = M.get(base.subSection) local rv, msg = uci:set(UCI_CONFIG_NAME, section, UCI_CONFIG_TYPE) if not rv and msg ~= nil then local errorMSG = "Issue getting subsection '"..utils.dump(base.subSection).."': "..utils.dump(msg); log:warning(MOD_ABBR, errorMSG) return nil, errorMSG; end end if value ~= nil then local rv, msg = uci:set(UCI_CONFIG_NAME, section, key, toUciValue(value, base.type)) if not rv and msg ~= nil then local errorMSG = "Issue setting setting '"..utils.dump(key).."' in section '"..utils.dump(section).."': "..utils.dump(msg); log:warning(MOD_ABBR, errorMSG) return nil, errorMSG; end else local rv, msg = uci:delete(UCI_CONFIG_NAME, section, key) if not rv and msg ~= nil then local errorMSG = "Issue deleting setting '"..utils.dump(key).."' in section '"..utils.dump(section).."': "..utils.dump(msg); log:warning(MOD_ABBR, errorMSG) return nil, errorMSG; end end if noCommit ~= true then uci:commit(UCI_CONFIG_NAME) end return true end --- Commit the UCI configuration, this can be used after making multiple changes -- which have not been committed yet. function M.commit() uci:commit(UCI_CONFIG_NAME) end --- Reset all settings to their default values -- @string key The key to set. -- @treturn bool|nil True if everything went well, nil in case of error. function M.resetAll() log:info(MOD_ABBR, "settings:resetAll") -- find all sections local allSections, msg = uci:get_all(UCI_CONFIG_NAME) if not allSections and msg ~= nil then local errorMSG = "Issue reading all settings: "..utils.dump(msg); log:warning(MOD_ABBR, errorMSG) return nil, errorMSG; end -- delete all uci sections but system for key,value in pairs(allSections) do if key ~= "system" and not key:match('^[A-Z_]*$') then --TEMP: skip 'constants', which should be moved anyway local rv, msg = uci:delete(UCI_CONFIG_NAME,key) if not rv and msg ~= nil then local errorMSG = "Issue deleting setting '"..utils.dump(key).."': "..utils.dump(msg); log:warning(MOD_ABBR, errorMSG) return nil, errorMSG; end end end -- reset all to defaults for k,_ in pairs(baseconfig) do if not k:match('^[A-Z_]*$') then --TEMP: skip 'constants', which should be moved anyway M.reset(k,true) end end M.commit() return true end --- Reset setting to default value -- @string key The key to reset. -- @p[opt=nil] noCommit If true, do not commit the uci configuration; this is more efficient when resetting multiple values -- @treturn bool|nil True if everything went well, nil in case of error. function M.reset(key, noCommit) log:info(MOD_ABBR, "settings:reset: "..utils.dump(key)) -- delete key = replaceDots(key) local base = getBaseKeyTable(key) if not base then return nil,ERR_NO_SUCH_KEY end local section = UCI_CONFIG_SECTION; if base.subSection ~= nil then section = M.get(base.subSection) end local rv, msg = uci:delete(UCI_CONFIG_NAME, section, key) -- we can't respond to errors in general here because when a key isn't found -- (which always happens when reset is used in resetall) it will also generate a error --if not rv and msg ~= nil then -- local errorMSG = "Issue deleting setting '"..utils.dump(key).."' in section '"..section.."': "..utils.dump(msg); -- log:warning(MOD_ABBR, errorMSG) -- return nil, errorMSG; --end -- reuse get logic to retrieve default and set it. M.set(key,M.get(key),true) if noCommit ~= true then uci:commit(UCI_CONFIG_NAME) end return true end --- Returns a UCI configuration key from the system section. -- @string key The key for which to return the value, must be non-empty. -- @return Requested value or false if it does not exist or nil on UCI error. function M.getSystemKey(key) if type(key) ~= 'string' or key:len() == 0 then return nil end local v,msg = uci:get(UCI_CONFIG_NAME, UCI_CONFIG_SYSTEM_SECTION, key) if not v and msg ~= nil then local errorMSG = "Issue getting system setting '"..utils.dump(key).."' in section '"..UCI_CONFIG_SYSTEM_SECTION.."': "..utils.dump(msg); return nil, errorMSG; end return v or false end --- Sets the value of a UCI key in the system section. -- Note that unlike the public settings, system keys are untyped and value must -- be of type string; UCI generally uses '1' and '0' for boolean values. -- @string key The key to set, must be non-empty. -- @string value The value to set key to. -- @return True on success or nil if key or value arguments are invalid. function M.setSystemKey(key, value) if type(key) ~= 'string' or key:len() == 0 then return nil end if type(value) ~= 'string' then return nil end local r = utils.create(UCI_CONFIG_FILE) -- make sure the file exists for uci to write to uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SYSTEM_SECTION, UCI_CONFIG_TYPE) uci:set(UCI_CONFIG_NAME, UCI_CONFIG_SYSTEM_SECTION, key, value) uci:commit(UCI_CONFIG_NAME) return true end return M