summaryrefslogtreecommitdiffhomepage
path: root/applications/luci-app-radicale/luasrc/model/cbi/radicale.lua
blob: 8abb68869d3158fafb4d4f9bffd23dcdf4d3b580 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
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">&nbsp;&nbsp;&nbsp;&nbsp;]]
			   .. translate("Software package '" .. TOOLS.service_name() .. "' is not installed.")
			   .. [[</font><br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;]]
			   .. translate("required") .. [[: ]] .. TOOLS.service_name() .. [[ ]] .. TOOLS.service_required()
			   .. [[<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;]]
			   .. [[<a href="]] .. DISP.build_url("admin", "system", "packages") ..[[">]]
			   .. translate("Please install current version !")
			   .. [[</a><br />&nbsp;</strong></h3>]]
	else
		v.value    = [[<h3><strong><br /><font color="red">&nbsp;&nbsp;&nbsp;&nbsp;]]
			   .. translate("Software package '" .. TOOLS.service_name() .. "' is outdated.")
			   .. [[</font><br /><br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;]]
			   .. translate("installed") .. [[: ]] .. TOOLS.service_name() .. [[ ]] .. TOOLS.service_installed()
			   .. [[<br />&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;]]
			   .. translate("required") .. [[: ]] .. TOOLS.service_name() .. [[ ]] .. TOOLS.service_required()
			   .. [[<br /><br />&nbsp;&nbsp;&nbsp;&nbsp;]]
			   .. [[<a href="]] .. DISP.build_url("admin", "system", "packages") ..[[">]]
			   .. translate("Please update to current version !")
			   .. [[</a><br />&nbsp;</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