602bc159af7442872370d1d575f31b85500ffc92
[project/luci.git] / applications / luci-app-ddns / luasrc / model / cbi / ddns / detail.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
3 -- Copyright 2013 Manuel Munz <freifunk at somakoma dot de>
4 -- Copyright 2014 Christian Schoenebeck <christian dot schoenebeck at gmail dot com>
5 -- Licensed to the public under the Apache License 2.0.
6
7 local NX   = require "nixio"
8 local NXFS = require "nixio.fs"
9 local SYS  = require "luci.sys"
10 local UTIL = require "luci.util"
11 local DISP = require "luci.dispatcher"
12 local WADM = require "luci.tools.webadmin"
13 local DTYP = require "luci.cbi.datatypes"
14 local DDNS = require "luci.tools.ddns"          -- ddns multiused functions
15
16 -- takeover arguments -- #######################################################
17 local section   = arg[1]
18
19 -- check supported options -- ##################################################
20 -- saved to local vars here because doing multiple os calls slow down the system
21 local has_ipv6   = DDNS.check_ipv6()    -- IPv6 support
22 local has_ssl    = DDNS.check_ssl()     -- HTTPS support
23 local has_proxy  = DDNS.check_proxy()   -- Proxy support
24 local has_dnstcp = DDNS.check_bind_host()       -- DNS TCP support
25 local has_force  = has_ssl and has_dnstcp       -- Force IP Protocoll
26
27 -- html constants -- ###########################################################
28 local LFLF      = (DDNS.get_theme() == "Bootstrap") and [[<br /><br />]] or [[]]
29 local font_red  = "<font color='red'>"
30 local font_off  = "</font>"
31 local bold_on   = "<strong>"
32 local bold_off  = "</strong>"
33
34 -- error text constants -- #####################################################
35 err_ipv6_plain = translate("IPv6 not supported") .. " - " ..
36                 translate("please select 'IPv4' address version")
37 err_ipv6_basic = bold_on ..
38                         font_red ..
39                                 translate("IPv6 not supported") ..
40                         font_off ..
41                         "<br />" .. translate("please select 'IPv4' address version") ..
42                  bold_off
43 err_ipv6_other = bold_on ..
44                         font_red ..
45                                 translate("IPv6 not supported") ..
46                         font_off ..
47                         "<br />" .. translate("please select 'IPv4' address version in") .. " " ..
48                         [[<a href="]] ..
49                                         DISP.build_url("admin", "services", "ddns", "detail", section) ..
50                                         "?tab.dns." .. section .. "=basic" ..
51                                 [[">]] ..
52                                 translate("Basic Settings") ..
53                         [[</a>]] ..
54                  bold_off
55
56 function err_tab_basic(self)
57         return translate("Basic Settings") .. " - " .. self.title .. ": "
58 end
59 function err_tab_adv(self)
60         return translate("Advanced Settings") .. " - " .. self.title .. ": "
61 end
62 function err_tab_timer(self)
63         return translate("Timer Settings") .. " - " .. self.title .. ": "
64 end
65
66 -- function to verify settings around ip_source
67 -- will use dynamic_dns_lucihelper to check if
68 -- local IP can be read
69 local function _verify_ip_source()
70         -- section is globally defined here be calling agrument (see above)
71         local _network   = "-"
72         local _url       = "-"
73         local _interface = "-"
74         local _script    = "-"
75         local _proxy     = ""
76
77         local _ipv6   = usev6:formvalue(section)
78         local _source = (_ipv6 == "1")
79                         and src6:formvalue(section)
80                         or  src4:formvalue(section)
81         if _source == "network" then
82                 _network = (_ipv6 == "1")
83                         and ipn6:formvalue(section)
84                         or  ipn4:formvalue(section)
85         elseif _source == "web" then
86                 _url = (_ipv6 == "1")
87                         and iurl6:formvalue(section)
88                         or  iurl4:formvalue(section)
89                 -- proxy only needed for checking url
90                 _proxy = (pxy) and pxy:formvalue(section) or ""
91         elseif _source == "interface" then
92                 _interface = ipi:formvalue(section)
93         elseif _source == "script" then
94                 _script = ips:formvalue(section)
95         end
96
97         local command = [[/usr/lib/ddns/dynamic_dns_lucihelper.sh get_local_ip ]] ..
98                 _ipv6 .. [[ ]] .. _source .. [[ ]] .. _network .. [[ ]] ..
99                 _url .. [[ ]] .. _interface .. [[ ']] .. _script.. [[' ]] .. _proxy
100         local ret = SYS.call(command)
101
102         if ret == 0 then
103                 return true     -- valid
104         else
105                 return nil      -- invalid
106         end
107 end
108
109 -- cbi-map definition -- #######################################################
110 m = Map("ddns")
111
112 -- first need to close <a> from cbi map template our <a> closed by template
113 m.title = [[</a><a href="]] .. DISP.build_url("admin", "services", "ddns") .. [[">]] ..
114                 translate("Dynamic DNS")
115
116 m.description = translate("Dynamic DNS allows that your router can be reached with " ..
117                         "a fixed hostname while having a dynamically changing " ..
118                         "IP address.")
119
120 m.redirect = DISP.build_url("admin", "services", "ddns")
121
122 m.on_after_commit = function(self)
123         if self.changed then    -- changes ?
124                 local pid = DDNS.get_pid(section)
125                 if pid > 0 then -- running ?
126                         local tmp = NX.kill(pid, 1)     -- send SIGHUP
127                 end
128         end
129 end
130
131 -- read application settings -- ################################################
132 -- date format; if not set use ISO format
133 date_format = m.uci:get(m.config, "global", "date_format") or "%F %R"
134 -- log directory
135 log_dir = m.uci:get(m.config, "global", "log_dir") or "/var/log/ddns"
136
137 -- cbi-section definition -- ###################################################
138 ns = m:section( NamedSection, section, "service",
139         translate("Details for") .. ([[: <strong>%s</strong>]] % section),
140         translate("Configure here the details for selected Dynamic DNS service.")
141         .. [[<br /><a href="http://wiki.openwrt.org/doc/uci/ddns#version_1x" target="_blank">]]
142         .. translate("For detailed information about parameter settings look here.")
143         .. [[</a>]] )
144 ns.instance = section   -- arg [1]
145 ns:tab("basic", translate("Basic Settings"), nil )
146 ns:tab("advanced", translate("Advanced Settings"), nil )
147 ns:tab("timer", translate("Timer Settings"), nil )
148 ns:tab("logview", translate("Log File Viewer"), nil )
149
150 -- TAB: Basic  #####################################################################################
151 -- enabled  -- #################################################################
152 en = ns:taboption("basic", Flag, "enabled",
153         translate("Enabled"),
154         translate("If this service section is disabled it could not be started." .. "<br />" ..
155                 "Neither from LuCI interface nor from console") )
156 en.orientation = "horizontal"
157 function en.parse(self, section)
158         DDNS.flag_parse(self, section)
159 end
160
161 -- use_ipv6 (NEW)  -- ##########################################################
162 usev6 = ns:taboption("basic", ListValue, "use_ipv6",
163         translate("IP address version"),
164         translate("Defines which IP address 'IPv4/IPv6' is send to the DDNS provider") )
165 usev6.widget  = "radio"
166 usev6.default = "0"
167 usev6:value("0", translate("IPv4-Address") )
168 function usev6.cfgvalue(self, section)
169         local value = AbstractValue.cfgvalue(self, section)
170         if has_ipv6 or (value == "1" and not has_ipv6) then
171                 self:value("1", translate("IPv6-Address") )
172         end
173         if value == "1" and not has_ipv6 then
174                 self.description = err_ipv6_basic
175         end
176         return value
177 end
178 function usev6.validate(self, value)
179         if (value == "1" and has_ipv6) or value == "0" then
180                 return value
181         end
182         return nil, err_tab_basic(self) .. err_ipv6_plain
183 end
184 function usev6.write(self, section, value)
185         if value == "0" then    -- force rmempty
186                 return self.map:del(section, self.option)
187         else
188                 return self.map:set(section, self.option, value)
189         end
190 end
191
192 -- IPv4 - service_name -- ######################################################
193 svc4 = ns:taboption("basic", ListValue, "ipv4_service_name",
194         translate("DDNS Service provider") .. " [IPv4]" )
195 svc4.default    = "-"
196 svc4:depends("use_ipv6", "0")   -- only show on IPv4
197
198 local services4 = { }
199 local fd4 = io.open("/usr/lib/ddns/services", "r")
200
201 if fd4 then
202         local ln
203         repeat
204                 ln = fd4:read("*l")
205                 local s = ln and ln:match('^%s*"([^"]+)"')
206                 if s then services4[#services4+1] = s end
207         until not ln
208         fd4:close()
209 end
210
211 for _, v in UTIL.vspairs(services4) do svc4:value(v) end
212 svc4:value("-", translate("-- custom --") )
213
214 function svc4.cfgvalue(self, section)
215         local v =  DDNS.read_value(self, section, "service_name")
216         if not v or #v == 0 then
217                 return "-"
218         else
219                 return v
220         end
221 end
222 function svc4.validate(self, value)
223         if usev6:formvalue(section) == "0" then -- do only on IPv4
224                 return value
225         else
226                 return ""       -- supress validate error
227         end
228 end
229 function svc4.write(self, section, value)
230         if usev6:formvalue(section) == "0" then -- do only IPv4 here
231                 self.map:del(section, self.option)      -- to be shure
232                 if value ~= "-" then                    -- and write "service_name
233                         self.map:del(section, "update_url")     -- delete update_url
234                         return self.map:set(section, "service_name", value)
235                 else
236                         return self.map:del(section, "service_name")
237                 end
238         end
239 end
240
241 -- IPv6 - service_name -- ######################################################
242 svc6 = ns:taboption("basic", ListValue, "ipv6_service_name",
243         translate("DDNS Service provider") .. " [IPv6]" )
244 svc6.default    = "-"
245 svc6:depends("use_ipv6", "1")   -- only show on IPv6
246 if not has_ipv6 then
247         svc6.description = err_ipv6_basic
248 end
249
250 local services6 = { }
251 local fd6 = io.open("/usr/lib/ddns/services_ipv6", "r")
252
253 if fd6 then
254         local ln
255         repeat
256                 ln = fd6:read("*l")
257                 local s = ln and ln:match('^%s*"([^"]+)"')
258                 if s then services6[#services6+1] = s end
259         until not ln
260         fd6:close()
261 end
262
263 for _, v in UTIL.vspairs(services6) do svc6:value(v) end
264 svc6:value("-", translate("-- custom --") )
265
266 function svc6.cfgvalue(self, section)
267         local v =  DDNS.read_value(self, section, "service_name")
268         if not v or #v == 0 then
269                 return "-"
270         else
271                 return v
272         end
273 end
274 function svc6.validate(self, value)
275         if usev6:formvalue(section) == "1" then -- do only on IPv6
276                 if has_ipv6 then return value end
277                 return nil, err_tab_basic(self) .. err_ipv6_plain
278         else
279                 return ""       -- supress validate error
280         end
281 end
282 function svc6.write(self, section, value)
283         if usev6:formvalue(section) == "1" then -- do only when IPv6
284                 self.map:del(section, self.option)      -- delete "ipv6_service_name" helper
285                 if value ~= "-" then                    -- and write "service_name
286                         self.map:del(section, "update_url")     -- delete update_url
287                         return self.map:set(section, "service_name", value)
288                 else
289                         return self.map:del(section, "service_name")
290                 end
291         end
292 end
293
294 -- IPv4/IPv6 - update_url -- ###################################################
295 uurl = ns:taboption("basic", Value, "update_url",
296         translate("Custom update-URL"),
297         translate("Update URL to be used for updating your DDNS Provider." .. "<br />" ..
298                 "Follow instructions you will find on their WEB page.") )
299 uurl:depends("ipv4_service_name", "-")
300 uurl:depends("ipv6_service_name", "-")
301 function uurl.validate(self, value)
302         local script = ush:formvalue(section)
303
304         if (usev6:formvalue(section) == "0" and svc4:formvalue(section) ~= "-") or
305            (usev6:formvalue(section) == "1" and svc6:formvalue(section) ~= "-") then
306                 return ""       -- suppress validate error
307         elseif not value then
308                 if not script or not (#script > 0) then
309                         return nil, err_tab_basic(self) .. translate("missing / required")
310                 else
311                         return ""       -- suppress validate error / update_script is given
312                 end
313         elseif (#script > 0) then
314                 return nil, err_tab_basic(self) .. translate("either url or script could be set")
315         end
316
317         local url = DDNS.parse_url(value)
318         if not url.scheme == "http" then
319                 return nil, err_tab_basic(self) .. translate("must start with 'http://'")
320         elseif not url.query then
321                 return nil, err_tab_basic(self) .. "<QUERY> " .. translate("missing / required")
322         elseif not url.host then
323                 return nil, err_tab_basic(self) .. "<HOST> " .. translate("missing / required")
324         elseif SYS.call([[nslookup ]] .. url.host .. [[ >/dev/null 2>&1]]) ~= 0 then
325                 return nil, err_tab_basic(self) .. translate("can not resolve host: ") .. url.host
326         end
327
328         return value
329 end
330
331 -- IPv4/IPv6 - update_script -- ################################################
332 ush = ns:taboption("basic", Value, "update_script",
333         translate("Custom update-script"),
334         translate("Custom update script to be used for updating your DDNS Provider.") )
335 ush:depends("ipv4_service_name", "-")
336 ush:depends("ipv6_service_name", "-")
337 function ush.validate(self, value)
338         local url = uurl:formvalue(section)
339
340         if (usev6:formvalue(section) == "0" and svc4:formvalue(section) ~= "-") or
341            (usev6:formvalue(section) == "1" and svc6:formvalue(section) ~= "-") then
342                 return ""       -- suppress validate error
343         elseif not value then
344                 if not url or not (#url > 0) then
345                         return nil, err_tab_basic(self) .. translate("missing / required")
346                 else
347                         return ""       -- suppress validate error / update_url is given
348                 end
349         elseif (#url > 0) then
350                 return nil, err_tab_basic(self) .. translate("either url or script could be set")
351         elseif not NXFS.access(value) then
352                 return nil, err_tab_basic(self) .. translate("File not found")
353         end
354         return value
355 end
356
357 -- IPv4/IPv6 - domain -- #######################################################
358 dom = ns:taboption("basic", Value, "domain",
359                 translate("Hostname/Domain"),
360                 translate("Replaces [DOMAIN] in Update-URL") )
361 dom.rmempty     = false
362 dom.placeholder = "mypersonaldomain.dyndns.org"
363 function dom.validate(self, value)
364         if not value
365         or not (#value > 0)
366         or not DTYP.hostname(value) then
367                 return nil, err_tab_basic(self) ..      translate("invalid - Sample") .. ": 'mypersonaldomain.dyndns.org'"
368         else
369                 return value
370         end
371 end
372
373 -- IPv4/IPv6 - username -- #####################################################
374 user = ns:taboption("basic", Value, "username",
375                 translate("Username"),
376                 translate("Replaces [USERNAME] in Update-URL") )
377 user.rmempty = false
378 function user.validate(self, value)
379         if not value then
380                 return nil, err_tab_basic(self) .. translate("missing / required")
381         end
382         return value
383 end
384
385 -- IPv4/IPv6 - password -- #####################################################
386 pw = ns:taboption("basic", Value, "password",
387                 translate("Password"),
388                 translate("Replaces [PASSWORD] in Update-URL") )
389 pw.rmempty  = false
390 pw.password = true
391 function pw.validate(self, value)
392         if not value then
393                 return nil, err_tab_basic(self) .. translate("missing / required")
394         end
395         return value
396 end
397
398 -- IPv4/IPv6 - use_https (NEW) -- ##############################################
399 if has_ssl or ( ( m:get(section, "use_https") or "0" ) == "1" ) then
400         https = ns:taboption("basic", Flag, "use_https",
401                 translate("Use HTTP Secure") )
402         https.orientation = "horizontal"
403         https.rmempty = false -- force validate function
404         function https.cfgvalue(self, section)
405                 local value = AbstractValue.cfgvalue(self, section)
406                 if not has_ssl and value == "1" then
407                         self.description = bold_on .. font_red ..
408                                 translate("HTTPS not supported") .. font_off .. "<br />" ..
409                                 translate("please disable") .. " !" .. bold_off
410                 else
411                         self.description = translate("Enable secure communication with DDNS provider")
412                 end
413                 return value
414         end
415         function https.parse(self, section)
416                 DDNS.flag_parse(self, section)
417         end
418         function https.validate(self, value)
419                 if (value == "1" and has_ssl ) or value == "0" then return value end
420                 return nil, err_tab_basic(self) .. translate("HTTPS not supported") .. " !"
421         end
422         function https.write(self, section, value)
423                 if value == "1" then
424                         return self.map:set(section, self.option, value)
425                 else
426                         self.map:del(section, "cacert")
427                         return self.map:del(section, self.option)
428                 end
429         end
430 end
431
432 -- IPv4/IPv6 - cacert (NEW) -- #################################################
433 if has_ssl then
434         cert = ns:taboption("basic", Value, "cacert",
435                 translate("Path to CA-Certificate"),
436                 translate("directory or path/file") .. "<br />" ..
437                 translate("or") .. bold_on .. " IGNORE " .. bold_off ..
438                 translate("to run HTTPS without verification of server certificates (insecure)") )
439         cert:depends("use_https", "1")
440         cert.rmempty = false -- force validate function
441         cert.default = "/etc/ssl/certs"
442         function cert.validate(self, value)
443                 if https:formvalue(section) == "0" then
444                         return ""       -- supress validate error if NOT https
445                 end
446                 if value then   -- otherwise errors in datatype check
447                         if DTYP.directory(value)
448                         or DTYP.file(value)
449                         or value == "IGNORE" then
450                                 return value
451                         end
452                 end
453                 return nil, err_tab_basic(self) ..
454                         translate("file or directory not found or not 'IGNORE'") .. " !"
455         end
456 end
457
458 -- TAB: Advanced  ##################################################################################
459 -- IPv4 - ip_source -- #########################################################
460 src4 = ns:taboption("advanced", ListValue, "ipv4_source",
461         translate("IP address source") .. " [IPv4]",
462         translate("Defines the source to read systems IPv4-Address from, that will be send to the DDNS provider") )
463 src4:depends("use_ipv6", "0")   -- IPv4 selected
464 src4.default = "network"
465 src4:value("network", translate("Network"))
466 src4:value("web", translate("URL"))
467 src4:value("interface", translate("Interface"))
468 src4:value("script", translate("Script"))
469 function src4.cfgvalue(self, section)
470         return DDNS.read_value(self, section, "ip_source")
471 end
472 function src4.validate(self, value)
473         if usev6:formvalue(section) == "1" then
474                 return ""       -- ignore on IPv6 selected
475         elseif not _verify_ip_source() then
476                 return nil, err_tab_adv(self) ..
477                         translate("can not detect local IP. Please select a different Source combination")
478         else
479                 return value
480         end
481 end
482 function src4.write(self, section, value)
483         if usev6:formvalue(section) == "1" then
484                 return true     -- ignore on IPv6 selected
485         elseif value == "network" then
486                 self.map:del(section, "ip_url")         -- delete not need parameters
487                 self.map:del(section, "ip_interface")
488                 self.map:del(section, "ip_script")
489         elseif value == "web" then
490                 self.map:del(section, "ip_network")     -- delete not need parameters
491                 self.map:del(section, "ip_interface")
492                 self.map:del(section, "ip_script")
493         elseif value == "interface" then
494                 self.map:del(section, "ip_network")     -- delete not need parameters
495                 self.map:del(section, "ip_url")
496                 self.map:del(section, "ip_script")
497         elseif value == "script" then
498                 self.map:del(section, "ip_network")
499                 self.map:del(section, "ip_url")         -- delete not need parameters
500                 self.map:del(section, "ip_interface")
501         end
502         self.map:del(section, self.option)               -- delete "ipv4_source" helper
503         return self.map:set(section, "ip_source", value) -- and write "ip_source
504 end
505
506 -- IPv6 - ip_source -- #########################################################
507 src6 = ns:taboption("advanced", ListValue, "ipv6_source",
508         translate("IP address source") .. " [IPv6]",
509         translate("Defines the source to read systems IPv6-Address from, that will be send to the DDNS provider") )
510 src6:depends("use_ipv6", 1)     -- IPv6 selected
511 src6.default = "network"
512 src6:value("network", translate("Network"))
513 src6:value("web", translate("URL"))
514 src6:value("interface", translate("Interface"))
515 src6:value("script", translate("Script"))
516 if not has_ipv6 then
517         src6.description = err_ipv6_other
518 end
519 function src6.cfgvalue(self, section)
520         return DDNS.read_value(self, section, "ip_source")
521 end
522 function src6.validate(self, value)
523         if usev6:formvalue(section) == "0" then
524                 return ""       -- ignore on IPv4 selected
525         elseif not has_ipv6 then
526                 return nil, err_tab_adv(self) .. err_ipv6_plain
527         elseif not _verify_ip_source() then
528                 return nil, err_tab_adv(self) ..
529                         translate("can not detect local IP. Please select a different Source combination")
530         else
531                 return value
532         end
533 end
534 function src6.write(self, section, value)
535         if usev6:formvalue(section) == "0" then
536                 return true     -- ignore on IPv4 selected
537         elseif value == "network" then
538                 self.map:del(section, "ip_url")         -- delete not need parameters
539                 self.map:del(section, "ip_interface")
540                 self.map:del(section, "ip_script")
541         elseif value == "web" then
542                 self.map:del(section, "ip_network")     -- delete not need parameters
543                 self.map:del(section, "ip_interface")
544                 self.map:del(section, "ip_script")
545         elseif value == "interface" then
546                 self.map:del(section, "ip_network")     -- delete not need parameters
547                 self.map:del(section, "ip_url")
548                 self.map:del(section, "ip_script")
549         elseif value == "script" then
550                 self.map:del(section, "ip_network")
551                 self.map:del(section, "ip_url")         -- delete not need parameters
552                 self.map:del(section, "ip_interface")
553         end
554         self.map:del(section, self.option)               -- delete "ipv4_source" helper
555         return self.map:set(section, "ip_source", value) -- and write "ip_source
556 end
557
558 -- IPv4 - ip_network (default "wan") -- ########################################
559 ipn4 = ns:taboption("advanced", ListValue, "ipv4_network",
560         translate("Network") .. " [IPv4]",
561         translate("Defines the network to read systems IPv4-Address from") )
562 ipn4:depends("ipv4_source", "network")
563 ipn4.default = "wan"
564 WADM.cbi_add_networks(ipn4)
565 function ipn4.cfgvalue(self, section)
566         return DDNS.read_value(self, section, "ip_network")
567 end
568 function ipn4.validate(self, value)
569         if usev6:formvalue(section) == "1"
570          or src4:formvalue(section) ~= "network" then
571                 -- ignore if IPv6 selected OR
572                 -- ignore everything except "network"
573                 return ""
574         else
575                 return value
576         end
577 end
578 function ipn4.write(self, section, value)
579         if usev6:formvalue(section) == "1"
580          or src4:formvalue(section) ~= "network" then
581                 -- ignore if IPv6 selected OR
582                 -- ignore everything except "network"
583                 return true
584         else
585                 -- set also as "interface" for monitoring events changes/hot-plug
586                 self.map:set(section, "interface", value)
587                 self.map:del(section, self.option)                -- delete "ipv4_network" helper
588                 return self.map:set(section, "ip_network", value) -- and write "ip_network"
589         end
590 end
591
592 -- IPv6 - ip_network (default "wan6") -- #######################################
593 ipn6 = ns:taboption("advanced", ListValue, "ipv6_network",
594         translate("Network") .. " [IPv6]" )
595 ipn6:depends("ipv6_source", "network")
596 ipn6.default = "wan6"
597 WADM.cbi_add_networks(ipn6)
598 if has_ipv6 then
599         ipn6.description = translate("Defines the network to read systems IPv6-Address from")
600 else
601         ipn6.description = err_ipv6_other
602 end
603 function ipn6.cfgvalue(self, section)
604         return DDNS.read_value(self, section, "ip_network")
605 end
606 function ipn6.validate(self, value)
607         if usev6:formvalue(section) == "0"
608          or src6:formvalue(section) ~= "network" then
609                 -- ignore if IPv4 selected OR
610                 -- ignore everything except "network"
611                 return ""
612         elseif has_ipv6 then
613                 return value
614         else
615                 return nil, err_tab_adv(self) .. err_ipv6_plain
616         end
617 end
618 function ipn6.write(self, section, value)
619         if usev6:formvalue(section) == "0"
620          or src6:formvalue(section) ~= "network" then
621                 -- ignore if IPv4 selected OR
622                 -- ignore everything except "network"
623                 return true
624         else
625                 -- set also as "interface" for monitoring events changes/hotplug
626                 self.map:set(section, "interface", value)
627                 self.map:del(section, self.option)                -- delete "ipv6_network" helper
628                 return self.map:set(section, "ip_network", value) -- and write "ip_network"
629         end
630 end
631
632 -- IPv4 - ip_url (default "checkip.dyndns.com") -- #############################
633 iurl4 = ns:taboption("advanced", Value, "ipv4_url",
634         translate("URL to detect") .. " [IPv4]",
635         translate("Defines the Web page to read systems IPv4-Address from") )
636 iurl4:depends("ipv4_source", "web")
637 iurl4.default = "http://checkip.dyndns.com"
638 function iurl4.cfgvalue(self, section)
639         return DDNS.read_value(self, section, "ip_url")
640 end
641 function iurl4.validate(self, value)
642         if usev6:formvalue(section) == "1"
643          or src4:formvalue(section) ~= "web" then
644                 -- ignore if IPv6 selected OR
645                 -- ignore everything except "web"
646                 return ""
647         elseif not value or #value == 0 then
648                 return nil, err_tab_adv(self) .. translate("missing / required")
649         end
650
651         local url = DDNS.parse_url(value)
652         if not (url.scheme == "http" or url.scheme == "https") then
653                 return nil, err_tab_adv(self) .. translate("must start with 'http://'")
654         elseif not url.host then
655                 return nil, err_tab_adv(self) .. "<HOST> " .. translate("missing / required")
656         elseif SYS.call([[nslookup ]] .. url.host .. [[>/dev/null 2>&1]]) ~= 0 then
657                 return nil, err_tab_adv(self) .. translate("can not resolve host: ") .. url.host
658         else
659                 return value
660         end
661 end
662 function iurl4.write(self, section, value)
663         if usev6:formvalue(section) == "1"
664          or src4:formvalue(section) ~= "web" then
665                 -- ignore if IPv6 selected OR
666                 -- ignore everything except "web"
667                 return true
668         else
669                 self.map:del(section, self.option)              -- delete "ipv4_url" helper
670                 return self.map:set(section, "ip_url", value)   -- and write "ip_url"
671         end
672 end
673
674 -- IPv6 - ip_url (default "checkipv6.dyndns.com") -- ###########################
675 iurl6 = ns:taboption("advanced", Value, "ipv6_url",
676         translate("URL to detect") .. " [IPv6]" )
677 iurl6:depends("ipv6_source", "web")
678 iurl6.default = "http://checkipv6.dyndns.com"
679 if has_ipv6 then
680         iurl6.description = translate("Defines the Web page to read systems IPv6-Address from")
681 else
682         iurl6.description = err_ipv6_other
683 end
684 function iurl6.cfgvalue(self, section)
685         return DDNS.read_value(self, section, "ip_url")
686 end
687 function iurl6.validate(self, value)
688         if usev6:formvalue(section) == "0"
689          or src6:formvalue(section) ~= "web" then
690                 -- ignore if IPv4 selected OR
691                 -- ignore everything except "web"
692                 return ""
693         elseif not has_ipv6 then
694                 return nil, err_tab_adv(self) .. err_ipv6_plain
695         elseif not value or #value == 0 then
696                 return nil, err_tab_adv(self) .. translate("missing / required")
697         end
698
699         local url = DDNS.parse_url(value)
700         if not (url.scheme == "http" or url.scheme == "https") then
701                 return nil, err_tab_adv(self) .. translate("must start with 'http://'")
702         elseif not url.host then
703                 return nil, err_tab_adv(self) .. "<HOST> " .. translate("missing / required")
704         elseif SYS.call([[nslookup ]] .. url.host .. [[>/dev/null 2>&1]]) ~= 0 then
705                 return nil, err_tab_adv(self) .. translate("can not resolve host: ") .. url.host
706         else
707                 return value
708         end
709 end
710 function iurl6.write(self, section, value)
711         if usev6:formvalue(section) == "0"
712          or src6:formvalue(section) ~= "web" then
713                 -- ignore if IPv4 selected OR
714                 -- ignore everything except "web"
715                 return true
716         else
717                 self.map:del(section, self.option)              -- delete "ipv6_url" helper
718                 return self.map:set(section, "ip_url", value)   -- and write "ip_url"
719         end
720 end
721
722 -- IPv4 + IPv6 - ip_interface -- ###############################################
723 ipi = ns:taboption("advanced", ListValue, "ip_interface",
724         translate("Interface"),
725         translate("Defines the interface to read systems IP-Address from") )
726 ipi:depends("ipv4_source", "interface") -- IPv4
727 ipi:depends("ipv6_source", "interface") -- or IPv6
728 for _, v in pairs(SYS.net.devices()) do
729         -- show only interface set to a network
730         -- and ignore loopback
731         net = WADM.iface_get_network(v)
732         if net and net ~= "loopback" then
733                 ipi:value(v)
734         end
735 end
736 function ipi.validate(self, value)
737         if (usev6:formvalue(section) == "0" and src4:formvalue(section) ~= "interface")
738         or (usev6:formvalue(section) == "1" and src6:formvalue(section) ~= "interface") then
739                 return ""
740         else
741                 return value
742         end
743 end
744 function ipi.write(self, section, value)
745         if (usev6:formvalue(section) == "0" and src4:formvalue(section) ~= "interface")
746         or (usev6:formvalue(section) == "1" and src6:formvalue(section) ~= "interface") then
747                 return true
748         else
749                 -- get network from device to
750                 -- set also as "interface" for monitoring events changes/hotplug
751                 local net = WADM.iface_get_network(value)
752                 self.map:set(section, "interface", net)
753                 return self.map:set(section, self.option, value)
754         end
755 end
756
757 -- IPv4 + IPv6 - ip_script (NEW) -- ############################################
758 ips = ns:taboption("advanced", Value, "ip_script",
759         translate("Script"),
760         translate("User defined script to read systems IP-Address") )
761 ips:depends("ipv4_source", "script")    -- IPv4
762 ips:depends("ipv6_source", "script")    -- or IPv6
763 ips.rmempty     = false
764 ips.placeholder = "/path/to/script.sh"
765 function ips.validate(self, value)
766         local split
767         if value then split = UTIL.split(value, " ") end
768
769         if (usev6:formvalue(section) == "0" and src4:formvalue(section) ~= "script")
770         or (usev6:formvalue(section) == "1" and src6:formvalue(section) ~= "script") then
771                 return ""
772         elseif not value or not (#value > 0) or not NXFS.access(split[1], "x") then
773                 return nil, err_tab_adv(self) ..
774                         translate("not found or not executable - Sample: '/path/to/script.sh'")
775         else
776                 return value
777         end
778 end
779 function ips.write(self, section, value)
780         if (usev6:formvalue(section) == "0" and src4:formvalue(section) ~= "script")
781         or (usev6:formvalue(section) == "1" and src6:formvalue(section) ~= "script") then
782                 return true
783         else
784                 return self.map:set(section, self.option, value)
785         end
786 end
787
788 -- IPv4 - interface - default "wan" -- #########################################
789 -- event network to monitor changes/hotplug/dynamic_dns_updater.sh
790 -- only needs to be set if "ip_source"="web" or "script"
791 -- if "ip_source"="network" or "interface" we use their network
792 eif4 = ns:taboption("advanced", ListValue, "ipv4_interface",
793         translate("Event Network") .. " [IPv4]",
794         translate("Network on which the ddns-updater scripts will be started") )
795 eif4:depends("ipv4_source", "web")
796 eif4:depends("ipv4_source", "script")
797 eif4.default = "wan"
798 WADM.cbi_add_networks(eif4)
799 function eif4.cfgvalue(self, section)
800         return DDNS.read_value(self, section, "interface")
801 end
802 function eif4.validate(self, value)
803         if usev6:formvalue(section) == "1"
804          or src4:formvalue(section) == "network"
805          or src4:formvalue(section) == "interface" then
806                 return ""       -- ignore IPv6, network, interface
807         else
808                 return value
809         end
810 end
811 function eif4.write(self, section, value)
812         if usev6:formvalue(section) == "1"
813          or src4:formvalue(section) == "network"
814          or src4:formvalue(section) == "interface" then
815                 return true     -- ignore IPv6, network, interface
816         else
817                 self.map:del(section, self.option)               -- delete "ipv4_interface" helper
818                 return self.map:set(section, "interface", value) -- and write "interface"
819         end
820 end
821
822 -- IPv6 - interface (NEW) - default "wan6" -- ##################################
823 -- event network to monitor changes/hotplug (NEW)
824 -- only needs to be set if "ip_source"="web" or "script"
825 -- if "ip_source"="network" or "interface" we use their network
826 eif6 = ns:taboption("advanced", ListValue, "ipv6_interface",
827         translate("Event Network") .. " [IPv6]" )
828 eif6:depends("ipv6_source", "web")
829 eif6:depends("ipv6_source", "script")
830 eif6.default = "wan6"
831 WADM.cbi_add_networks(eif6)
832 if not has_ipv6 then
833         eif6.description = err_ipv6_other
834 else
835         eif6.description = translate("Network on which the ddns-updater scripts will be started")
836 end
837 function eif6.cfgvalue(self, section)
838         return DDNS.read_value(self, section, "interface")
839 end
840 function eif6.validate(self, value)
841         if usev6:formvalue(section) == "0"
842          or src4:formvalue(section) == "network"
843          or src4:formvalue(section) == "interface" then
844                 return ""       -- ignore IPv4, network, interface
845         elseif not has_ipv6 then
846                 return nil, err_tab_adv(self) .. err_ipv6_plain
847         else
848                 return value
849         end
850 end
851 function eif6.write(self, section, value)
852         if usev6:formvalue(section) == "0"
853          or src4:formvalue(section) == "network"
854          or src4:formvalue(section) == "interface" then
855                 return true     -- ignore IPv4, network, interface
856         else
857                 self.map:del(section, self.option)               -- delete "ipv6_interface" helper
858                 return self.map:set(section, "interface", value) -- and write "interface"
859         end
860 end
861
862 -- IPv4/IPv6 - bind_network -- #################################################
863 if has_ssl or ( ( m:get(section, "bind_network") or "" ) ~= "" ) then
864         bnet = ns:taboption("advanced", ListValue, "bind_network",
865                 translate("Bind Network") )
866         bnet:depends("ipv4_source", "web")
867         bnet:depends("ipv6_source", "web")
868         bnet.rmempty = true
869         bnet.default = ""
870         bnet:value("", translate("-- default --"))
871         WADM.cbi_add_networks(bnet)
872         function bnet.cfgvalue(self, section)
873                 local value = AbstractValue.cfgvalue(self, section)
874                 if not has_ssl and value ~= "" then
875                         self.description = bold_on .. font_red ..
876                                 translate("Binding to a specific network not supported") .. font_off .. "<br />" ..
877                                 translate("please set to 'default'") .. " !" .. bold_off
878                 else
879                         self.description = translate("OPTIONAL: Network to use for communication") ..
880                                 "<br />" .. translate("Casual users should not change this setting")
881                 end
882                 return value
883         end
884         function bnet.validate(self, value)
885                 if (value ~= "" and has_ssl ) or value == "" then return value end
886                 return nil, err_tab_adv(self) .. translate("Binding to a specific network not supported") .. " !"
887         end
888 end
889
890 -- IPv4 + IPv6 - force_ipversion (NEW) -- ######################################
891 -- optional to force wget/curl and host to use only selected IP version
892 -- command parameter "-4" or "-6"
893 if has_force or ( ( m:get(section, "force_ipversion") or "0" ) ~= "0" ) then
894         fipv = ns:taboption("advanced", Flag, "force_ipversion",
895                 translate("Force IP Version") )
896         fipv.orientation = "horizontal"
897         function fipv.cfgvalue(self, section)
898                 local value = AbstractValue.cfgvalue(self, section)
899                 if not has_force and value ~= "0" then
900                         self.description = bold_on .. font_red ..
901                                 translate("Force IP Version not supported") .. font_off .. "<br />" ..
902                                 translate("please disable") .. " !" .. bold_off
903                 else
904                         self.description = translate("OPTIONAL: Force the usage of pure IPv4/IPv6 only communication.")
905                 end
906                 return value
907         end
908         function fipv.validate(self, value)
909                 if (value == "1" and has_force) or value == "0" then return value end
910                 return nil, err_tab_adv(self) .. translate("Force IP Version not supported")
911         end
912         function fipv.parse(self, section)
913                 DDNS.flag_parse(self, section)
914         end
915         function fipv.write(self, section, value)
916                 if value == "1" then
917                         return self.map:set(section, self.option, value)
918                 else
919                         return self.map:del(section, self.option)
920                 end
921         end
922 end
923
924 -- IPv4 + IPv6 - dns_server (NEW) -- ###########################################
925 -- optional DNS Server to use resolving my IP if "ip_source"="web"
926 dns = ns:taboption("advanced", Value, "dns_server",
927         translate("DNS-Server"),
928         translate("OPTIONAL: Use non-default DNS-Server to detect 'Registered IP'.") .. "<br />" ..
929         translate("Format: IP or FQDN"))
930 dns.placeholder = "mydns.lan"
931 function dns.validate(self, value)
932         -- if .datatype is set, then it is checked before calling this function
933         if not value then
934                 return ""       -- ignore on empty
935         elseif not DTYP.host(value) then
936                 return nil, err_tab_adv(self) .. translate("use hostname, FQDN, IPv4- or IPv6-Address")
937         else
938                 local ipv6  = usev6:formvalue(section)
939                 local force = (fipv) and fipv:formvalue(section) or "0"
940                 local command = [[/usr/lib/ddns/dynamic_dns_lucihelper.sh verify_dns ]] ..
941                         value .. [[ ]] .. ipv6 .. [[ ]] .. force
942                 local ret = SYS.call(command)
943                 if     ret == 0 then return value       -- everything OK
944                 elseif ret == 2 then return nil, err_tab_adv(self) .. translate("nslookup can not resolve host")
945                 elseif ret == 3 then return nil, err_tab_adv(self) .. translate("nc (netcat) can not connect")
946                 elseif ret == 4 then return nil, err_tab_adv(self) .. translate("Forced IP Version don't matched")
947                 else                 return nil, err_tab_adv(self) .. translate("unspecific error")
948                 end
949         end
950 end
951
952 -- IPv4 + IPv6 - force_dnstcp (NEW) -- #########################################
953 if has_dnstcp or ( ( m:get(section, "force_dnstcp") or "0" ) ~= "0" ) then
954         tcp = ns:taboption("advanced", Flag, "force_dnstcp",
955                 translate("Force TCP on DNS") )
956         tcp.orientation = "horizontal"
957         function tcp.cfgvalue(self, section)
958                 local value = AbstractValue.cfgvalue(self, section)
959                 if not has_dnstcp and value ~= "0" then
960                         self.description = bold_on .. font_red ..
961                                 translate("DNS requests via TCP not supported") .. font_off .. "<br />" ..
962                                 translate("please disable") .. " !" .. bold_off
963                 else
964                         self.description = translate("OPTIONAL: Force the use of TCP instead of default UDP on DNS requests.")
965                 end
966                 return value
967         end
968         function tcp.validate(self, value)
969                 if (value == "1" and has_dnstcp ) or value == "0" then
970                         return value
971                 end
972                 return nil, err_tab_adv(self) .. translate("DNS requests via TCP not supported")
973         end
974         function tcp.parse(self, section)
975                 DDNS.flag_parse(self, section)
976         end
977 end
978
979 -- IPv4 + IPv6 - proxy (NEW) -- ################################################
980 -- optional Proxy to use for http/https requests  [user:password@]proxyhost[:port]
981 if has_proxy or ( ( m:get(section, "proxy") or "" ) ~= "" ) then
982         pxy = ns:taboption("advanced", Value, "proxy",
983                 translate("PROXY-Server") )
984         pxy.placeholder="user:password@myproxy.lan:8080"
985         function pxy.cfgvalue(self, section)
986                 local value = AbstractValue.cfgvalue(self, section)
987                 if not has_proxy and value ~= "" then
988                         self.description = bold_on .. font_red ..
989                                 translate("PROXY-Server not supported") .. font_off .. "<br />" ..
990                                 translate("please remove entry") .. "!" .. bold_off
991                 else
992                         self.description = translate("OPTIONAL: Proxy-Server for detection and updates.") .. "<br />" ..
993                                 translate("Format") .. ": " .. bold_on .. "[user:password@]proxyhost:port" .. bold_off .. "<br />" ..
994                                 translate("IPv6 address must be given in square brackets") .. ": " ..
995                                 bold_on .. " [2001:db8::1]:8080" .. bold_off
996                 end
997                 return value
998         end
999         function pxy.validate(self, value)
1000                 -- if .datatype is set, then it is checked before calling this function
1001                 if not value then
1002                         return ""       -- ignore on empty
1003                 elseif has_proxy then
1004                         local ipv6  = usev6:formvalue(section) or "0"
1005                         local force = (fipv) and fipv:formvalue(section) or "0"
1006                         local command = [[/usr/lib/ddns/dynamic_dns_lucihelper.sh verify_proxy ]] ..
1007                                 value .. [[ ]] .. ipv6 .. [[ ]] .. force
1008                         local ret = SYS.call(command)
1009                         if     ret == 0 then return value
1010                         elseif ret == 2 then return nil, err_tab_adv(self) .. translate("nslookup can not resolve host")
1011                         elseif ret == 3 then return nil, err_tab_adv(self) .. translate("nc (netcat) can not connect")
1012                         elseif ret == 4 then return nil, err_tab_adv(self) .. translate("Forced IP Version don't matched")
1013                         elseif ret == 5 then return nil, err_tab_adv(self) .. translate("proxy port missing")
1014                         else                 return nil, err_tab_adv(self) .. translate("unspecific error")
1015                         end
1016                 else
1017                         return nil, err_tab_adv(self) .. translate("PROXY-Server not supported")
1018                 end
1019         end
1020 end
1021
1022 -- use_syslog -- ###############################################################
1023 slog = ns:taboption("advanced", ListValue, "use_syslog",
1024         translate("Log to syslog"),
1025         translate("Writes log messages to syslog. Critical Errors will always be written to syslog.") )
1026 slog.default = "2"
1027 slog:value("0", translate("No logging"))
1028 slog:value("1", translate("Info"))
1029 slog:value("2", translate("Notice"))
1030 slog:value("3", translate("Warning"))
1031 slog:value("4", translate("Error"))
1032
1033 -- use_logfile (NEW) -- ########################################################
1034 logf = ns:taboption("advanced", Flag, "use_logfile",
1035         translate("Log to file"),
1036         translate("Writes detailed messages to log file. File will be truncated automatically.") .. "<br />" ..
1037         translate("File") .. [[: "]] .. log_dir .. [[/]] .. section .. [[.log"]] )
1038 logf.orientation = "horizontal"
1039 logf.rmempty = false    -- we want to save in /etc/config/ddns file on "0" because
1040 logf.default = "1"      -- if not defined write to log by default
1041 function logf.parse(self, section)
1042         DDNS.flag_parse(self, section)
1043 end
1044
1045 -- TAB: Timer  #####################################################################################
1046 -- check_interval -- ###########################################################
1047 ci = ns:taboption("timer", Value, "check_interval",
1048         translate("Check Interval") )
1049 ci.template = "ddns/detail_value"
1050 ci.default  = 10
1051 ci.rmempty = false      -- validate ourselves for translatable error messages
1052 function ci.validate(self, value)
1053         if not DTYP.uinteger(value)
1054         or tonumber(value) < 1 then
1055                 return nil, err_tab_timer(self) .. translate("minimum value 5 minutes == 300 seconds")
1056         end
1057
1058         local secs = DDNS.calc_seconds(value, cu:formvalue(section))
1059         if secs >= 300 then
1060                 return value
1061         else
1062                 return nil, err_tab_timer(self) .. translate("minimum value 5 minutes == 300 seconds")
1063         end
1064 end
1065 function ci.write(self, section, value)
1066         -- simulate rmempty=true remove default
1067         local secs = DDNS.calc_seconds(value, cu:formvalue(section))
1068         if secs ~= 600 then     --default 10 minutes
1069                 return self.map:set(section, self.option, value)
1070         else
1071                 self.map:del(section, "check_unit")
1072                 return self.map:del(section, self.option)
1073         end
1074 end
1075
1076 -- check_unit -- ###############################################################
1077 cu = ns:taboption("timer", ListValue, "check_unit", "not displayed, but needed otherwise error",
1078         translate("Interval to check for changed IP" .. "<br />" ..
1079                 "Values below 5 minutes == 300 seconds are not supported") )
1080 cu.template = "ddns/detail_lvalue"
1081 cu.default  = "minutes"
1082 cu.rmempty  = false     -- want to control write process
1083 cu:value("seconds", translate("seconds"))
1084 cu:value("minutes", translate("minutes"))
1085 cu:value("hours", translate("hours"))
1086 --cu:value("days", translate("days"))
1087 function cu.write(self, section, value)
1088         -- simulate rmempty=true remove default
1089         local secs = DDNS.calc_seconds(ci:formvalue(section), value)
1090         if secs ~= 600 then     --default 10 minutes
1091                 return self.map:set(section, self.option, value)
1092         else
1093                 return true
1094         end
1095 end
1096
1097 -- force_interval (modified) -- ################################################
1098 fi = ns:taboption("timer", Value, "force_interval",
1099         translate("Force Interval") )
1100 fi.template = "ddns/detail_value"
1101 fi.default  = 72        -- see dynamic_dns_updater.sh script
1102 fi.rmempty = false      -- validate ourselves for translatable error messages
1103 function fi.validate(self, value)
1104         if not DTYP.uinteger(value)
1105         or tonumber(value) < 0 then
1106                 return nil, err_tab_timer(self) .. translate("minimum value '0'")
1107         end
1108
1109         local force_s = DDNS.calc_seconds(value, fu:formvalue(section))
1110         if force_s == 0 then
1111                 return value
1112         end
1113
1114         local ci_value = ci:formvalue(section)
1115         if not DTYP.uinteger(ci_value) then
1116                 return ""       -- ignore because error in check_interval above
1117         end
1118
1119         local check_s = DDNS.calc_seconds(ci_value, cu:formvalue(section))
1120         if force_s >= check_s then
1121                 return value
1122         end
1123
1124         return nil, err_tab_timer(self) .. translate("must be greater or equal 'Check Interval'")
1125 end
1126 function fi.write(self, section, value)
1127         -- simulate rmempty=true remove default
1128         local secs = DDNS.calc_seconds(value, fu:formvalue(section))
1129         if secs ~= 259200 then  --default 72 hours == 3 days
1130                 return self.map:set(section, self.option, value)
1131         else
1132                 self.map:del(section, "force_unit")
1133                 return self.map:del(section, self.option)
1134         end
1135 end
1136
1137 -- force_unit -- ###############################################################
1138 fu = ns:taboption("timer", ListValue, "force_unit", "not displayed, but needed otherwise error",
1139         translate("Interval to force updates send to DDNS Provider" .. "<br />" ..
1140                 "Setting this parameter to 0 will force the script to only run once" .. "<br />" ..
1141                 "Values lower 'Check Interval' except '0' are not supported") )
1142 fu.template = "ddns/detail_lvalue"
1143 fu.default  = "hours"
1144 fu.rmempty  = false     -- want to control write process
1145 --fu:value("seconds", translate("seconds"))
1146 fu:value("minutes", translate("minutes"))
1147 fu:value("hours", translate("hours"))
1148 fu:value("days", translate("days"))
1149 function fu.write(self, section, value)
1150         -- simulate rmempty=true remove default
1151         local secs = DDNS.calc_seconds(fi:formvalue(section), value)
1152         if secs ~= 259200 and secs ~= 0 then    --default 72 hours == 3 days
1153                 return self.map:set(section, self.option, value)
1154         else
1155                 return true
1156         end
1157 end
1158
1159 -- retry_count (NEW) -- ########################################################
1160 rc = ns:taboption("timer", Value, "retry_count",
1161         translate("Error Retry Counter"),
1162         translate("On Error the script will stop execution after given number of retrys") )
1163 rc.default = 5
1164 rc.rmempty = false      -- validate ourselves for translatable error messages
1165 function rc.validate(self, value)
1166         if not DTYP.uinteger(value) then
1167                 return nil, err_tab_timer(self) .. translate("minimum value '0'")
1168         else
1169                 return value
1170         end
1171 end
1172 function rc.write(self, section, value)
1173         -- simulate rmempty=true remove default
1174         if tonumber(value) ~= self.default then
1175                 return self.map:set(section, self.option, value)
1176         else
1177                 return self.map:del(section, self.option)
1178         end
1179 end
1180
1181 -- retry_interval -- ###########################################################
1182 ri = ns:taboption("timer", Value, "retry_interval",
1183         translate("Error Retry Interval") )
1184 ri.template = "ddns/detail_value"
1185 ri.default  = 60
1186 ri.rmempty  = false     -- validate ourselves for translatable error messages
1187 function ri.validate(self, value)
1188         if not DTYP.uinteger(value)
1189         or tonumber(value) < 1 then
1190                 return nil, err_tab_timer(self) .. translate("minimum value '1'")
1191         else
1192                 return value
1193         end
1194 end
1195 function ri.write(self, section, value)
1196         -- simulate rmempty=true remove default
1197         local secs = DDNS.calc_seconds(value, ru:formvalue(section))
1198         if secs ~= 60 then      --default 60seconds
1199                 return self.map:set(section, self.option, value)
1200         else
1201                 self.map:del(section, "retry_unit")
1202                 return self.map:del(section, self.option)
1203         end
1204 end
1205
1206 -- retry_unit -- ###############################################################
1207 ru = ns:taboption("timer", ListValue, "retry_unit", "not displayed, but needed otherwise error",
1208         translate("On Error the script will retry the failed action after given time") )
1209 ru.template = "ddns/detail_lvalue"
1210 ru.default  = "seconds"
1211 ru.rmempty  = false     -- want to control write process
1212 ru:value("seconds", translate("seconds"))
1213 ru:value("minutes", translate("minutes"))
1214 --ru:value("hours", translate("hours"))
1215 --ru:value("days", translate("days"))
1216 function ru.write(self, section, value)
1217         -- simulate rmempty=true remove default
1218         local secs = DDNS.calc_seconds(ri:formvalue(section), value)
1219         if secs ~= 60 then      --default 60seconds
1220                 return self.map:set(section, self.option, value)
1221         else
1222                 return true -- will be deleted by retry_interval
1223         end
1224 end
1225
1226 -- TAB: LogView  (NEW) #############################################################################
1227 lv = ns:taboption("logview", DummyValue, "_logview")
1228 lv.template = "ddns/detail_logview"
1229 lv.inputtitle = translate("Read / Reread log file")
1230 lv.rows = 50
1231 function lv.cfgvalue(self, section)
1232         local lfile=log_dir .. "/" .. section .. ".log"
1233         if NXFS.access(lfile) then
1234                 return lfile .. "\n" .. translate("Please press [Read] button")
1235         end
1236         return lfile .. "\n" .. translate("File not found or empty")
1237 end
1238
1239 return m