-- Copyright 2015 Christian Schoenebeck -- Licensed under the Apache License, Version 2.0 local NXFS = require("nixio.fs") local DISP = require("luci.dispatcher") local DTYP = require("luci.cbi.datatypes") local HTTP = require("luci.http") local UTIL = require("luci.util") local UCI = require("luci.model.uci") local SYS = require("luci.sys") local TOOLS = require("luci.controller.radicale") -- this application's controller and multiused functions -- ################################################################################################# -- takeover arguments if any -- ################################################ -- then show/edit selected file if arg[1] then local argument = arg[1] local filename = "" -- SimpleForm ------------------------------------------------ local ft = SimpleForm("_text") ft.title = TOOLS.app_title_back() ft.description = TOOLS.app_description() ft.redirect = DISP.build_url("admin", "services", "radicale") .. "#cbi-radicale-" .. argument if argument == "logger" then ft.reset = false ft.submit = translate("Reload") local uci = UCI.cursor() filename = uci:get("radicale", "logger", "file_path") or "/var/log/radicale" uci:unload("radicale") filename = filename .. "/radicale" elseif argument == "auth" then ft.submit = translate("Save") filename = "/etc/radicale/users" elseif argument == "rights" then ft.submit = translate("Save") filename = "/etc/radicale/rights" else error("Invalid argument given as section") end if argument ~= "logger" and not NXFS.access(filename) then NXFS.writefile(filename, "") end -- SimpleSection --------------------------------------------- local fs = ft:section(SimpleSection) if argument == "logger" then fs.title = translate("Log-file Viewer") fs.description = translate("Please press [Reload] button below to reread the file.") elseif argument == "auth" then fs.title = translate("Authentication") fs.description = translate("Place here the 'user:password' pairs for your users which should have access to Radicale.") .. [[
]] .. translate("Keep in mind to use the correct hashing algorithm !") .. [[]] else -- rights fs.title = translate("Rights") fs.description = translate("Authentication login is matched against the 'user' key, " .. "and collection's path is matched against the 'collection' key.") .. " " .. translate("You can use Python's ConfigParser interpolation values %(login)s and %(path)s.") .. " " .. translate("You can also get groups from the user regex in the collection with {0}, {1}, etc.") .. [[
]] .. translate("For example, for the 'user' key, '.+' means 'authenticated user'" .. " " .. "and '.*' means 'anybody' (including anonymous users).") .. [[
]] .. translate("Section names are only used for naming the rule.") .. [[
]] .. translate("Leading or ending slashes are trimmed from collection's path.") end -- TextValue ------------------------------------------------- local tt = fs:option(TextValue, "_textvalue") tt.rmempty = true if argument == "logger" then tt.readonly = true tt.rows = 30 function tt.write() HTTP.redirect(DISP.build_url("admin", "services", "radicale", "edit", argument)) end else tt.rows = 15 function tt.write(self, section, value) if not value then value = "" end NXFS.writefile(filename, value:gsub("\r\n", "\n")) return true --HTTP.redirect(DISP.build_url("admin", "services", "radicale", "edit") .. "#cbi-radicale-" .. argument) end end function tt.cfgvalue() return NXFS.readfile(filename) or string.format(translate("File '%s' not found !"), filename) end return ft end -- ################################################################################################# -- Error handling if not installed or wrong version -- ######################### if not TOOLS.service_ok() then local f = SimpleForm("_no_config") f.title = TOOLS.app_title_main() f.description = TOOLS.app_description() f.submit = false f.reset = false local s = f:section(SimpleSection) local v = s:option(DummyValue, "_update_needed") v.rawhtml = true if TOOLS.service_installed() == "0" then v.value = [[


    ]] .. translate("Software package '" .. TOOLS.service_name() .. "' is not installed.") .. [[

      ]] .. translate("required") .. [[: ]] .. TOOLS.service_name() .. [[ ]] .. TOOLS.service_required() .. [[

    ]] .. [[]] .. translate("Please install current version !") .. [[
 

]] else v.value = [[


    ]] .. translate("Software package '" .. TOOLS.service_name() .. "' is outdated.") .. [[

      ]] .. translate("installed") .. [[: ]] .. TOOLS.service_name() .. [[ ]] .. TOOLS.service_installed() .. [[
      ]] .. translate("required") .. [[: ]] .. TOOLS.service_name() .. [[ ]] .. TOOLS.service_required() .. [[

    ]] .. [[]] .. translate("Please update to current version !") .. [[
 

]] end return f end -- ################################################################################################# -- Error handling if no config, create an empty one -- ######################### if not NXFS.access("/etc/config/radicale") then NXFS.writefile("/etc/config/radicale", "") end -- cbi-map -- ################################################################## local m = Map("radicale") m.title = TOOLS.app_title_main() m.description = TOOLS.app_description() function m.commit_handler(self) if self.changed then -- changes ? os.execute("/etc/init.d/radicale reload &") -- reload configuration end end -- cbi-section "System" -- ##################################################### local sys = m:section( NamedSection, "_system" ) sys.title = translate("System") sys.description = nil function sys.cfgvalue(self, section) return "_dummysection" end -- start/stop button ----------------------------------------------------------- local btn = sys:option(DummyValue, "_startstop") btn.template = "radicale/btn_startstop" btn.inputstyle = nil btn.rmempty = true btn.title = translate("Start / Stop") btn.description = translate("Start/Stop Radicale server") function btn.cfgvalue(self, section) local pid = TOOLS.get_pid(true) if pid > 0 then btn.inputtitle = "PID: " .. pid btn.inputstyle = "reset" btn.disabled = false else btn.inputtitle = translate("Start") btn.inputstyle = "apply" btn.disabled = false end return true end -- enabled --------------------------------------------------------------------- local ena = sys:option(Flag, "_enabled") ena.title = translate("Auto-start") ena.description = translate("Enable/Disable auto-start of Radicale on system start-up and interface events") ena.orientation = "horizontal" -- put description under the checkbox ena.rmempty = false -- we need write function ena.cfgvalue(self, section) return (SYS.init.enabled("radicale")) and "1" or "0" end function ena.write(self, section, value) if value == "1" then return SYS.init.enable("radicale") else return SYS.init.disable("radicale") end end -- cbi-section "Server" -- ##################################################### local srv = m:section( NamedSection, "server", "setting" ) srv.title = translate("Server") srv.description = nil function srv.cfgvalue(self, section) if not self.map:get(section) then -- section might not exist self.map:set(section, nil, self.sectiontype) end return self.map:get(section) end -- hosts ----------------------------------------------------------------------- local sh = srv:option( DynamicList, "hosts" ) sh.title = translate("Address:Port") sh.description = translate("'Hostname:Port' or 'IPv4:Port' or '[IPv6]:Port' Radicale should listen on") .. [[
]] .. translate("Port numbers below 1024 (Privileged ports) are not supported") .. [[]] sh.placeholder = "0.0.0.0:5232" sh.rmempty = true -- realm ----------------------------------------------------------------------- local alm = srv:option( Value, "realm" ) alm.title = translate("Logon message") alm.description = translate("Message displayed in the client when a password is needed.") alm.default = "Radicale - Password Required" alm.rmempty = false function alm.parse(self, section) AbstractValue.parse(self, section, "true") -- otherwise unspecific validate error end function alm.validate(self, value) if value then return value else return self.default end end function alm.write(self, section, value) if value ~= self.default then return self.map:set(section, self.option, value) else return self.map:del(section, self.option) end end -- ssl ------------------------------------------------------------------------- local ssl = srv:option( Flag, "ssl" ) ssl.title = translate("Enable HTTPS") ssl.description = nil ssl.rmempty = false function ssl.parse(self, section) TOOLS.flag_parse(self, section) end function ssl.write(self, section, value) if value == "0" then -- delete all if not https enabled self.map:del(section, "protocol") -- protocol self.map:del(section, "certificate") -- certificate self.map:del(section, "key") -- private key self.map:del(section, "ciphers") -- ciphers return self.map:del(section, self.option) else return self.map:set(section, self.option, value) end end -- protocol -------------------------------------------------------------------- local prt = srv:option( ListValue, "protocol" ) prt.title = translate("SSL Protocol") prt.description = translate("'AUTO' selects the highest protocol version that client and server support.") prt.widget = "select" prt.default = "PROTOCOL_SSLv23" prt:depends ("ssl", "1") prt:value ("PROTOCOL_SSLv23", translate("AUTO")) prt:value ("PROTOCOL_SSLv2", "SSL v2") prt:value ("PROTOCOL_SSLv3", "SSL v3") prt:value ("PROTOCOL_TLSv1", "TLS v1") prt:value ("PROTOCOL_TLSv1_1", "TLS v1.1") prt:value ("PROTOCOL_TLSv1_2", "TLS v1.2") -- certificate ----------------------------------------------------------------- local crt = srv:option( Value, "certificate" ) crt.title = translate("Certificate file") crt.description = translate("Full path and file name of certificate") crt.placeholder = "/etc/radicale/ssl/server.crt" crt.rmempty = false -- force validate/write crt:depends ("ssl", "1") function crt.parse(self, section) local _ssl = ssl:formvalue(section) or "0" local novld = (_ssl == "0") AbstractValue.parse(self, section, novld) -- otherwise unspecific validate error end function crt.validate(self, value) local _ssl = ssl:formvalue(srv.section) or "0" if _ssl == "0" then return "" -- ignore if not https enabled end if value then -- otherwise errors in datatype check if DTYP.file(value) then return value else return nil, self.title .. " - " .. translate("File not found !") end else return nil, self.title .. " - " .. translate("Path/File required !") end end function crt.write(self, section, value) if not value or #value == 0 then return self.map:del(section, self.option) else return self.map:set(section, self.option, value) end end -- key ------------------------------------------------------------------------- local key = srv:option( Value, "key" ) key.title = translate("Private key file") key.description = translate("Full path and file name of private key") key.placeholder = "/etc/radicale/ssl/server.key" key.rmempty = false -- force validate/write key:depends ("ssl", "1") function key.parse(self, section) local _ssl = ssl:formvalue(section) or "0" local novld = (_ssl == "0") AbstractValue.parse(self, section, novld) -- otherwise unspecific validate error end function key.validate(self, value) local _ssl = ssl:formvalue(srv.section) or "0" if _ssl == "0" then return "" -- ignore if not https enabled end if value then -- otherwise errors in datatype check if DTYP.file(value) then return value else return nil, self.title .. " - " .. translate("File not found !") end else return nil, self.title .. " - " .. translate("Path/File required !") end end function key.write(self, section, value) if not value or #value == 0 then return self.map:del(section, self.option) else return self.map:set(section, self.option, value) end end -- ciphers --------------------------------------------------------------------- --local cip = srv:option( Value, "ciphers" ) --cip.title = translate("Ciphers") --cip.description = translate("OPTIONAL: See python's ssl module for available ciphers") --cip.rmempty = true --cip:depends ("ssl", "1") -- cbi-section "Authentication" -- ############################################# local aut = m:section( NamedSection, "auth", "setting" ) aut.title = translate("Authentication") aut.description = translate("Authentication method to allow access to Radicale server.") function aut.cfgvalue(self, section) if not self.map:get(section) then -- section might not exist self.map:set(section, nil, self.sectiontype) end return self.map:get(section) end -- type ----------------------------------------------------------------------- local aty = aut:option( ListValue, "type" ) aty.title = translate("Authentication method") aty.description = nil aty.widget = "select" aty.default = "None" aty:value ("None", translate("None")) aty:value ("htpasswd", translate("htpasswd file")) --aty:value ("IMAP", "IMAP") -- The IMAP authentication module relies on the imaplib module. --aty:value ("LDAP", "LDAP") -- The LDAP authentication module relies on the python-ldap module. --aty:value ("PAM", "PAM") -- The PAM authentication module relies on the python-pam module. --aty:value ("courier", "courier") --aty:value ("HTTP", "HTTP") -- The HTTP authentication module relies on the requests module --aty:value ("remote_user", "remote_user") --aty:value ("custom", translate("custom")) function aty.write(self, section, value) if value ~= "htpasswd" then self.map:del(section, "htpasswd_encryption") elseif value ~= "IMAP" then self.map:del(section, "imap_hostname") self.map:del(section, "imap_port") self.map:del(section, "imap_ssl") end if value ~= self.default then return self.map:set(section, self.option, value) else return self.map:del(section, self.option) end end -- htpasswd_encryption --------------------------------------------------------- local hte = aut:option( ListValue, "htpasswd_encryption" ) hte.title = translate("Encryption method") hte.description = nil hte.widget = "select" hte.default = "crypt" hte:depends ("type", "htpasswd") hte:value ("crypt", translate("crypt")) hte:value ("plain", translate("plain")) hte:value ("sha1", translate("SHA-1")) hte:value ("ssha", translate("salted SHA-1")) -- htpasswd_file (dummy) ------------------------------------------------------- local htf = aut:option( DummyValue, "_htf" ) htf.title = translate("htpasswd file") htf.description = [[]] .. translate("Read only!") .. [[ ]] .. translate("Radicale uses '/etc/radicale/users' as htpasswd file.") .. [[
]] .. translate("To edit the file follow this link!") .. [[]] htf.keylist = {} -- required by template htf.vallist = {} -- required by template htf.template = "radicale/ro_value" htf.readonly = true htf:depends ("type", "htpasswd") function htf.cfgvalue() return "/etc/radicale/users" end -- cbi-section "Rights" -- ##################################################### local rig = m:section( NamedSection, "rights", "setting" ) rig.title = translate("Rights") rig.description = translate("Control the access to data collections.") function rig.cfgvalue(self, section) if not self.map:get(section) then -- section might not exist self.map:set(section, nil, self.sectiontype) end return self.map:get(section) end -- type ----------------------------------------------------------------------- local rty = rig:option( ListValue, "type" ) rty.title = translate("Rights backend") rty.description = nil rty.widget = "select" rty.default = "None" rty:value ("None", translate("Full access for everybody (including anonymous)")) rty:value ("authenticated", translate("Full access for authenticated Users") ) rty:value ("owner_only", translate("Full access for Owner only") ) rty:value ("owner_write", translate("Owner allow write, authenticated users allow read") ) rty:value ("from_file", translate("Rights are based on a regexp-based file") ) --rty:value ("custom", "Custom handler") function rty.write(self, section, value) if value ~= "custom" then self.map:del(section, "custom_handler") end if value ~= self.default then return self.map:set(section, self.option, value) else return self.map:del(section, self.option) end end -- from_file (dummy) ----------------------------------------------------------- local rtf = rig:option( DummyValue, "_rtf" ) rtf.title = translate("RegExp file") rtf.description = [[]] .. translate("Read only!") .. [[ ]] .. translate("Radicale uses '/etc/radicale/rights' as regexp-based file.") .. [[
]] .. translate("To edit the file follow this link!") .. [[]] rtf.keylist = {} -- required by template rtf.vallist = {} -- required by template rtf.template = "radicale/ro_value" rtf.readonly = true rtf:depends ("type", "from_file") function rtf.cfgvalue() return "/etc/radicale/rights" end -- cbi-section "Storage" -- #################################################### local sto = m:section( NamedSection, "storage", "setting" ) sto.title = translate("Storage") sto.description = nil function sto.cfgvalue(self, section) if not self.map:get(section) then -- section might not exist self.map:set(section, nil, self.sectiontype) end return self.map:get(section) end -- type ----------------------------------------------------------------------- local sty = sto:option( ListValue, "type" ) sty.title = translate("Storage backend") sty.description = translate("WARNING: Only 'File-system' is documented and tested by Radicale development") sty.widget = "select" sty.default = "filesystem" sty:value ("filesystem", translate("File-system")) --sty:value ("multifilesystem", translate("") ) --sty:value ("database", translate("Database") ) --sty:value ("custom", translate("Custom") ) function sty.write(self, section, value) if value ~= "filesystem" then self.map:del(section, "filesystem_folder") end if value ~= self.default then return self.map:set(section, self.option, value) else return self.map:del(section, self.option) end end --filesystem_folder ------------------------------------------------------------ local sfi = sto:option( Value, "filesystem_folder" ) sfi.title = translate("Directory") sfi.description = nil sfi.default = "/srv/radicale" sfi.rmempty = false -- force validate/write sfi:depends ("type", "filesystem") function sfi.parse(self, section) local _typ = sty:formvalue(sto.section) or "" local novld = (_typ ~= "filesystem") AbstractValue.parse(self, section, novld) -- otherwise unspecific validate error end function sfi.validate(self, value) local _typ = sty:formvalue(sto.section) or "" if _typ ~= "filesystem" then return "" -- ignore if not htpasswd end if value then -- otherwise errors in datatype check if DTYP.directory(value) then return value else return nil, self.title .. " - " .. translate("Directory not exists/found !") end else return nil, self.title .. " - " .. translate("Directory required !") end end -- cbi-section "Logging" -- #################################################### local log = m:section( NamedSection, "logger", "logging" ) log.title = translate("Logging") log.description = nil function log.cfgvalue(self, section) if not self.map:get(section) then -- section might not exist self.map:set(section, nil, self.sectiontype) end return self.map:get(section) end -- console_level --------------------------------------------------------------- local lco = log:option( ListValue, "console_level" ) lco.title = translate("Console Log level") lco.description = nil lco.widget = "select" lco.default = "ERROR" lco:value ("DEBUG", translate("Debug")) lco:value ("INFO", translate("Info") ) lco:value ("WARNING", translate("Warning") ) lco:value ("ERROR", translate("Error") ) lco:value ("CRITICAL", translate("Critical") ) function lco.write(self, section, value) if value ~= self.default then return self.map:set(section, self.option, value) else return self.map:del(section, self.option) end end -- syslog_level ---------------------------------------------------------------- local lsl = log:option( ListValue, "syslog_level" ) lsl.title = translate("Syslog Log level") lsl.description = nil lsl.widget = "select" lsl.default = "WARNING" lsl:value ("DEBUG", translate("Debug")) lsl:value ("INFO", translate("Info") ) lsl:value ("WARNING", translate("Warning") ) lsl:value ("ERROR", translate("Error") ) lsl:value ("CRITICAL", translate("Critical") ) function lsl.write(self, section, value) if value ~= self.default then return self.map:set(section, self.option, value) else return self.map:del(section, self.option) end end -- file_level ------------------------------------------------------------------ local lfi = log:option( ListValue, "file_level" ) lfi.title = translate("File Log level") lfi.description = nil lfi.widget = "select" lfi.default = "INFO" lfi:value ("DEBUG", translate("Debug")) lfi:value ("INFO", translate("Info") ) lfi:value ("WARNING", translate("Warning") ) lfi:value ("ERROR", translate("Error") ) lfi:value ("CRITICAL", translate("Critical") ) function lfi.write(self, section, value) if value ~= self.default then return self.map:set(section, self.option, value) else return self.map:del(section, self.option) end end -- file_path ------------------------------------------------------------------- local lfp = log:option( Value, "file_path" ) lfp.title = translate("Log-file directory") lfp.description = translate("Directory where the rotating log-files are stored") .. [[
]] .. translate("To view latest log file follow this link!") .. [[]] lfp.default = "/var/log/radicale" function lfp.write(self, section, value) if value ~= self.default then return self.map:set(section, self.option, value) else return self.map:del(section, self.option) end end -- file_maxbytes --------------------------------------------------------------- local lmb = log:option( Value, "file_maxbytes" ) lmb.title = translate("Log-file size") lmb.description = translate("Maximum size of each rotation log-file.") .. [[
]] .. translate("Setting this parameter to '0' will disable rotation of log-file.") .. [[]] lmb.default = "8196" lmb.rmempty = false function lmb.validate(self, value) if value then -- otherwise errors in datatype check if DTYP.uinteger(value) then return value else return nil, self.title .. " - " .. translate("Value is not an Integer >= 0 !") end else return nil, self.title .. " - " .. translate("Value required ! Integer >= 0 !") end end function lmb.write(self, section, value) if value ~= self.default then return self.map:set(section, self.option, value) else return self.map:del(section, self.option) end end -- file_backupcount ------------------------------------------------------------ local lbc = log:option( Value, "file_backupcount" ) lbc.title = translate("Log-backup Count") lbc.description = translate("Number of backup files of log to create.") .. [[
]] .. translate("Setting this parameter to '0' will disable rotation of log-file.") .. [[]] lbc.default = "1" lbc.rmempty = false function lbc.validate(self, value) if value then -- otherwise errors in datatype check if DTYP.uinteger(value) then return value else return nil, self.title .. " - " .. translate("Value is not an Integer >= 0 !") end else return nil, self.title .. " - " .. translate("Value required ! Integer >= 0 !") end end function lbc.write(self, section, value) if value ~= self.default then return self.map:set(section, self.option, value) else return self.map:del(section, self.option) end end -- cbi-section "Encoding" -- ################################################### local enc = m:section( NamedSection, "encoding", "setting" ) enc.title = translate("Encoding") enc.description = translate("Change here the encoding Radicale will use instead of 'UTF-8' " .. "for responses to the client and/or to store data inside collections.") function enc.cfgvalue(self, section) if not self.map:get(section) then -- section might not exist self.map:set(section, nil, self.sectiontype) end return self.map:get(section) end -- request --------------------------------------------------------------------- local enr = enc:option( Value, "request" ) enr.title = translate("Response Encoding") enr.description = translate("Encoding for responding requests.") enr.default = "utf-8" enr.optional = true -- stock ----------------------------------------------------------------------- local ens = enc:option( Value, "stock" ) ens.title = translate("Storage Encoding") ens.description = translate("Encoding for storing local collections.") ens.default = "utf-8" ens.optional = true -- cbi-section "Headers" -- #################################################### local hea = m:section( NamedSection, "headers", "setting" ) hea.title = translate("Additional HTTP headers") hea.description = translate("Cross-origin resource sharing (CORS) is a mechanism that allows restricted resources (e.g. fonts, JavaScript, etc.) " .. "on a web page to be requested from another domain outside the domain from which the resource originated.") function hea.cfgvalue(self, section) if not self.map:get(section) then -- section might not exist self.map:set(section, nil, self.sectiontype) end return self.map:get(section) end -- Access_Control_Allow_Origin ------------------------------------------------- local heo = hea:option( DynamicList, "Access_Control_Allow_Origin" ) heo.title = translate("Access-Control-Allow-Origin") heo.description = nil heo.default = "*" heo.optional = true -- Access_Control_Allow_Methods ------------------------------------------------ local hem = hea:option( DynamicList, "Access_Control_Allow_Methods" ) hem.title = translate("Access-Control-Allow-Methods") hem.description = nil hem.optional = true -- Access_Control_Allow_Headers ------------------------------------------------ local heh = hea:option( DynamicList, "Access_Control_Allow_Headers" ) heh.title = translate("Access-Control-Allow-Headers") heh.description = nil heh.optional = true -- Access_Control_Expose_Headers ----------------------------------------------- local hee = hea:option( DynamicList, "Access_Control_Expose_Headers" ) hee.title = translate("Access-Control-Expose-Headers") hee.description = nil hee.optional = true return m