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