diff options
Diffstat (limited to 'applications/luci-app-radicale/luasrc')
4 files changed, 1042 insertions, 0 deletions
diff --git a/applications/luci-app-radicale/luasrc/controller/radicale.lua b/applications/luci-app-radicale/luasrc/controller/radicale.lua new file mode 100755 index 0000000000..662c60d5a6 --- /dev/null +++ b/applications/luci-app-radicale/luasrc/controller/radicale.lua @@ -0,0 +1,210 @@ +-- Copyright 2014 Christian Schoenebeck <christian dot schoenebeck at gmail dot com> +-- Licensed under the Apache License, Version 2.0 + +module("luci.controller.radicale", package.seeall) + +local NX = require("nixio") +local NXFS = require("nixio.fs") +local DISP = require "luci.dispatcher" +local HTTP = require("luci.http") +local I18N = require("luci.i18n") -- not globally avalible here +local UTIL = require("luci.util") +local SYS = require("luci.sys") + +function index() + entry( {"admin", "services", "radicale"}, alias("admin", "services", "radicale", "edit"), _("CalDAV/CardDAV"), 58) + entry( {"admin", "services", "radicale", "edit"}, cbi("radicale") ).leaf = true + entry( {"admin", "services", "radicale", "logview"}, call("_logread") ).leaf = true + entry( {"admin", "services", "radicale", "startstop"}, call("_startstop") ).leaf = true + entry( {"admin", "services", "radicale", "status"}, call("_status") ).leaf = true +end + +-- called by XHR.get from detail_logview.htm +function _logread() + -- read application settings + local uci = UCI.cursor() + local logfile = uci:get("radicale", "radicale", "logfile") or "/var/log/radicale" + uci:unload("radicale") + + local ldata=NXFS.readfile(logfile) + if not ldata or #ldata == 0 then + ldata="_nodata_" + end + HTTP.write(ldata) +end + +-- called by XHR.get from detail_startstop.htm +function _startstop() + local pid = get_pid() + if pid > 0 then + SYS.call("/etc/init.d/radicale stop") + NX.nanosleep(1) -- sleep a second + if NX.kill(pid, 0) then -- still running + NX.kill(pid, 9) -- send SIGKILL + end + pid = 0 + else + SYS.call("/etc/init.d/radicale start") + NX.nanosleep(1) -- sleep a second + pid = get_pid() + if pid > 0 and not NX.kill(pid, 0) then + pid = 0 -- process did not start + end + end + HTTP.write(tostring(pid)) -- HTTP needs string not number +end + +-- called by XHR.poll from detail_startstop.htm +function _status() + local pid = get_pid() + HTTP.write(tostring(pid)) -- HTTP needs string not number +end + +-- Application / Service specific information functions ######################## +function luci_app_name() + return "luci-app-radicale" +end + +function service_name() + return "radicale" +end +function service_required() + return "0.10-1" +end +function service_installed() + local v = ipkg_ver_installed("radicale-py2") + if not v or #v == 0 then v = ipkg_ver_installed("radicale-py3") end + if not v or #v == 0 then v = "0" end + return v +end +function service_ok() + return ipkg_ver_compare(service_installed(),">=",service_required()) +end + +function app_title_main() + return [[</a><a href="javascript:alert(']] + .. I18N.translate("Version Information") + .. [[\n\n]] .. luci_app_name() + .. [[\n\t]] .. I18N.translate("Version") .. [[:\t]] + .. (ipkg_ver_installed(luci_app_name()) == "" + and I18N.translate("NOT installed") + or ipkg_ver_installed(luci_app_name()) ) + .. [[\n\n]] .. service_name() .. [[ ]] .. I18N.translate("required") .. [[:]] + .. [[\n\t]] .. I18N.translate("Version") .. [[:\t]] + .. service_required() .. [[ ]] .. I18N.translate("or higher") + .. [[\n\n]] .. service_name() .. [[ ]] .. I18N.translate("installed") .. [[:]] + .. [[\n\t]] .. I18N.translate("Version") .. [[:\t]] + .. (service_installed() == "0" + and I18N.translate("NOT installed") + or service_installed()) + .. [[\n\n]] + .. [[')">]] + .. I18N.translate("Radicale CalDAV/CardDAV Server") +end +function app_title_back() + return [[</a><a href="]] + .. DISP.build_url("admin", "services", "radicale") + .. [[">]] + .. I18N.translate("Radicale CalDAV/CardDAV Server") +end +function app_description() + return I18N.translate("The Radicale Project is a complete CalDAV (calendar) and CardDAV (contact) server solution.") .. [[<br />]] + .. I18N.translate("Calendars and address books are available for both local and remote access, possibly limited through authentication policies.") .. [[<br />]] + .. I18N.translate("They can be viewed and edited by calendar and contact clients on mobile phones or computers.") +end + +-- other multiused functions ################################################### + +--return pid of running process +function get_pid() + return tonumber(SYS.exec([[ps | grep "[p]ython.*[r]adicale" 2>/dev/null | awk '{print $1}']])) or 0 +end + +-- compare versions using "<=" "<" ">" ">=" "=" "<<" ">>" +function ipkg_ver_compare(ver1, comp, ver2) + if not ver1 or not (#ver1 > 0) + or not ver2 or not (#ver2 > 0) + or not comp or not (#comp > 0) then return nil end + -- correct compare string + if comp == "<>" or comp == "><" or comp == "!=" or comp == "~=" then comp = "~=" + elseif comp == "<=" or comp == "<" or comp == "=<" then comp = "<=" + elseif comp == ">=" or comp == ">" or comp == "=>" then comp = ">=" + elseif comp == "=" or comp == "==" then comp = "==" + elseif comp == "<<" then comp = "<" + elseif comp == ">>" then comp = ">" + else return nil end + + local av1 = UTIL.split(ver1, "[%.%-]", nil, true) + local av2 = UTIL.split(ver2, "[%.%-]", nil, true) + + for i = 1, math.max(table.getn(av1),table.getn(av2)), 1 do + local s1 = av1[i] or "" + local s2 = av2[i] or "" + local n1 = tonumber(s1) + local n2 = tonumber(s2) + + -- one numeric and other empty string then set other to 0 + if n1 and not n2 and (not s2 or #s2 == 0) then n2 = 0 end + if n2 and not n1 and (not s1 or #s1 == 0) then n1 = 0 end + + local nc = (n1 and n2) -- numeric compare + + if nc then + -- first "not equal" found return true + if comp == "~=" and (n1 ~= n2) then return true end + -- first "lower" found return true + if (comp == "<" or comp == "<=") and (n1 < n2) then return true end + -- first "greater" found return true + if (comp == ">" or comp == ">=") and (n1 > n2) then return true end + -- not equal then return false + if (n1 ~= n2) then return false end + else + if comp == "~=" and (s1 ~= s2) then return true end + if (comp == "<" or comp == "<=") and (s1 < s2) then return true end + if (comp == ">" or comp == ">=") and (s1 > s2) then return true end + if (s1 ~= s2) then return false end + end + end + -- all equal then true + return true +end + +-- read version information for given package if installed +function ipkg_ver_installed(pkg) + local version = "" + local control = io.open("/usr/lib/opkg/info/%s.control" % pkg, "r") + if control then + local ln + repeat + ln = control:read("*l") + if ln and ln:match("^Version: ") then + version = ln:gsub("^Version: ", "") + break + end + until not ln + control:close() + end + return version +end + +-- replacement of build-in Flag.parse of cbi.lua +-- modified to mark section as changed if value changes +-- current parse did not do this, but it is done AbstaractValue.parse() +function flag_parse(self, section) + local fexists = self.map:formvalue( + luci.cbi.FEXIST_PREFIX .. self.config .. "." .. section .. "." .. self.option) + + if fexists then + local fvalue = self:formvalue(section) and self.enabled or self.disabled + local cvalue = self:cfgvalue(section) + if fvalue ~= self.default or (not self.optional and not self.rmempty) then + self:write(section, fvalue) + else + self:remove(section) + end + if (fvalue ~= cvalue) then self.section.changed = true end + else + self:remove(section) + self.section.changed = true + end +end diff --git a/applications/luci-app-radicale/luasrc/model/cbi/radicale.lua b/applications/luci-app-radicale/luasrc/model/cbi/radicale.lua new file mode 100755 index 0000000000..8abb68869d --- /dev/null +++ b/applications/luci-app-radicale/luasrc/model/cbi/radicale.lua @@ -0,0 +1,748 @@ +-- Copyright 2015 Christian Schoenebeck <christian dot schoenebeck at gmail dot com> +-- 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.") + .. [[<br /><strong>]] + .. translate("Keep in mind to use the correct hashing algorithm !") + .. [[</strong>]] + 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.") + .. [[<br />]] + .. translate("For example, for the 'user' key, '.+' means 'authenticated user'" .. " " + .. "and '.*' means 'anybody' (including anonymous users).") + .. [[<br />]] + .. translate("Section names are only used for naming the rule.") + .. [[<br />]] + .. 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 = [[<h3><strong><br /><font color="red"> ]] + .. translate("Software package '" .. TOOLS.service_name() .. "' is not installed.") + .. [[</font><br /><br /> ]] + .. translate("required") .. [[: ]] .. TOOLS.service_name() .. [[ ]] .. TOOLS.service_required() + .. [[<br /><br /> ]] + .. [[<a href="]] .. DISP.build_url("admin", "system", "packages") ..[[">]] + .. translate("Please install current version !") + .. [[</a><br /> </strong></h3>]] + else + v.value = [[<h3><strong><br /><font color="red"> ]] + .. translate("Software package '" .. TOOLS.service_name() .. "' is outdated.") + .. [[</font><br /><br /> ]] + .. translate("installed") .. [[: ]] .. TOOLS.service_name() .. [[ ]] .. TOOLS.service_installed() + .. [[<br /> ]] + .. translate("required") .. [[: ]] .. TOOLS.service_name() .. [[ ]] .. TOOLS.service_required() + .. [[<br /><br /> ]] + .. [[<a href="]] .. DISP.build_url("admin", "system", "packages") ..[[">]] + .. translate("Please update to current version !") + .. [[</a><br /> </strong></h3>]] + 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") + .. [[<br /><strong>]] + .. translate("Port numbers below 1024 (Privileged ports) are not supported") + .. [[</strong>]] +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 = [[<strong>]] + .. translate("Read only!") + .. [[</strong> ]] + .. translate("Radicale uses '/etc/radicale/users' as htpasswd file.") + .. [[<br /><a href="]] + .. DISP.build_url("admin", "services", "radicale", "edit") .. [[/auth]] + .. [[">]] + .. translate("To edit the file follow this link!") + .. [[</a>]] +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 = [[<strong>]] + .. translate("Read only!") + .. [[</strong> ]] + .. translate("Radicale uses '/etc/radicale/rights' as regexp-based file.") + .. [[<br /><a href="]] + .. DISP.build_url("admin", "services", "radicale", "edit") .. [[/rights]] + .. [[">]] + .. translate("To edit the file follow this link!") + .. [[</a>]] +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") + .. [[<br /><a href="]] + .. DISP.build_url("admin", "services", "radicale", "edit") .. [[/logger]] + .. [[">]] + .. translate("To view latest log file follow this link!") + .. [[</a>]] +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.") + .. [[<br /><strong>]] + .. translate("Setting this parameter to '0' will disable rotation of log-file.") + .. [[</strong>]] +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.") + .. [[<br /><strong>]] + .. translate("Setting this parameter to '0' will disable rotation of log-file.") + .. [[</strong>]] +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 diff --git a/applications/luci-app-radicale/luasrc/view/radicale/btn_startstop.htm b/applications/luci-app-radicale/luasrc/view/radicale/btn_startstop.htm new file mode 100644 index 0000000000..79d1c36297 --- /dev/null +++ b/applications/luci-app-radicale/luasrc/view/radicale/btn_startstop.htm @@ -0,0 +1,49 @@ + +<!-- ++ BEGIN ++ Radicale ++ btn_startstop.htm ++ --> +<script type="text/javascript">//<![CDATA[ + + // show XHR.poll/XHR.get response on button + function _data2elements(x) { + var btn = document.getElementById("cbid.radicale._system._startstop"); + if ( ! btn ) { return; } // security check + if (x.responseText == "0") { + btn.value = "<%:Start%>"; + btn.className = "cbi-button cbi-button-apply"; + btn.disabled = false; + } else { + btn.value = "PID: " + x.responseText; + btn.className = "cbi-button cbi-button-reset"; + btn.disabled = false; + } + } + + // event handler for start/stop button + function onclick_startstop(id) { + // do start/stop + var btnXHR = new XHR(); + btnXHR.get('<%=luci.dispatcher.build_url("admin", "services", "radicale", "startstop")%>', null, + function(x) { _data2elements(x); } + ); + } + + XHR.poll(5, '<%=luci.dispatcher.build_url("admin", "services", "radicale", "status")%>', null, + function(x, data) { _data2elements(x); } + ); + +//]]></script> + +<%+cbi/valueheader%> + +<% if self:cfgvalue(section) ~= false then +-- We need to garantie that function cfgvalue run first to set missing parameters +%> + <!-- style="font-size: 100%;" needed for openwrt theme to fix font size --> + <!-- type="button" onclick="..." enable standard onclick functionalty --> + <input class="cbi-button cbi-input-<%=self.inputstyle or "button" %>" style="font-size: 100%;" type="button" onclick="onclick_startstop(this.id)" + <%= + attr("name", section) .. attr("id", cbid) .. attr("value", self.inputtitle) .. ifattr(self.disabled, "disabled") + %> /> +<% end %> + +<%+cbi/valuefooter%> +<!-- ++ END ++ Radicale ++ btn_startstop.htm ++ --> diff --git a/applications/luci-app-radicale/luasrc/view/radicale/ro_value.htm b/applications/luci-app-radicale/luasrc/view/radicale/ro_value.htm new file mode 100644 index 0000000000..6e05206aa1 --- /dev/null +++ b/applications/luci-app-radicale/luasrc/view/radicale/ro_value.htm @@ -0,0 +1,35 @@ +<%+cbi/valueheader%> + <input type="<%=self.password and 'password" class="cbi-input-password' or 'text" class="cbi-input-text' %>" onchange="cbi_d_update(this.id)"<%= + attr("name", cbid) .. attr("id", cbid) .. attr("value", self:cfgvalue(section) or self.default) .. + ifattr(self.size, "size") .. ifattr(self.placeholder, "placeholder") .. ifattr(self.readonly, "readonly") + %> /> + <% if self.password then %><img src="<%=resource%>/cbi/reload.gif" style="vertical-align:middle" title="<%:Reveal/hide password%>" onclick="var e = document.getElementById('<%=cbid%>'); e.type = (e.type=='password') ? 'text' : 'password';" /><% end %> + <% if #self.keylist > 0 or self.datatype then -%> + <script type="text/javascript">//<![CDATA[ + <% if #self.keylist > 0 then -%> + cbi_combobox_init('<%=cbid%>', { + <%- + for i, k in ipairs(self.keylist) do + -%> + <%-=string.format("%q", k) .. ":" .. string.format("%q", self.vallist[i])-%> + <%-if i<#self.keylist then-%>,<%-end-%> + <%- + end + -%> + }, '<%- if not self.rmempty and not self.optional then -%> + <%-: -- Please choose -- -%> + <%- elseif self.placeholder then -%> + <%-= pcdata(self.placeholder) -%> + <%- end -%>', ' + <%- if self.combobox_manual then -%> + <%-=self.combobox_manual-%> + <%- else -%> + <%-: -- custom -- -%> + <%- end -%>'); + <%- end %> + <% if self.datatype then -%> + cbi_validate_field('<%=cbid%>', <%=tostring((self.optional or self.rmempty) == true)%>, '<%=self.datatype:gsub("'", "\\'")%>'); + <%- end %> + //]]></script> + <% end -%> +<%+cbi/valuefooter%> |