Browse Source

Re-added luci (again)

risci_atom 9 years ago
parent
commit
2006b46d8b
100 changed files with 9872 additions and 0 deletions
  1. 43 0
      luci/CONTRIBUTING.md
  2. 27 0
      luci/INSTALL
  3. 201 0
      luci/LICENSE
  4. 99 0
      luci/Makefile
  5. 9 0
      luci/NOTICE
  6. 21 0
      luci/README.md
  7. 26 0
      luci/THANKYOU
  8. 2 0
      luci/applications/freifunk-community/Makefile
  9. 4 0
      luci/applications/luci-ahcp/Makefile
  10. 60 0
      luci/applications/luci-ahcp/luasrc/controller/ahcp.lua
  11. 121 0
      luci/applications/luci-ahcp/luasrc/model/cbi/ahcp.lua
  12. 1 0
      luci/applications/luci-ahcp/luasrc/view/admin_status/index/ahcp.htm
  13. 53 0
      luci/applications/luci-ahcp/luasrc/view/ahcp_status.htm
  14. 11 0
      luci/applications/luci-ahcp/root/etc/uci-defaults/luci-ahcp
  15. 4 0
      luci/applications/luci-asterisk/Makefile
  16. 6 0
      luci/applications/luci-asterisk/ipkg/postinst
  17. 759 0
      luci/applications/luci-asterisk/luasrc/asterisk.lua
  18. 254 0
      luci/applications/luci-asterisk/luasrc/asterisk/cc_idd.lua
  19. 205 0
      luci/applications/luci-asterisk/luasrc/controller/asterisk.lua
  20. 102 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-dialplans.lua
  21. 60 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-iax-connections.lua
  22. 33 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-meetme.lua
  23. 391 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-app.lua
  24. 58 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-cdr.lua
  25. 56 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-chan.lua
  26. 64 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-codec.lua
  27. 100 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-format.lua
  28. 40 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-func.lua
  29. 64 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-pbx.lua
  30. 111 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-res-feature.lua
  31. 88 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-res.lua
  32. 109 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-sip-connections.lua
  33. 52 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-voice.lua
  34. 162 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk.lua
  35. 137 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/dialplan_out.lua
  36. 115 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/dialplans.lua
  37. 135 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/dialzones.lua
  38. 49 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/meetme.lua
  39. 28 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/meetme_settings.lua
  40. 157 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/phone_sip.lua
  41. 116 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/phones.lua
  42. 98 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/trunk_sip.lua
  43. 106 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/trunks.lua
  44. 59 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/voicemail.lua
  45. 62 0
      luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/voicemail_settings.lua
  46. 22 0
      luci/applications/luci-asterisk/luasrc/view/asterisk/cbi/cell.htm
  47. 254 0
      luci/applications/luci-asterisk/luasrc/view/asterisk/dialplans.htm
  48. 174 0
      luci/applications/luci-asterisk/luasrc/view/asterisk/dialzones.htm
  49. 231 0
      luci/applications/luci-asterisk/root/etc/config/asterisk
  50. 11 0
      luci/applications/luci-asterisk/root/etc/uci-defaults/luci-asterisk
  51. 4 0
      luci/applications/luci-commands/Makefile
  52. 237 0
      luci/applications/luci-commands/luasrc/controller/commands.lua
  53. 37 0
      luci/applications/luci-commands/luasrc/model/cbi/commands.lua
  54. 176 0
      luci/applications/luci-commands/luasrc/view/commands.htm
  55. 4 0
      luci/applications/luci-coovachilli/Makefile
  56. 26 0
      luci/applications/luci-coovachilli/luasrc/controller/coovachilli.lua
  57. 31 0
      luci/applications/luci-coovachilli/luasrc/model/cbi/coovachilli.lua
  58. 76 0
      luci/applications/luci-coovachilli/luasrc/model/cbi/coovachilli_auth.lua
  59. 67 0
      luci/applications/luci-coovachilli/luasrc/model/cbi/coovachilli_network.lua
  60. 67 0
      luci/applications/luci-coovachilli/luasrc/model/cbi/coovachilli_radius.lua
  61. 243 0
      luci/applications/luci-coovachilli/root/etc/config/coovachilli
  62. 51 0
      luci/applications/luci-ddns/CHANGELOG
  63. 4 0
      luci/applications/luci-ddns/Makefile
  64. 5 0
      luci/applications/luci-ddns/ipkg/postinst
  65. 5 0
      luci/applications/luci-ddns/ipkg/postinst-pkg
  66. 261 0
      luci/applications/luci-ddns/luasrc/controller/ddns.lua
  67. 1224 0
      luci/applications/luci-ddns/luasrc/model/cbi/ddns/detail.lua
  68. 147 0
      luci/applications/luci-ddns/luasrc/model/cbi/ddns/hints.lua
  69. 256 0
      luci/applications/luci-ddns/luasrc/model/cbi/ddns/overview.lua
  70. 295 0
      luci/applications/luci-ddns/luasrc/tools/ddns.lua
  71. 1 0
      luci/applications/luci-ddns/luasrc/view/admin_status/index/ddns.htm
  72. 56 0
      luci/applications/luci-ddns/luasrc/view/ddns/detail_logview.htm
  73. 22 0
      luci/applications/luci-ddns/luasrc/view/ddns/detail_lvalue.htm
  74. 9 0
      luci/applications/luci-ddns/luasrc/view/ddns/detail_value.htm
  75. 10 0
      luci/applications/luci-ddns/luasrc/view/ddns/overview_doubleline.htm
  76. 15 0
      luci/applications/luci-ddns/luasrc/view/ddns/overview_enabled.htm
  77. 17 0
      luci/applications/luci-ddns/luasrc/view/ddns/overview_startstop.htm
  78. 208 0
      luci/applications/luci-ddns/luasrc/view/ddns/overview_status.htm
  79. 158 0
      luci/applications/luci-ddns/luasrc/view/ddns/system_status.htm
  80. 19 0
      luci/applications/luci-ddns/root/etc/uci-defaults/luci-ddns
  81. 4 0
      luci/applications/luci-diag-core/Makefile
  82. 26 0
      luci/applications/luci-diag-core/luasrc/controller/luci_diag.lua
  83. 18 0
      luci/applications/luci-diag-core/luasrc/view/diag/index.htm
  84. 18 0
      luci/applications/luci-diag-core/luasrc/view/diag/network_config_index.htm
  85. 4 0
      luci/applications/luci-diag-devinfo/Makefile
  86. 195 0
      luci/applications/luci-diag-devinfo/luasrc/controller/luci_diag/devinfo_common.lua
  87. 56 0
      luci/applications/luci-diag-devinfo/luasrc/controller/luci_diag/luci_diag_devinfo.lua
  88. 101 0
      luci/applications/luci-diag-devinfo/luasrc/controller/luci_diag/netdiscover_common.lua
  89. 112 0
      luci/applications/luci-diag-devinfo/luasrc/controller/luci_diag/smap_common.lua
  90. 36 0
      luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/mactodevinfo.lua
  91. 33 0
      luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/netdiscover_devinfo.lua
  92. 30 0
      luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/netdiscover_devinfo_config.lua
  93. 30 0
      luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/netdiscover_devinfo_config_mini.lua
  94. 33 0
      luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/netdiscover_devinfo_mini.lua
  95. 33 0
      luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/smap_devinfo.lua
  96. 35 0
      luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/smap_devinfo_config.lua
  97. 36 0
      luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/smap_devinfo_config_mini.lua
  98. 33 0
      luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/smap_devinfo_mini.lua
  99. 137 0
      luci/applications/luci-diag-devinfo/luasrc/view/diag/smapsection.htm
  100. 21 0
      luci/applications/luci-diag-devinfo/luasrc/view/diag/smapvalue.htm

+ 43 - 0
luci/CONTRIBUTING.md

@@ -0,0 +1,43 @@
+# Contributing Guidelines
+
+Patches and pull-requests:
+
+If you want to contribute a change to LuCI, please either send a patch using git send-email
+or open a pull request against the librecmc/luci repository.
+
+Regardless of whether you send a patch or open a pull request, please try to follow these rules:
+
+* Have a useful subject prefixed with the component name  
+    (E.g.: "modules/admin-full: fix wifi channel selection on multiple STA networks")
+* Shortly explain the changes made and - if applicable - the reasoning behind them
+* Include Signed-off-by in the comment  
+    (See <https://dev.librecmc.org/wiki/SubmittingPatches#a10.Signyourwork>)
+	
+In case you like to send patches by mail, please use the [LuCI mailinglist](https://lists.subsignal.org/mailman/listinfo/luci)
+or the [libreCMC Development List](https://lists.librecmc.org/cgi-bin/mailman/listinfo/librecmc-devel).
+
+If you send via the libreCMC list, include a "[luci]" tag in your subject line.
+For general information on patch submission, follow the [libreCMC patch submission guideline](https://dev.librecmc.org/wiki/SubmittingPatches).
+
+
+If you have commit access:
+
+* Do NOT use git push --force.
+* Use Pull Requests if you are unsure and to suggest changes to other developers.
+
+Gaining commit access:
+
+* Commit access will be granted to responsible contributors who have made
+  useful pull requests and / or feedback or patches to this repository or
+  libreCMC in general. Please include your request for commit access in your
+  next pull request or ticket.
+
+Release Branches:
+
+* Branches named "luci-X.Y" (e.g. luci-0.12) are release branches.
+* These branches are built with the respective libreCMC release and are created
+  during the release stabilisation phase.
+* Please ONLY cherry-pick or commit security and bug-fixes to these branches.
+* Do NOT add new packages and do NOT do major upgrades of packages here.
+* If you are unsure if your change is suitable, please use a pull request.
+

+ 27 - 0
luci/INSTALL

@@ -0,0 +1,27 @@
+LuCI Installation Instructions
+
+TOC:
+1. Kamikaze Feed
+2. Kamikaze Packages
+
+
+1. Kamikaze Feed
+	1. Change to your libreCMC buildroot
+
+	2. Add the following line to your libreCMC feeds.conf:
+		src-svn luci http://svn.luci.subsignal.org/luci/trunk/contrib/package
+		
+	3. Run ./scripts/feeds update
+	
+	4. Run ./scripts/feeds install -a -p luci
+	
+	5. Type make menuconfig and you will find luci in the menu "Administration"
+	
+		
+2. Kamikaze Packages
+	1.	cd to the "package" directory of your kamikaze buildroot
+	
+	3.	Type: ln -s /path/to/luci/contrib/package/* ./
+	
+	4.	cd to your kamikaze build root and type: make menuconfig
+		You will find luci in the menu "Administration"

+ 201 - 0
luci/LICENSE

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 99 - 0
luci/Makefile

@@ -0,0 +1,99 @@
+include build/config.mk
+
+MODULES = contrib/* applications/* libs/* modules/* themes/* i18n/*
+
+OS:=$(shell uname)
+MODULES:=$(foreach item,$(wildcard $(MODULES)),$(if $(realpath $(wildcard $(item)/Makefile)),$(item)))
+
+export OS
+
+.PHONY: all build gccbuild luabuild clean host gcchost luahost hostcopy hostclean
+
+all: build
+
+build: gccbuild luabuild
+
+gccbuild:
+	make -C modules/base CC="cc" CFLAGS="" LDFLAGS="" SDK="$(shell test -f .running-sdk && echo 1)" host-install
+	for i in $(MODULES); do \
+		make -C$$i SDK="$(shell test -f .running-sdk && echo 1)" compile || { \
+			echo "*** Compilation of $$i failed!"; \
+			exit 1; \
+		}; \
+	done
+
+luabuild: i18nbuild
+	for i in $(MODULES); do HOST=$(realpath host) \
+		SDK="$(shell test -f .running-sdk && echo 1)" make -C$$i luabuild; done
+
+i18nbuild:
+	mkdir -p host/lua-po
+	./build/i18n-po2lua.pl ./po host/lua-po
+
+clean:
+	rm -f .running-sdk
+	rm -rf docs
+	make -C modules/base host-clean
+	for i in $(MODULES); do make -C$$i clean; done
+
+
+host: build hostcopy
+
+gcchost: gccbuild hostcopy
+
+luahost: luabuild hostcopy
+
+hostcopy: 
+	mkdir -p host/tmp
+	mkdir -p host/var/state
+	for i in $(MODULES); do cp -pR $$i/dist/* host/ 2>/dev/null || true; done
+	for i in $(MODULES); do cp -pR $$i/hostfiles/* host/ 2>/dev/null || true; done
+	rm -f host/luci
+	ln -s .$(LUCI_MODULEDIR) host/luci
+	rm -rf /tmp/luci-* || true
+
+hostenv: sdk host ucidefaults
+
+sdk:
+	touch .running-sdk
+
+ucidefaults:
+	build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) "$(realpath host)/bin/uci-defaults --exclude luci-freifunk-*"
+
+runuhttpd: hostenv
+	cp $(realpath build)/luci.cgi $(realpath host)/www/cgi-bin/luci
+	build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) "$(realpath host)/usr/sbin/uhttpd -p 8080 -h $(realpath host)/www -f"
+
+runlua: hostenv
+	build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) "lua -i build/setup.lua"
+
+runshell: hostenv
+	build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) $$SHELL
+
+hostclean: clean
+	rm -rf host
+
+apidocs: hostenv
+	build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) "build/makedocs.sh host/luci/ docs"
+
+nixiodocs: hostenv
+	build/hostenv.sh $(realpath host) $(LUA_MODULEDIR) $(LUA_LIBRARYDIR) "build/makedocs.sh libs/nixio/ nixiodocs"
+
+po: host
+	for L in $${LANGUAGE:-$$(find i18n/ -path 'i18n/*/luasrc/i18n/*' -name 'default.*.lua' | \
+	  sed -e 's!.*/default\.\(.*\)\.lua!\1!')}; do \
+	    build/i18n-lua2po.pl . $(realpath host)/po $$L; \
+	done
+
+run:
+	#	make run is deprecated				#
+	#	Please use:					#
+	#							#
+	#	To run LuCI WebUI using uhttpd			#
+	#	make runuhttpd					#
+	#							#
+	#	To start a shell in the LuCI environment	#
+	#	make runshell					#
+	#							#
+	#	To run Lua CLI in the LuCI environment		#
+	#	make runlua					#

+ 9 - 0
luci/NOTICE

@@ -0,0 +1,9 @@
+LuCI - Lua Configuration Interface
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+Licensed under the Apache License, Version 2.0.
+
+Contains code from:
+coxpcall	- Copyright 2005 - Kepler Project (www.keplerproject.org)
+ltn12/luasocket - Copyright 2004-2007 Diego Nehab
+axTLS		- Copyright 2008 Cameron Rich

+ 21 - 0
luci/README.md

@@ -0,0 +1,21 @@
+# libreCMC luci feed
+
+## Description
+
+This is the libreCMC "luci"-feed containing LuCI - libreCMC Configuration Interface.
+
+## Usage
+
+This feed is enabled by default. To install all its package definitions, run:
+```
+./scripts/feeds update luci
+./scripts/feeds install -a -p luci
+```
+
+## License
+
+See [LICENSE](LICENSE) file.
+ 
+## Package Guidelines
+
+See [CONTRIBUTING.md](CONTRIBUTING.md) file.

+ 26 - 0
luci/THANKYOU

@@ -0,0 +1,26 @@
+I'd like to thank the following people for contributing to this software:
+
+* Anton Popov
+	- for rewriting the librecmc.org theme
+
+* Florian Fainelli (libreCMC)
+	- for the french translation
+
+* Alina Friedrichsen
+	- for the reworked translation system, help on standards compliance and accessibility
+	
+* Yanira
+	- several applications and bugreports
+
+
+Also a big thank you goes to:
+	
+* Mono (Freifunk Halle)
+	- for donating a Linksys WRT54GL for development purposes
+	
+* tetzlav (Freifunk Leipzig)
+	- for donating several boards for testing and his feedback
+	
+* Mickey (Freifunk Hannover)
+	- for his feedback and fixes for the libreCMC builds
+	

+ 2 - 0
luci/applications/freifunk-community/Makefile

@@ -0,0 +1,2 @@
+include ../../build/config.mk
+include ../../build/module.mk

+ 4 - 0
luci/applications/luci-ahcp/Makefile

@@ -0,0 +1,4 @@
+PO = ahcp
+
+include ../../build/config.mk
+include ../../build/module.mk

+ 60 - 0
luci/applications/luci-ahcp/luasrc/controller/ahcp.lua

@@ -0,0 +1,60 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2011 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id: init.lua 6731 2011-01-14 19:44:03Z soma $
+]]--
+
+module("luci.controller.ahcp", package.seeall)
+
+function index()
+	if not nixio.fs.access("/etc/config/ahcpd") then
+		return
+	end
+
+	entry({"admin", "network", "ahcpd"}, cbi("ahcp"), _("AHCP Server"), 90)
+	entry({"admin", "network", "ahcpd", "status"}, call("ahcp_status"))
+end
+
+function ahcp_status()
+	local nfs = require "nixio.fs"
+	local uci = require "luci.model.uci".cursor()
+	local lsd = uci:get_first("ahcpd", "ahcpd", "lease_dir") or "/var/lib/leases"
+	local idf = uci:get_first("ahcpd", "ahcpd", "id_file")   or "/var/lib/ahcpd-unique-id"
+
+	local rv = {
+		uid    = "00:00:00:00:00:00:00:00",
+		leases = { }
+	}
+
+	idf = nfs.readfile(idf)
+	if idf and #idf == 8 then
+		rv.uid = "%02X:%02X:%02X:%02X:%02X:%02X:%02X:%02X" %{ idf:byte(1, 8) }
+	end
+
+	local itr = nfs.dir(lsd)
+	if itr then
+		local addr
+		for addr in itr do
+			if addr:match("^%d+%.%d+%.%d+%.%d+$") then
+				local s = nfs.stat(lsd .. "/" .. addr)
+				rv.leases[#rv.leases+1] = {
+					addr = addr,
+					age  = s and (os.time() - s.mtime) or 0
+				}
+			end
+		end
+	end
+
+	table.sort(rv.leases, function(a, b) return a.age < b.age end)
+
+	luci.http.prepare_content("application/json")
+	luci.http.write_json(rv)
+end

+ 121 - 0
luci/applications/luci-ahcp/luasrc/model/cbi/ahcp.lua

@@ -0,0 +1,121 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2011 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id: init.lua 5764 2010-03-08 19:05:34Z jow $
+]]--
+
+m = Map("ahcpd", translate("AHCP Server"), translate("AHCP is an autoconfiguration protocol " ..
+	"for IPv6 and dual-stack IPv6/IPv4 networks designed to be used in place of router " ..
+	"discovery or DHCP on networks where it is difficult or impossible to configure a " ..
+	"server within every link-layer broadcast domain, for example mobile ad-hoc networks."))
+
+
+m:section(SimpleSection).template = "ahcp_status"
+
+s = m:section(TypedSection, "ahcpd")
+s:tab("general", translate("General Setup"))
+s:tab("advanced", translate("Advanced Settings"))
+s.addremove = false
+s.anonymous = true
+
+
+mode = s:taboption("general", ListValue, "mode", translate("Operation mode"))
+mode:value("server", translate("Server"))
+mode:value("forwarder", translate("Forwarder"))
+
+net = s:taboption("general", Value, "interface", translate("Served interfaces"))
+net.template = "cbi/network_netlist"
+net.widget   = "checkbox"
+net.nocreate = true
+
+function net.cfgvalue(self, section)
+	return m.uci:get("ahcpd", section, "interface")
+end
+
+pfx = s:taboption("general", DynamicList, "prefix", translate("Announced prefixes"),
+	translate("Specifies the announced IPv4 and IPv6 network prefixes in CIDR notation"))
+pfx.optional  = true
+pfx.datatype  = "ipaddr"
+pfx:depends("mode", "server")
+
+nss = s:taboption("general", DynamicList, "name_server", translate("Announced DNS servers"),
+	translate("Specifies the announced IPv4 and IPv6 name servers"))
+nss.optional = true
+nss.datatype = "ipaddr"
+nss:depends("mode", "server")
+
+ntp = s:taboption("general", DynamicList, "ntp_server", translate("Announced NTP servers"),
+	translate("Specifies the announced IPv4 and IPv6 NTP servers"))
+ntp.optional = true
+ntp.datatype = "ipaddr"
+ntp:depends("mode", "server")
+
+mca = s:taboption("general", Value, "multicast_address", translate("Multicast address"))
+mca.optional    = true
+mca.placeholder = "ff02::cca6:c0f9:e182:5359"
+mca.datatype    = "ip6addr"
+
+port = s:taboption("general", Value, "port", translate("Port"))
+port.optional    = true
+port.placeholder = 5359
+port.datatype    = "port"
+
+fam = s:taboption("general", ListValue, "_family", translate("Protocol family"))
+fam:value("", translate("IPv4 and IPv6"))
+fam:value("ipv4", translate("IPv4 only"))
+fam:value("ipv6", translate("IPv6 only"))
+
+function fam.cfgvalue(self, section)
+	local v4 = m.uci:get_bool("ahcpd", section, "ipv4_only")
+	local v6 = m.uci:get_bool("ahcpd", section, "ipv6_only")
+	if v4 then
+		return "ipv4"
+	elseif v6 then
+		return "ipv6"
+	end
+	return ""
+end
+
+function fam.write(self, section, value)
+	if value == "ipv4" then
+		m.uci:set("ahcpd", section, "ipv4_only", "true")
+		m.uci:delete("ahcpd", section, "ipv6_only")
+	elseif value == "ipv6" then
+		m.uci:set("ahcpd", section, "ipv6_only", "true")
+		m.uci:delete("ahcpd", section, "ipv4_only")
+	end
+end
+
+function fam.remove(self, section)
+	m.uci:delete("ahcpd", section, "ipv4_only")
+	m.uci:delete("ahcpd", section, "ipv6_only")
+end
+
+ltime = s:taboption("general", Value, "lease_time", translate("Lease validity time"))
+ltime.optional    = true
+ltime.placeholder = 3666
+ltime.datatype    = "uinteger"
+
+
+ld = s:taboption("advanced", Value, "lease_dir", translate("Lease directory"))
+ld.datatype    = "directory"
+ld.placeholder = "/var/lib/leases"
+
+id = s:taboption("advanced", Value, "id_file", translate("Unique ID file"))
+--id.datatype    = "file"
+id.placeholder = "/var/lib/ahcpd-unique-id"
+
+log = s:taboption("advanced", Value, "log_file", translate("Log file"))
+--log.datatype    = "file"
+log.placeholder = "/var/log/ahcpd.log"
+
+
+return m

+ 1 - 0
luci/applications/luci-ahcp/luasrc/view/admin_status/index/ahcp.htm

@@ -0,0 +1 @@
+<%+ahcp_status%>

+ 53 - 0
luci/applications/luci-ahcp/luasrc/view/ahcp_status.htm

@@ -0,0 +1,53 @@
+<script type="text/javascript">//<![CDATA[
+	XHR.poll(5, '<%=luci.dispatcher.build_url("admin", "network", "ahcpd", "status")%>', null,
+		function(x, st)
+		{
+			var tb = document.getElementById('ahcpd_status_table');
+			var tx = document.getElementById('ahcpd_status_text');
+			if (st && tb && tx)
+			{
+				/* clear all rows */
+				while( tb.rows.length > 1 )
+					tb.deleteRow(1);
+
+				for( var i = 0; i < st.leases.length; i++ )
+				{
+					var tr = tb.insertRow(-1);
+						tr.className = 'cbi-section-table-row cbi-rowstyle-' + ((i % 2) + 1);
+
+					tr.insertCell(-1).innerHTML = st.leases[i].addr;
+					tr.insertCell(-1).innerHTML = String.format('%t', st.leases[i].age);
+				}
+
+				if( tb.rows.length == 1 )
+				{
+					var tr = tb.insertRow(-1);
+						tr.className = 'cbi-section-table-row';
+
+					var td = tr.insertCell(-1);
+						td.colSpan = 2;
+						td.innerHTML = '<em><br /><%:There are no active leases.%></em>';
+				}
+
+				if( st.uid == '00:00:00:00:00:00:00:00' )
+					tx.innerHTML = '<%:The AHCP Service is not running.%>';
+				else
+					tx.innerHTML = String.format('<%:The AHCP Service is running with ID %s.%>', st.uid);
+			}
+		}
+	);
+//]]></script>
+
+<fieldset class="cbi-section">
+	<legend><%:Active AHCP Leases%></legend>
+	<p id="ahcpd_status_text"></p>
+	<table class="cbi-section-table" id="ahcpd_status_table">
+		<tr class="cbi-section-table-titles">
+			<th class="cbi-section-table-cell"><%:Address%></th>
+			<th class="cbi-section-table-cell"><%:Age%></th>
+		</tr>
+		<tr class="cbi-section-table-row">
+			<td colspan="5"><em><br /><%:Collecting data...%></em></td>
+		</tr>
+	</table>
+</fieldset>

+ 11 - 0
luci/applications/luci-ahcp/root/etc/uci-defaults/luci-ahcp

@@ -0,0 +1,11 @@
+#!/bin/sh
+
+uci -q batch <<-EOF >/dev/null
+	delete ucitrack.@ahcpd[-1]
+	add ucitrack ahcpd
+	set ucitrack.@ahcpd[-1].init=ahcpd
+	commit ucitrack
+EOF
+
+rm -f /tmp/luci-indexcache
+exit 0

+ 4 - 0
luci/applications/luci-asterisk/Makefile

@@ -0,0 +1,4 @@
+PO = asterisk
+
+include ../../build/config.mk
+include ../../build/module.mk

+ 6 - 0
luci/applications/luci-asterisk/ipkg/postinst

@@ -0,0 +1,6 @@
+#!/bin/sh
+[ -n "${IPKG_INSTROOT}" ] || {
+	( . /etc/uci-defaults/luci-asterisk ) && rm -f /etc/uci-defaults/luci-asterisk
+	/etc/init.d/asterisk enabled || /etc/init.d/asterisk enable
+	exit 0
+}

+ 759 - 0
luci/applications/luci-asterisk/luasrc/asterisk.lua

@@ -0,0 +1,759 @@
+--[[
+LuCI - Lua Configuration Interface
+Asterisk PBX interface library
+
+Copyright 2009 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+]]--
+
+module("luci.asterisk", package.seeall)
+require("luci.asterisk.cc_idd")
+
+local _io  = require("io")
+local uci  = require("luci.model.uci").cursor()
+local sys  = require("luci.sys")
+local util = require("luci.util")
+
+AST_BIN   = "/usr/sbin/asterisk"
+AST_FLAGS = "-r -x"
+
+
+--- LuCI Asterisk - Resync uci context
+function uci_resync()
+	uci = luci.model.uci.cursor()
+end
+
+--- LuCI Asterisk io interface
+-- Handles low level io.
+-- @type	module
+io = luci.util.class()
+
+--- Execute command and return output
+-- @param command	String containing the command to execute
+-- @return			String containing the command output
+function io.exec(command)
+	local fh = _io.popen( "%s %s %q" %{ AST_BIN, AST_FLAGS, command }, "r" )
+	assert(fh, "Failed to invoke asterisk")
+
+	local buffer = fh:read("*a")
+	fh:close()
+	return buffer
+end
+
+--- Execute command and invoke given callback for each readed line
+-- @param command	String containing the command to execute
+-- @param callback	Function to call back for each line
+-- @return			Always true
+function io.execl(command, callback)
+	local ln
+	local fh = _io.popen( "%s %s %q" %{ AST_BIN, AST_FLAGS, command }, "r" )
+	assert(fh, "Failed to invoke asterisk")
+
+	repeat
+		ln = fh:read("*l")
+		callback(ln)
+	until not ln
+
+	fh:close()
+	return true
+end
+
+--- Execute command and return an iterator that returns one line per invokation
+-- @param command	String containing the command to execute
+-- @return			Iterator function
+function io.execi(command)
+	local fh = _io.popen( "%s %s %q" %{ AST_BIN, AST_FLAGS, command }, "r" )
+	assert(fh, "Failed to invoke asterisk")
+
+	return function()
+		local ln = fh:read("*l")
+		if not ln then fh:close() end
+		return ln
+	end
+end
+
+
+--- LuCI Asterisk - core status
+core = luci.util.class()
+
+--- Retrive version string.
+-- @return	String containing the reported asterisk version
+function core.version(self)
+	local version = io.exec("core show version")
+	return version:gsub(" *\n", "")
+end
+
+
+--- LuCI Asterisk - SIP information.
+-- @type module
+sip = luci.util.class()
+
+--- Get a list of known SIP peers
+-- @return		Table containing each SIP peer
+function sip.peers(self)
+	local head  = false
+	local peers = { }
+
+	for line in io.execi("sip show peers") do
+		if not head then
+			head = true
+		elseif not line:match(" sip peers ") then
+			local online, delay, id, uid
+			local name, host, dyn, nat, acl, port, status =
+				line:match("(.-) +(.-) +([D ])   ([N ])   (.)  (%d+) +(.+)")
+
+			if host == '(Unspecified)' then host = nil end
+			if port == '0' then port = nil else port = tonumber(port) end
+
+			dyn = ( dyn == 'D' and true or false )
+			nat = ( nat == 'N' and true or false )
+			acl = ( acl ~= ' ' and true or false )
+
+			online, delay = status:match("(OK) %((%d+) ms%)")
+
+			if online == 'OK' then
+				online = true
+				delay  = tonumber(delay)
+			elseif status ~= 'Unmonitored' then
+				online = false
+				delay  = 0
+			else
+				online = nil
+				delay  = 0
+			end
+
+			id, uid = name:match("(.+)/(.+)")
+
+			if not ( id and uid ) then
+				id  = name .. "..."
+				uid = nil
+			end
+
+			peers[#peers+1] = {
+				online  = online,
+				delay   = delay,
+				name    = id,
+				user    = uid,
+				dynamic = dyn,
+				nat     = nat,
+				acl     = acl,
+				host    = host,
+				port    = port
+			}
+		end
+	end
+
+	return peers
+end
+
+--- Get informations of given SIP peer
+-- @param peer	String containing the name of the SIP peer
+function sip.peer(peer)
+	local info = { }
+	local keys = { }
+
+	for line in io.execi("sip show peer " .. peer) do
+		if #line > 0 then
+			local key, val = line:match("(.-) *: +(.*)")
+			if key and val then
+
+				key = key:gsub("^ +",""):gsub(" +$", "")
+				val = val:gsub("^ +",""):gsub(" +$", "")
+
+				if key == "* Name" then
+					key = "Name"
+				elseif key == "Addr->IP" then
+					info.address, info.port = val:match("(.+) Port (.+)")
+					info.port = tonumber(info.port)
+				elseif key == "Status" then
+					info.online, info.delay = val:match("(OK) %((%d+) ms%)")
+					if info.online == 'OK' then
+						info.online = true
+						info.delay  = tonumber(info.delay)
+					elseif status ~= 'Unmonitored' then
+						info.online = false
+						info.delay  = 0
+					else
+						info.online = nil
+						info.delay  = 0
+					end
+				end
+
+				if val == 'Yes' or val == 'yes' or val == '<Set>' then
+					val = true
+				elseif val == 'No' or val == 'no' then
+					val = false
+				elseif val == '<Not set>' or val == '(none)' then
+					val = nil
+				end
+
+				keys[#keys+1] = key
+				info[key] = val
+			end
+		end
+	end
+
+	return info, keys
+end
+
+
+--- LuCI Asterisk - Internal helpers
+-- @type module
+tools = luci.util.class()
+
+--- Convert given value to a list of tokens. Split by white space.
+-- @param val	String or table value
+-- @return		Table containing tokens
+function tools.parse_list(v)
+	local tokens = { }
+
+	v = type(v) == "table" and v or { v }
+	for _, v in ipairs(v) do
+		if type(v) == "string" then
+			for v in v:gmatch("(%S+)") do
+				tokens[#tokens+1] = v
+			end
+		end
+	end
+
+	return tokens
+end
+
+--- Convert given list to a collection of hyperlinks
+-- @param list	Table of tokens
+-- @param url	String pattern or callback function to construct urls (optional)
+-- @param sep	String containing the seperator (optional, default is ", ")
+-- @return		String containing the html fragment
+function tools.hyperlinks(list, url, sep)
+	local html
+
+	local function mkurl(p, t)
+		if type(p) == "string" then
+			return p:format(t)
+		elseif type(p) == "function" then
+			return p(t)
+		else
+			return '#'
+		end
+	end
+
+	list = list or { }
+	url  = url  or "%s"
+	sep  = sep  or ", "
+
+	for _, token in ipairs(list) do
+		html = ( html and html .. sep or '' ) ..
+			'<a href="%s">%s</a>' %{ mkurl(url, token), token }
+	end
+
+	return html or ''
+end
+
+
+--- LuCI Asterisk - International Direct Dialing Prefixes
+-- @type module
+idd = luci.util.class()
+
+--- Lookup the country name for the given IDD code.
+-- @param country	String containing IDD code
+-- @return			String containing the country name
+function idd.country(c)
+	for _, v in ipairs(cc_idd.CC_IDD) do
+		if type(v[3]) == "table" then
+			for _, v2 in ipairs(v[3]) do
+				if v2 == tostring(c) then
+					return v[1]
+				end
+			end
+		elseif v[3] == tostring(c) then
+			return v[1]
+		end
+	end
+end
+
+--- Lookup the country code for the given IDD code.
+-- @param country	String containing IDD code
+-- @return			Table containing the country code(s)
+function idd.cc(c)
+	for _, v in ipairs(cc_idd.CC_IDD) do
+		if type(v[3]) == "table" then
+			for _, v2 in ipairs(v[3]) do
+				if v2 == tostring(c) then
+					return type(v[2]) == "table"
+						and v[2] or { v[2] }
+				end
+			end
+		elseif v[3] == tostring(c) then
+			return type(v[2]) == "table"
+				and v[2] or { v[2] }
+		end
+	end
+end
+
+--- Lookup the IDD code(s) for the given country.
+-- @param idd		String containing the country name
+-- @return			Table containing the IDD code(s)
+function idd.idd(c)
+	for _, v in ipairs(cc_idd.CC_IDD) do
+		if v[1]:lower():match(c:lower()) then
+			return type(v[3]) == "table"
+				and v[3] or { v[3] }
+		end
+	end
+end
+
+--- Populate given CBI field with IDD codes.
+-- @param field		CBI option object
+-- @return			(nothing)
+function idd.cbifill(o)
+	for i, v in ipairs(cc_idd.CC_IDD) do
+		o:value("_%i" % i, util.pcdata(v[1]))
+	end
+
+	o.formvalue = function(...)
+		local val = luci.cbi.Value.formvalue(...)
+		if val:sub(1,1) == "_" then
+			val = tonumber((val:gsub("^_", "")))
+			if val then
+				return type(cc_idd.CC_IDD[val][3]) == "table"
+					and cc_idd.CC_IDD[val][3] or { cc_idd.CC_IDD[val][3] }
+			end
+		end
+		return val
+	end
+
+	o.cfgvalue = function(...)
+		local val = luci.cbi.Value.cfgvalue(...)
+		if val then
+			val = tools.parse_list(val)
+			for i, v in ipairs(cc_idd.CC_IDD) do
+				if type(v[3]) == "table" then
+					if v[3][1] == val[1] then
+						return "_%i" % i
+					end
+				else
+					if v[3] == val[1] then
+						return "_%i" % i
+					end
+				end
+			end
+		end
+		return val
+	end
+end
+
+
+--- LuCI Asterisk - Country Code Prefixes
+-- @type module
+cc = luci.util.class()
+
+--- Lookup the country name for the given CC code.
+-- @param country	String containing CC code
+-- @return			String containing the country name
+function cc.country(c)
+	for _, v in ipairs(cc_idd.CC_IDD) do
+		if type(v[2]) == "table" then
+			for _, v2 in ipairs(v[2]) do
+				if v2 == tostring(c) then
+					return v[1]
+				end
+			end
+		elseif v[2] == tostring(c) then
+			return v[1]
+		end
+	end
+end
+
+--- Lookup the international dialing code for the given CC code.
+-- @param cc		String containing CC code
+-- @return			String containing IDD code
+function cc.idd(c)
+	for _, v in ipairs(cc_idd.CC_IDD) do
+		if type(v[2]) == "table" then
+			for _, v2 in ipairs(v[2]) do
+				if v2 == tostring(c) then
+					return type(v[3]) == "table"
+						and v[3] or { v[3] }
+				end
+			end
+		elseif v[2] == tostring(c) then
+			return type(v[3]) == "table"
+				and v[3] or { v[3] }
+		end
+	end
+end
+
+--- Lookup the CC code(s) for the given country.
+-- @param country	String containing the country name
+-- @return			Table containing the CC code(s)
+function cc.cc(c)
+	for _, v in ipairs(cc_idd.CC_IDD) do
+		if v[1]:lower():match(c:lower()) then
+			return type(v[2]) == "table"
+				and v[2] or { v[2] }
+		end
+	end
+end
+
+--- Populate given CBI field with CC codes.
+-- @param field		CBI option object
+-- @return			(nothing)
+function cc.cbifill(o)
+	for i, v in ipairs(cc_idd.CC_IDD) do
+		o:value("_%i" % i, util.pcdata(v[1]))
+	end
+
+	o.formvalue = function(...)
+		local val = luci.cbi.Value.formvalue(...)
+		if val:sub(1,1) == "_" then
+			val = tonumber((val:gsub("^_", "")))
+			if val then
+				return type(cc_idd.CC_IDD[val][2]) == "table"
+					and cc_idd.CC_IDD[val][2] or { cc_idd.CC_IDD[val][2] }
+			end
+		end
+		return val
+	end
+
+	o.cfgvalue = function(...)
+		local val = luci.cbi.Value.cfgvalue(...)
+		if val then
+			val = tools.parse_list(val)
+			for i, v in ipairs(cc_idd.CC_IDD) do
+				if type(v[2]) == "table" then
+					if v[2][1] == val[1] then
+						return "_%i" % i
+					end
+				else
+					if v[2] == val[1] then
+						return "_%i" % i
+					end
+				end
+			end
+		end
+		return val
+	end
+end
+
+
+--- LuCI Asterisk - Dialzone
+-- @type	module
+dialzone = luci.util.class()
+
+--- Parse a dialzone section
+-- @param zone	Table containing the zone info
+-- @return		Table with parsed information
+function dialzone.parse(z)
+	if z['.name'] then
+		return {
+			trunks		= tools.parse_list(z.uses),
+			name    	= z['.name'],
+			description	= z.description or z['.name'],
+			addprefix	= z.addprefix,
+			matches		= tools.parse_list(z.match),
+			intlmatches	= tools.parse_list(z.international),
+			countrycode	= z.countrycode,
+			localzone	= z.localzone,
+			localprefix	= z.localprefix
+		}
+	end
+end
+
+--- Get a list of known dial zones
+-- @return		Associative table of zones and table of zone names
+function dialzone.zones()
+	local zones  = { }
+	local znames = { }
+	uci:foreach("asterisk", "dialzone",
+		function(z)
+			zones[z['.name']] = dialzone.parse(z)
+			znames[#znames+1] = z['.name']
+		end)
+	return zones, znames
+end
+
+--- Get a specific dial zone
+-- @param name	Name of the dial zone
+-- @return		Table containing zone information
+function dialzone.zone(n)
+	local zone
+	uci:foreach("asterisk", "dialzone",
+		function(z)
+			if z['.name'] == n then
+				zone = dialzone.parse(z)
+			end
+		end)
+	return zone
+end
+
+--- Find uci section hash for given zone number
+-- @param idx	Zone number
+-- @return		String containing the uci hash pointing to the section
+function dialzone.ucisection(i)
+	local hash
+	local index = 1
+	i = tonumber(i)
+	uci:foreach("asterisk", "dialzone",
+		function(z)
+			if not hash and index == i then
+				hash = z['.name']
+			end
+			index = index + 1
+		end)
+	return hash
+end
+
+
+--- LuCI Asterisk - Voicemailbox
+-- @type	module
+voicemail = luci.util.class()
+
+--- Parse a voicemail section
+-- @param zone	Table containing the mailbox info
+-- @return		Table with parsed information
+function voicemail.parse(z)
+	if z.number and #z.number > 0 then
+		local v = {
+			id			= '%s@%s' %{ z.number, z.context or 'default' },
+			number		= z.number,
+			context		= z.context 	or 'default',
+			name		= z.name		or z['.name'] or 'libreCMC',
+			zone		= z.zone		or 'homeloc',
+			password	= z.password	or '0000',
+			email		= z.email		or '',
+			page		= z.page		or '',
+			dialplans	= { }
+		}
+
+		uci:foreach("asterisk", "dialplanvoice",
+			function(s)
+				if s.dialplan and #s.dialplan > 0 and
+				   s.voicebox == v.number
+				then
+					v.dialplans[#v.dialplans+1] = s.dialplan
+				end
+			end)
+
+		return v
+	end
+end
+
+--- Get a list of known voicemail boxes
+-- @return		Associative table of boxes and table of box numbers
+function voicemail.boxes()
+	local vboxes = { }
+	local vnames = { }
+	uci:foreach("asterisk", "voicemail",
+		function(z)
+			local v = voicemail.parse(z)
+			if v then
+				local n = '%s@%s' %{ v.number, v.context }
+				vboxes[n]  = v
+				vnames[#vnames+1] = n
+			end
+		end)
+	return vboxes, vnames
+end
+
+--- Get a specific voicemailbox
+-- @param number	Number of the voicemailbox
+-- @return			Table containing mailbox information
+function voicemail.box(n)
+	local box
+	n = n:gsub("@.+$","")
+	uci:foreach("asterisk", "voicemail",
+		function(z)
+			if z.number == tostring(n) then
+				box = voicemail.parse(z)
+			end
+		end)
+	return box
+end
+
+--- Find all voicemailboxes within the given dialplan
+-- @param plan	Dialplan name or table
+-- @return		Associative table containing extensions mapped to mailbox info
+function voicemail.in_dialplan(p)
+	local plan  = type(p) == "string" and p or p.name
+	local boxes = { }
+	uci:foreach("asterisk", "dialplanvoice",
+		function(s)
+			if s.extension and #s.extension > 0 and s.dialplan == plan then
+				local box = voicemail.box(s.voicebox)
+				if box then
+					boxes[s.extension] = box
+				end
+			end
+		end)
+	return boxes
+end
+
+--- Remove voicemailbox and associated extensions from config
+-- @param box	Voicemailbox number or table
+-- @param ctx	UCI context to use (optional)
+-- @return		Boolean indicating success
+function voicemail.remove(v, ctx)
+	ctx = ctx or uci
+	local box = type(v) == "string" and v or v.number
+	local ok1 = ctx:delete_all("asterisk", "voicemail", {number=box})
+	local ok2 = ctx:delete_all("asterisk", "dialplanvoice", {voicebox=box})
+	return ( ok1 or ok2 ) and true or false
+end
+
+
+--- LuCI Asterisk - MeetMe Conferences
+-- @type	module
+meetme = luci.util.class()
+
+--- Parse a meetme section
+-- @param room	Table containing the room info
+-- @return		Table with parsed information
+function meetme.parse(r)
+	if r.room and #r.room > 0 then
+		local v = {
+			room		= r.room,
+			pin			= r.pin 			or '',
+			adminpin	= r.adminpin		or '',
+			description = r._description	or '',
+			dialplans	= { }
+		}
+
+		uci:foreach("asterisk", "dialplanmeetme",
+			function(s)
+				if s.dialplan and #s.dialplan > 0 and s.room == v.room then
+					v.dialplans[#v.dialplans+1] = s.dialplan
+				end
+			end)
+
+		return v
+	end
+end
+
+--- Get a list of known meetme rooms
+-- @return		Associative table of rooms and table of room numbers
+function meetme.rooms()
+	local mrooms = { }
+	local mnames = { }
+	uci:foreach("asterisk", "meetme",
+		function(r)
+			local v = meetme.parse(r)
+			if v then
+				mrooms[v.room] = v
+				mnames[#mnames+1] = v.room
+			end
+		end)
+	return mrooms, mnames
+end
+
+--- Get a specific meetme room
+-- @param number	Number of the room
+-- @return			Table containing room information
+function meetme.room(n)
+	local room
+	uci:foreach("asterisk", "meetme",
+		function(r)
+			if r.room == tostring(n) then
+				room = meetme.parse(r)
+			end
+		end)
+	return room
+end
+
+--- Find all meetme rooms within the given dialplan
+-- @param plan	Dialplan name or table
+-- @return		Associative table containing extensions mapped to room info
+function meetme.in_dialplan(p)
+	local plan  = type(p) == "string" and p or p.name
+	local rooms = { }
+	uci:foreach("asterisk", "dialplanmeetme",
+		function(s)
+			if s.extension and #s.extension > 0 and s.dialplan == plan then
+				local room = meetme.room(s.room)
+				if room then
+					rooms[s.extension] = room
+				end
+			end
+		end)
+	return rooms
+end
+
+--- Remove meetme room and associated extensions from config
+-- @param room	Voicemailbox number or table
+-- @param ctx	UCI context to use (optional)
+-- @return		Boolean indicating success
+function meetme.remove(v, ctx)
+	ctx = ctx or uci
+	local room = type(v) == "string" and v or v.number
+	local ok1  = ctx:delete_all("asterisk", "meetme", {room=room})
+	local ok2  = ctx:delete_all("asterisk", "dialplanmeetme", {room=room})
+	return ( ok1 or ok2 ) and true or false
+end
+
+
+--- LuCI Asterisk - Dialplan
+-- @type	module
+dialplan = luci.util.class()
+
+--- Parse a dialplan section
+-- @param plan	Table containing the plan info
+-- @return		Table with parsed information
+function dialplan.parse(z)
+	if z['.name'] then
+		local plan = {
+			zones		= { },
+			name    	= z['.name'],
+			description	= z.description or z['.name']
+		}
+
+		-- dialzones
+		for _, name in ipairs(tools.parse_list(z.include)) do
+			local zone = dialzone.zone(name)
+			if zone then
+				plan.zones[#plan.zones+1] = zone
+			end
+		end
+
+		-- voicemailboxes
+		plan.voicemailboxes = voicemail.in_dialplan(plan)
+
+		-- meetme conferences
+		plan.meetmerooms = meetme.in_dialplan(plan)
+
+		return plan
+	end
+end
+
+--- Get a list of known dial plans
+-- @return		Associative table of plans and table of plan names
+function dialplan.plans()
+	local plans  = { }
+	local pnames = { }
+	uci:foreach("asterisk", "dialplan",
+		function(p)
+			plans[p['.name']] = dialplan.parse(p)
+			pnames[#pnames+1] = p['.name']
+		end)
+	return plans, pnames
+end
+
+--- Get a specific dial plan
+-- @param name	Name of the dial plan
+-- @return		Table containing plan information
+function dialplan.plan(n)
+	local plan
+	uci:foreach("asterisk", "dialplan",
+		function(p)
+			if p['.name'] == n then
+				plan = dialplan.parse(p)
+			end
+		end)
+	return plan
+end

+ 254 - 0
luci/applications/luci-asterisk/luasrc/asterisk/cc_idd.lua

@@ -0,0 +1,254 @@
+--[[
+LuCI - Asterisk - International Direct Dialing Prefixes and Country Codes
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+]]--
+
+module "luci.asterisk.cc_idd"
+
+CC_IDD = {
+--	Country, CC, IDD
+	{ "Afghanistan", "93", "00" },
+	{ "Albania", "355", "00" },
+	{ "Algeria", "213", "00" },
+	{ "American Samoa", "684", "00" },
+	{ "Andorra", "376", "00" },
+	{ "Angola", "244", "00" },
+	{ "Anguilla", "264", "011" },
+	{ "Antarctica", "672", "" },
+	{ "Antigua", "268", "011" },
+	{ "Argentina", "54", "00" },
+	{ "Armenia", "374", "00" },
+	{ "Aruba", "297", "00" },
+	{ "Ascension Island", "247", "00" },
+	{ "Australia", "61", "0011" },
+	{ "Austria", "43", "00" },
+	{ "Azberbaijan", "994", "00" },
+	{ "Bahamas", "242", "011" },
+	{ "Bahrain", "973", "00" },
+	{ "Bangladesh", "880", "00" },
+	{ "Barbados", "246", "011" },
+	{ "Barbuda", "268", "011" },
+	{ "Belarus", "375", "810" },
+	{ "Belgium", "32", "00" },
+	{ "Belize", "501", "00" },
+	{ "Benin", "229", "00" },
+	{ "Bermuda", "441", "011" },
+	{ "Bhutan", "975", "00" },
+	{ "Bolivia", "591", "00" },
+	{ "Bosnia", "387", "00" },
+	{ "Botswana", "267", "00" },
+	{ "Brazil", "55", "00" },
+	{ "British Virgin Islands", "284", "011" },
+	{ "Brunei", "673", "00" },
+	{ "Bulgaria", "359", "00" },
+	{ "Burkina Faso", "226", "00" },
+	{ "Burma (Myanmar)", "95", "00" },
+	{ "Burundi", "257", "00" },
+	{ "Cambodia", "855", "001" },
+	{ "Cameroon", "237", "00" },
+	{ "Canada", "1", "011" },
+	{ "Cape Verde Islands", "238", "0" },
+	{ "Cayman Islands", "345", "011" },
+	{ "Central African Rep.", "236", "00" },
+	{ "Chad", "235", "15" },
+	{ "Chile", "56", "00" },
+	{ "China", "86", "00" },
+	{ "Christmas Island", "61", "0011" },
+	{ "Cocos Islands", "61", "0011" },
+	{ "Colombia", "57", "00" },
+	{ "Comoros", "269", "00" },
+	{ "Congo", "242", "00" },
+	{ "Congo, Dem. Rep. of", "243", "00" },
+	{ "Cook Islands", "682", "00" },
+	{ "Costa Rica", "506", "00" },
+	{ "Croatia", "385", "00" },
+	{ "Cuba", "53", "119" },
+	{ "Cyprus", "357", "00" },
+	{ "Czech Republic", "420", "00" },
+	{ "Denmark", "45", "00" },
+	{ "Diego Garcia", "246", "00" },
+	{ "Djibouti", "253", "00" },
+	{ "Dominica", "767", "011" },
+	{ "Dominican Rep.", "809", "011" },
+	{ "Ecuador", "593", "00" },
+	{ "Egypt", "20", "00" },
+	{ "El Salvador", "503", "00" },
+	{ "Equatorial Guinea", "240", "00" },
+	{ "Eritrea", "291", "00" },
+	{ "Estonia", "372", "00" },
+	{ "Ethiopia", "251", "00" },
+	{ "Faeroe Islands", "298", "00" },
+	{ "Falkland Islands", "500", "00" },
+	{ "Fiji Islands", "679", "00" },
+	{ "Finland", "358", "00" },
+	{ "France", "33", "00" },
+	{ "French Antilles", "596", "00" },
+	{ "French Guiana", "594", "00" },
+	{ "French Polynesia", "689", "00" },
+	{ "Gabon", "241", "00" },
+	{ "Gambia", "220", "00" },
+	{ "Georgia", "995", "810" },
+	{ "Germany", "49", "00" },
+	{ "Ghana", "233", "00" },
+	{ "Gibraltar", "350", "00" },
+	{ "Greece", "30", "00" },
+	{ "Greenland", "299", "00" },
+	{ "Grenada", "473", "011" },
+	{ "Guadeloupe", "590", "00" },
+	{ "Guam", "671", "011" },
+	{ "Guantanamo Bay", "5399", "00" },
+	{ "Guatemala", "502", "00" },
+	{ "Guinea", "224", "00" },
+	{ "Guinea Bissau", "245", "00" },
+	{ "Guyana", "592", "001" },
+	{ "Haiti", "509", "00" },
+	{ "Honduras", "504", "00" },
+	{ "Hong Kong", "852", "001" },
+	{ "Hungary", "36", "00" },
+	{ "Iceland", "354", "00" },
+	{ "India", "91", "00" },
+	{ "Indonesia", "62", { "001", "008" } },
+	{ "Iran", "98", "00" },
+	{ "Iraq", "964", "00" },
+	{ "Ireland", "353", "00" },
+	{ "Israel", "972", "00" },
+	{ "Italy", "39", "00" },
+	{ "Ivory Coast", "225", "00" },
+	{ "Jamaica", "876", "011" },
+	{ "Japan", "81", "001" },
+	{ "Jordan", "962", "00" },
+	{ "Kazakhstan", "7", "810" },
+	{ "Kenya", "254", "000" },
+	{ "Kiribati", "686", "00" },
+	{ "Korea, North", "850", "00" },
+	{ "Korea, South", "82", "001" },
+	{ "Kuwait", "965", "00" },
+	{ "Kyrgyzstan", "996", "00" },
+	{ "Laos", "856", "00" },
+	{ "Latvia", "371", "00" },
+	{ "Lebanon", "961", "00" },
+	{ "Lesotho", "266", "00" },
+	{ "Liberia", "231", "00" },
+	{ "Libya", "218", "00" },
+	{ "Liechtenstein", "423", "00" },
+	{ "Lithuania", "370", "00" },
+	{ "Luxembourg", "352", "00" },
+	{ "Macau", "853", "00" },
+	{ "Macedonia", "389", "00" },
+	{ "Madagascar", "261", "00" },
+	{ "Malawi", "265", "00" },
+	{ "Malaysia", "60", "00" },
+	{ "Maldives", "960", "00" },
+	{ "Mali", "223", "00" },
+	{ "Malta", "356", "00" },
+	{ "Mariana Islands", "670", "011" },
+	{ "Marshall Islands", "692", "011" },
+	{ "Martinique", "596", "00" },
+	{ "Mauritania", "222", "00" },
+	{ "Mauritius", "230", "00" },
+	{ "Mayotte Islands", "269", "00" },
+	{ "Mexico", "52", "00" },
+	{ "Micronesia", "691", "011" },
+	{ "Midway Island", "808", "011" },
+	{ "Moldova", "373", "00" },
+	{ "Monaco", "377", "00" },
+	{ "Mongolia", "976", "001" },
+	{ "Montserrat", "664", "011" },
+	{ "Morocco", "212", "00" },
+	{ "Mozambique", "258", "00" },
+	{ "Myanmar (Burma)", "95", "00" },
+	{ "Namibia", "264", "00" },
+	{ "Nauru", "674", "00" },
+	{ "Nepal", "977", "00" },
+	{ "Netherlands", "31", "00" },
+	{ "Netherlands Antilles", "599", "00" },
+	{ "Nevis", "869", "011" },
+	{ "New Caledonia", "687", "00" },
+	{ "New Zealand", "64", "00" },
+	{ "Nicaragua", "505", "00" },
+	{ "Niger", "227", "00" },
+	{ "Nigeria", "234", "009" },
+	{ "Niue", "683", "00" },
+	{ "Norfolk Island", "672", "00" },
+	{ "Norway", "47", "00" },
+	{ "Oman", "968", "00" },
+	{ "Pakistan", "92", "00" },
+	{ "Palau", "680", "011" },
+	{ "Palestine", "970", "00" },
+	{ "Panama", "507", "00" },
+	{ "Papua New Guinea", "675", "05" },
+	{ "Paraguay", "595", "002" },
+	{ "Peru", "51", "00" },
+	{ "Philippines", "63", "00" },
+	{ "Poland", "48", "00" },
+	{ "Portugal", "351", "00" },
+	{ "Puerto Rico", { "787", "939" }, "011" },
+	{ "Qatar", "974", "00" },
+	{ "Reunion Island", "262", "00" },
+	{ "Romania", "40", "00" },
+	{ "Russia", "7", "810" },
+	{ "Rwanda", "250", "00" },
+	{ "St. Helena", "290", "00" },
+	{ "St. Kitts", "869", "011" },
+	{ "St. Lucia", "758", "011" },
+	{ "St. Perre & Miquelon", "508", "00" },
+	{ "St. Vincent", "784", "011" },
+	{ "San Marino", "378", "00" },
+	{ "Sao Tome & Principe", "239", "00" },
+	{ "Saudi Arabia", "966", "00" },
+	{ "Senegal", "221", "00" },
+	{ "Serbia", "381", "99" },
+	{ "Seychelles", "248", "00" },
+	{ "Sierra Leone", "232", "00" },
+	{ "Singapore", "65", "001" },
+	{ "Slovakia", "421", "00" },
+	{ "Slovenia", "386", "00" },
+	{ "Solomon Islands", "677", "00" },
+	{ "Somalia", "252", "00" },
+	{ "South Africa", "27", "09" },
+	{ "Spain", "34", "00" },
+	{ "Sri Lanka", "94", "00" },
+	{ "Sudan", "249", "00" },
+	{ "Suriname", "597", "00" },
+	{ "Swaziland", "268", "00" },
+	{ "Sweden", "46", "00" },
+	{ "Switzerland", "41", "00" },
+	{ "Syria", "963", "00" },
+	{ "Taiwan", "886", "002" },
+	{ "Tajikistan", "992", "810" },
+	{ "Tanzania", "255", "00" },
+	{ "Thailand", "66", "001" },
+	{ "Togo", "228", "00" },
+	{ "Tonga", "676", "00" },
+	{ "Trinidad & Tobago", "868", "011" },
+	{ "Tunisia", "216", "00" },
+	{ "Turkey", "90", "00" },
+	{ "Turkmenistan", "993", "810" },
+	{ "Turks & Caicos", "649", "011" },
+	{ "Tuvalu", "688", "00" },
+	{ "Uganda", "256", "000" },
+	{ "Ukraine", "380", "810" },
+	{ "United Arab Emirates", "971", "00" },
+	{ "United Kingdom", "44", "00" },
+	{ "Uruguay", "598", "00" },
+	{ "USA", "1", "011" },
+	{ "US Virgin Islands", "340", "011" },
+	{ "Uzbekistan", "998", "810" },
+	{ "Vanuatu", "678", "00" },
+	{ "Vatican City", "39", "00" },
+	{ "Venezuela", "58", "00" },
+	{ "Vietnam", "84", "00" },
+	{ "Wake Island", "808", "00" },
+	{ "Wallis & Futuna", "681", "19" },
+	{ "Western Samoa", "685", "00" },
+	{ "Yemen", "967", "00" },
+	{ "Yugoslavia", "381", "99" },
+	{ "Zambia", "260", "00" },
+	{ "Zimbabwe", "263", "00" }
+}

+ 205 - 0
luci/applications/luci-asterisk/luasrc/controller/asterisk.lua

@@ -0,0 +1,205 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+module("luci.controller.asterisk", package.seeall)
+
+function index()
+
+	entry({"admin", "services", "asterisk"}, 				  cbi("asterisk"), 			  "Asterisk",				80)
+
+	entry({"admin", "services", "asterisk", "voice"},         cbi("asterisk-voice"),      "Voice Functions",        1)
+	entry({"admin", "services", "asterisk", "meetme"},        cbi("asterisk-meetme"),     "Meetme Conferences",     2)
+
+	entry({"admin", "services", "asterisk", "iax-conns"},     cbi("asterisk-iax-connections"), "IAX Connections",   3)
+	entry({"admin", "services", "asterisk", "sip-conns"},     cbi("asterisk-sip-connections"), "SIP Connections", 	4)
+
+	entry({"admin", "services", "asterisk", "dialplans"},     cbi("asterisk-dialplans"),  "Dial Plans", 			5)
+
+	entry({"admin", "services", "asterisk", "mod"},           cbi("asterisk-mod-app"),    "Modules", 				4)
+	entry({"admin", "services", "asterisk", "mod", "app"},    cbi("asterisk-mod-app"),    "Applications", 			1)
+	entry({"admin", "services", "asterisk", "mod", "cdr"},    cbi("asterisk-mod-cdr"),    "Call Detail Records", 	2)
+	entry({"admin", "services", "asterisk", "mod", "chan"},   cbi("asterisk-mod-chan"),   "Channels", 				3)
+	entry({"admin", "services", "asterisk", "mod", "codec"},  cbi("asterisk-mod-codec"),  "Codecs", 				4)
+	entry({"admin", "services", "asterisk", "mod", "format"}, cbi("asterisk-mod-format"), "Format",					5)
+	entry({"admin", "services", "asterisk", "mod", "func"},   cbi("asterisk-mod-func"),   "Functions", 				6)
+	entry({"admin", "services", "asterisk", "mod", "pbx"},    cbi("asterisk-mod-pbx"),    "PBX", 					7)
+	entry({"admin", "services", "asterisk", "mod", "res"},    cbi("asterisk-mod-res"),    "Resources", 				8)
+	entry({"admin", "services", "asterisk", "mod", "res", "feature"},
+		cbi("asterisk-mod-res-feature"), "Feature Module Configuration", 9 )
+
+
+	entry({"admin", "asterisk"},                    		cbi("asterisk/main"),        		"Asterisk",  99).i18n = "asterisk"
+
+	entry({"admin", "asterisk", "phones"},          		cbi("asterisk/phones"),      		"Phones",       1)
+	entry({"admin", "asterisk", "phones", "sip"},   		cbi("asterisk/phone_sip"),   		nil,            1).leaf = true
+	--entry({"admin", "asterisk", "phones", "exten"}, 		cbi("asterisk/phone_exten"), 		"Extensions",   2).leaf = true
+
+	entry({"admin", "asterisk", "trunks"},          		cbi("asterisk/trunks"),      		"Trunks",       2)
+	entry({"admin", "asterisk", "trunks", "sip"},   		cbi("asterisk/trunk_sip"),   		nil,            1).leaf = true
+
+	entry({"admin", "asterisk", "voicemail"},          		cbi("asterisk/voicemail"),      	"Voicemail",    3)
+	entry({"admin", "asterisk", "voicemail", "mailboxes"},	cbi("asterisk/voicemail"),			"Mailboxes",    1)
+	entry({"admin", "asterisk", "voicemail", "settings"},	cbi("asterisk/voicemail_settings"),	"Settings",     2)
+
+	entry({"admin", "asterisk", "meetme"},          		cbi("asterisk/meetme"),      		"MeetMe",		4)
+	entry({"admin", "asterisk", "meetme", "rooms"},			cbi("asterisk/meetme"),				"Rooms",		1)
+	entry({"admin", "asterisk", "meetme", "settings"},		cbi("asterisk/meetme_settings"),	"Settings",     2)
+
+	entry({"admin", "asterisk", "dialplans"},				call("handle_dialplan"),     		"Call Routing", 5)
+	entry({"admin", "asterisk", "dialplans", "out"},		cbi("asterisk/dialplan_out"),		nil,            1).leaf = true
+	entry({"admin", "asterisk", "dialplans", "zones"},		call("handle_dialzones"),			"Dial Zones",	2).leaf = true
+
+end
+
+
+function handle_dialplan()
+	local uci = luci.model.uci.cursor()
+	local ast = require "luci.asterisk"
+	local err = false
+
+	for k, v in pairs(luci.http.formvaluetable("delzone")) do
+		local plan = ast.dialplan.plan(k)
+		if #v > 0 and plan then
+			local newinc = { }
+
+			for _, z in ipairs(plan.zones) do
+				if z.name ~= v then
+					newinc[#newinc+1] = z.name
+				end
+			end
+
+			uci:delete("asterisk", plan.name, "include")
+
+			if #newinc > 0 then
+				uci:set("asterisk", plan.name, "include", newinc)
+			end
+		end
+	end
+
+	for k, v in pairs(luci.http.formvaluetable("addzone")) do
+		local plan = ast.dialplan.plan(k)
+		local zone = ast.dialzone.zone(v)
+		if #v > 0 and plan and zone then
+			local newinc = { zone.name }
+
+			for _, z in ipairs(plan.zones) do
+				newinc[#newinc+1] = z.name
+			end
+
+			uci:delete("asterisk", plan.name, "include")
+
+			if #newinc > 0 then
+				uci:set("asterisk", plan.name, "include", newinc)
+			end
+		end
+	end
+
+	for k, v in pairs(luci.http.formvaluetable("delvbox")) do
+		local plan = ast.dialplan.plan(k)
+		if #v > 0 and plan then
+			uci:delete_all("asterisk", "dialplanvoice",
+				{ extension=v, dialplan=plan.name })
+		end
+	end
+
+	for k, v in pairs(luci.http.formvaluetable("addvbox")) do
+		local plan = ast.dialplan.plan(k)
+		local vbox = ast.voicemail.box(v)
+		if plan and vbox then
+			local vext = luci.http.formvalue("addvboxext.%s" % plan.name)
+			vext = ( vext and #vext > 0 ) and vext or vbox.number
+			uci:section("asterisk", "dialplanvoice", nil, {
+				dialplan		= plan.name,
+				extension		= vext,
+				voicebox		= vbox.number,
+				voicecontext	= vbox.context
+			})
+		end
+	end
+
+	for k, v in pairs(luci.http.formvaluetable("delmeetme")) do
+		local plan = ast.dialplan.plan(k)
+		if #v > 0 and plan then
+			uci:delete_all("asterisk", "dialplanmeetme",
+				{ extension=v, dialplan=plan.name })
+		end
+	end
+
+	for k, v in pairs(luci.http.formvaluetable("addmeetme")) do
+		local plan = ast.dialplan.plan(k)
+		local meetme = ast.meetme.room(v)
+		if plan and meetme then
+			local mext = luci.http.formvalue("addmeetmeext.%s" % plan.name)
+			mext = ( mext and #mext > 0 ) and mext or meetme.room
+			uci:section("asterisk", "dialplanmeetme", nil, {
+				dialplan	= plan.name,
+				extension	= mext,
+				room		= meetme.room
+			})
+		end
+	end
+
+	local aname = luci.http.formvalue("addplan")
+	if aname and #aname > 0 then
+		if aname:match("^[a-zA-Z0-9_]+$") then
+			uci:section("asterisk", "dialplan", aname, { })
+		else
+			err = true
+		end
+	end
+
+	local dname = luci.http.formvalue("delplan")
+	if dname and #dname > 0 then
+		if uci:get("asterisk", dname) == "dialplan" then
+			uci:delete("asterisk", dname)
+			uci:delete_all("asterisk", "dialplanvoice", { dialplan=dname })
+			uci:delete_all("asterisk", "dialplanmeetme", { dialplan=dname })
+		end
+	end
+
+	uci:save("asterisk")
+	ast.uci_resync()
+
+	luci.template.render("asterisk/dialplans", { create_error = err })
+end
+
+function handle_dialzones()
+	local ast = require "luci.asterisk"
+	local uci = luci.model.uci.cursor()
+	local err = false
+
+	if luci.http.formvalue("newzone") then
+		local name = luci.http.formvalue("newzone_name")
+		if name and name:match("^[a-zA-Z0-9_]+$") then
+			uci:section("asterisk", "dialzone", name, {
+				uses  = ast.tools.parse_list(luci.http.formvalue("newzone_uses") or {}),
+				match = ast.tools.parse_list(luci.http.formvalue("newzone_match") or {})
+			})
+			uci:save("asterisk")
+		else
+			err = true
+		end
+	end
+
+	if luci.http.formvalue("delzone") then
+		local name = luci.http.formvalue("delzone")
+		if uci:get("asterisk", name) == "dialzone" then
+			uci:delete("asterisk", name)
+			uci:save("asterisk")
+		end
+	end
+
+	luci.template.render("asterisk/dialzones", { create_error = err })
+end

+ 102 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-dialplans.lua

@@ -0,0 +1,102 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+dialplan = cbimap:section(TypedSection, "dialplan", "Section dialplan", "")
+dialplan.addremove = true
+dialplan.dynamic = true
+
+include = dialplan:option(MultiValue, "include", "Include zones and plans", "")
+cbimap.uci:foreach( "asterisk", "dialplan", function(s) include:value(s['.name']) end )
+cbimap.uci:foreach( "asterisk", "dialzone", function(s) include:value(s['.name']) end )
+
+dialplanexten = cbimap:section(TypedSection, "dialplanexten", "Dialplan Extension", "")
+dialplanexten.anonymous = true
+dialplanexten.addremove = true
+dialplanexten.dynamic = true
+
+
+dialplangeneral = cbimap:section(TypedSection, "dialplangeneral", "Dialplan General Options", "")
+dialplangeneral.anonymous = true
+dialplangeneral.addremove = true
+
+allowtransfer = dialplangeneral:option(Flag, "allowtransfer", "Allow transfer", "")
+allowtransfer.rmempty = true
+
+canreinvite = dialplangeneral:option(ListValue, "canreinvite", "Reinvite/redirect media connections", "")
+canreinvite:value("yes", "Yes")
+canreinvite:value("nonat", "Yes when not behind NAT")
+canreinvite:value("update", "Use UPDATE rather than INVITE for path redirection")
+canreinvite:value("no", "No")
+canreinvite.rmempty = true
+
+clearglobalvars = dialplangeneral:option(Flag, "clearglobalvars", "Clear global vars", "")
+clearglobalvars.rmempty = true
+
+
+dialplangoto = cbimap:section(TypedSection, "dialplangoto", "Dialplan Goto", "")
+dialplangoto.anonymous = true
+dialplangoto.addremove = true
+dialplangoto.dynamic = true
+
+
+dialplanmeetme = cbimap:section(TypedSection, "dialplanmeetme", "Dialplan Conference", "")
+dialplanmeetme.anonymous = true
+dialplanmeetme.addremove = true
+dialplanmeetme.dynamic = true
+
+
+dialplansaytime = cbimap:section(TypedSection, "dialplansaytime", "Dialplan Time", "")
+dialplansaytime.anonymous = true
+dialplansaytime.addremove = true
+dialplansaytime.dynamic = true
+
+
+dialplanvoice = cbimap:section(TypedSection, "dialplanvoice", "Dialplan Voicemail", "")
+dialplanvoice.anonymous = true
+dialplanvoice.addremove = true
+dialplanvoice.dynamic = true
+
+
+dialzone = cbimap:section(TypedSection, "dialzone", "Dial Zones for Dialplan", "")
+dialzone.addremove = true
+dialzone.template = "cbi/tblsection"
+
+addprefix = dialzone:option(Value, "addprefix", "Prefix to add matching dialplans", "")
+addprefix.rmempty = true
+
+--international = dialzone:option(DynamicList, "international", "Match International prefix", "")
+international = dialzone:option(Value, "international", "Match International prefix", "")
+international.rmempty = true
+
+localprefix = dialzone:option(Value, "localprefix", "Prefix (0) to add/remove to/from intl. numbers", "")
+localprefix.rmempty = true
+
+localzone = dialzone:option(Value, "localzone", "Dialzone for intl. numbers matched as local", "")
+localzone.titleref = luci.dispatcher.build_url( "admin", "services", "asterisk", "dialplans" )
+cbimap.uci:foreach( "asterisk", "dialplan", function(s) localzone:value(s['.name']) end )
+cbimap.uci:foreach( "asterisk", "dialzone", function(s) localzone:value(s['.name']) end )
+
+match = dialzone:option(Value, "match", "Match plan", "")
+match.rmempty = true
+
+uses = dialzone:option(ListValue, "uses", "Connection to use", "")
+uses.titleref = luci.dispatcher.build_url( "admin", "services", "asterisk", "sip-conns" )
+cbimap.uci:foreach( "asterisk", "sip", function(s) uses:value('SIP/'..s['.name']) end )
+cbimap.uci:foreach( "asterisk", "iax", function(s) uses:value('IAX/'..s['.name']) end )
+
+
+return cbimap

+ 60 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-iax-connections.lua

@@ -0,0 +1,60 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+iax = cbimap:section(TypedSection, "iax", "IAX Connection", "")
+iax.addremove = true
+
+alwaysinternational = iax:option(Flag, "alwaysinternational", "Always Dial International", "")
+alwaysinternational.optional = true
+
+context = iax:option(ListValue, "context", "Context to use", "")
+context.titleref = luci.dispatcher.build_url( "admin", "services", "asterisk", "dialplans" )
+cbimap.uci:foreach( "asterisk", "dialplan", function(s) context:value(s['.name']) end )
+cbimap.uci:foreach( "asterisk", "dialzone", function(s) context:value(s['.name']) end )
+
+countrycode = iax:option(Value, "countrycode", "Country Code for connection", "")
+countrycode.optional = true
+
+extension = iax:option(Value, "extension", "Add as Extension", "")
+extension.optional = true
+
+host = iax:option(Value, "host", "Host name (or blank)", "")
+host.optional = true
+
+internationalprefix = iax:option(Value, "internationalprefix", "International Dial Prefix", "")
+internationalprefix.optional = true
+
+prefix = iax:option(Value, "prefix", "Dial Prefix (for external line)", "")
+prefix.optional = true
+
+secret = iax:option(Value, "secret", "Secret", "")
+secret.optional = true
+
+timeout = iax:option(Value, "timeout", "Dial Timeout (sec)", "")
+timeout.optional = true
+
+type = iax:option(ListValue, "type", "Option type", "")
+type:value("friend", "Friend (outbound/inbound)")
+type:value("user", "User (inbound - authenticate by \"from\")")
+type:value("peer", "Peer (outbound - match by host)")
+type.optional = true
+
+username = iax:option(Value, "username", "User name", "")
+username.optional = true
+
+
+return cbimap

+ 33 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-meetme.lua

@@ -0,0 +1,33 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+meetmegeneral = cbimap:section(TypedSection, "meetmegeneral", "Meetme Conference General Options", "")
+
+audiobuffers = meetmegeneral:option(Value, "audiobuffers", "Number of 20ms audio buffers to be used", "")
+
+
+meetme = cbimap:section(TypedSection, "meetme", "Meetme Conference", "")
+meetme.addremove = true
+
+adminpin = meetme:option(Value, "adminpin", "Admin PIN", "")
+adminpin.password = true
+
+pin = meetme:option(Value, "pin", "Meeting PIN", "")
+pin.password = true
+
+
+return cbimap

+ 391 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-app.lua

@@ -0,0 +1,391 @@
+cbimap = Map("asterisk", "asterisk", "")
+
+module = cbimap:section(TypedSection, "module", "Modules", "")
+module.anonymous = true
+
+app_alarmreceiver = module:option(ListValue, "app_alarmreceiver", "Alarm Receiver Application", "")
+app_alarmreceiver:value("yes", "Load")
+app_alarmreceiver:value("no", "Do Not Load")
+app_alarmreceiver:value("auto", "Load as Required")
+app_alarmreceiver.rmempty = true
+
+app_authenticate = module:option(ListValue, "app_authenticate", "Authentication Application", "")
+app_authenticate:value("yes", "Load")
+app_authenticate:value("no", "Do Not Load")
+app_authenticate:value("auto", "Load as Required")
+app_authenticate.rmempty = true
+
+app_cdr = module:option(ListValue, "app_cdr", "Make sure asterisk doesnt save CDR", "")
+app_cdr:value("yes", "Load")
+app_cdr:value("no", "Do Not Load")
+app_cdr:value("auto", "Load as Required")
+app_cdr.rmempty = true
+
+app_chanisavail = module:option(ListValue, "app_chanisavail", "Check if channel is available", "")
+app_chanisavail:value("yes", "Load")
+app_chanisavail:value("no", "Do Not Load")
+app_chanisavail:value("auto", "Load as Required")
+app_chanisavail.rmempty = true
+
+app_chanspy = module:option(ListValue, "app_chanspy", "Listen in on any channel", "")
+app_chanspy:value("yes", "Load")
+app_chanspy:value("no", "Do Not Load")
+app_chanspy:value("auto", "Load as Required")
+app_chanspy.rmempty = true
+
+app_controlplayback = module:option(ListValue, "app_controlplayback", "Control Playback Application", "")
+app_controlplayback:value("yes", "Load")
+app_controlplayback:value("no", "Do Not Load")
+app_controlplayback:value("auto", "Load as Required")
+app_controlplayback.rmempty = true
+
+app_cut = module:option(ListValue, "app_cut", "Cuts up variables", "")
+app_cut:value("yes", "Load")
+app_cut:value("no", "Do Not Load")
+app_cut:value("auto", "Load as Required")
+app_cut.rmempty = true
+
+app_db = module:option(ListValue, "app_db", "Database access functions", "")
+app_db:value("yes", "Load")
+app_db:value("no", "Do Not Load")
+app_db:value("auto", "Load as Required")
+app_db.rmempty = true
+
+app_dial = module:option(ListValue, "app_dial", "Dialing Application", "")
+app_dial:value("yes", "Load")
+app_dial:value("no", "Do Not Load")
+app_dial:value("auto", "Load as Required")
+app_dial.rmempty = true
+
+app_dictate = module:option(ListValue, "app_dictate", "Virtual Dictation Machine Application", "")
+app_dictate:value("yes", "Load")
+app_dictate:value("no", "Do Not Load")
+app_dictate:value("auto", "Load as Required")
+app_dictate.rmempty = true
+
+app_directed_pickup = module:option(ListValue, "app_directed_pickup", "Directed Call Pickup Support", "")
+app_directed_pickup:value("yes", "Load")
+app_directed_pickup:value("no", "Do Not Load")
+app_directed_pickup:value("auto", "Load as Required")
+app_directed_pickup.rmempty = true
+
+app_directory = module:option(ListValue, "app_directory", "Extension Directory", "")
+app_directory:value("yes", "Load")
+app_directory:value("no", "Do Not Load")
+app_directory:value("auto", "Load as Required")
+app_directory.rmempty = true
+
+app_disa = module:option(ListValue, "app_disa", "DISA (Direct Inward System Access) Application", "")
+app_disa:value("yes", "Load")
+app_disa:value("no", "Do Not Load")
+app_disa:value("auto", "Load as Required")
+app_disa.rmempty = true
+
+app_dumpchan = module:option(ListValue, "app_dumpchan", "Dump channel variables Application", "")
+app_dumpchan:value("yes", "Load")
+app_dumpchan:value("no", "Do Not Load")
+app_dumpchan:value("auto", "Load as Required")
+app_dumpchan.rmempty = true
+
+app_echo = module:option(ListValue, "app_echo", "Simple Echo Application", "")
+app_echo:value("yes", "Load")
+app_echo:value("no", "Do Not Load")
+app_echo:value("auto", "Load as Required")
+app_echo.rmempty = true
+
+app_enumlookup = module:option(ListValue, "app_enumlookup", "ENUM Lookup", "")
+app_enumlookup:value("yes", "Load")
+app_enumlookup:value("no", "Do Not Load")
+app_enumlookup:value("auto", "Load as Required")
+app_enumlookup.rmempty = true
+
+app_eval = module:option(ListValue, "app_eval", "Reevaluates strings", "")
+app_eval:value("yes", "Load")
+app_eval:value("no", "Do Not Load")
+app_eval:value("auto", "Load as Required")
+app_eval.rmempty = true
+
+app_exec = module:option(ListValue, "app_exec", "Executes applications", "")
+app_exec:value("yes", "Load")
+app_exec:value("no", "Do Not Load")
+app_exec:value("auto", "Load as Required")
+app_exec.rmempty = true
+
+app_externalivr = module:option(ListValue, "app_externalivr", "External IVR application interface", "")
+app_externalivr:value("yes", "Load")
+app_externalivr:value("no", "Do Not Load")
+app_externalivr:value("auto", "Load as Required")
+app_externalivr.rmempty = true
+
+app_forkcdr = module:option(ListValue, "app_forkcdr", "Fork The CDR into 2 seperate entities", "")
+app_forkcdr:value("yes", "Load")
+app_forkcdr:value("no", "Do Not Load")
+app_forkcdr:value("auto", "Load as Required")
+app_forkcdr.rmempty = true
+
+app_getcpeid = module:option(ListValue, "app_getcpeid", "Get ADSI CPE ID", "")
+app_getcpeid:value("yes", "Load")
+app_getcpeid:value("no", "Do Not Load")
+app_getcpeid:value("auto", "Load as Required")
+app_getcpeid.rmempty = true
+
+app_groupcount = module:option(ListValue, "app_groupcount", "Group Management Routines", "")
+app_groupcount:value("yes", "Load")
+app_groupcount:value("no", "Do Not Load")
+app_groupcount:value("auto", "Load as Required")
+app_groupcount.rmempty = true
+
+app_ices = module:option(ListValue, "app_ices", "Encode and Stream via icecast and ices", "")
+app_ices:value("yes", "Load")
+app_ices:value("no", "Do Not Load")
+app_ices:value("auto", "Load as Required")
+app_ices.rmempty = true
+
+app_image = module:option(ListValue, "app_image", "Image Transmission Application", "")
+app_image:value("yes", "Load")
+app_image:value("no", "Do Not Load")
+app_image:value("auto", "Load as Required")
+app_image.rmempty = true
+
+app_lookupblacklist = module:option(ListValue, "app_lookupblacklist", "Look up Caller*ID name/number from black", "")
+app_lookupblacklist:value("yes", "Load")
+app_lookupblacklist:value("no", "Do Not Load")
+app_lookupblacklist:value("auto", "Load as Required")
+app_lookupblacklist.rmempty = true
+
+app_lookupcidname = module:option(ListValue, "app_lookupcidname", "Look up CallerID Name from local databas", "")
+app_lookupcidname:value("yes", "Load")
+app_lookupcidname:value("no", "Do Not Load")
+app_lookupcidname:value("auto", "Load as Required")
+app_lookupcidname.rmempty = true
+
+app_macro = module:option(ListValue, "app_macro", "Extension Macros", "")
+app_macro:value("yes", "Load")
+app_macro:value("no", "Do Not Load")
+app_macro:value("auto", "Load as Required")
+app_macro.rmempty = true
+
+app_math = module:option(ListValue, "app_math", "A simple math Application", "")
+app_math:value("yes", "Load")
+app_math:value("no", "Do Not Load")
+app_math:value("auto", "Load as Required")
+app_math.rmempty = true
+
+app_md5 = module:option(ListValue, "app_md5", "MD5 checksum Application", "")
+app_md5:value("yes", "Load")
+app_md5:value("no", "Do Not Load")
+app_md5:value("auto", "Load as Required")
+app_md5.rmempty = true
+
+app_milliwatt = module:option(ListValue, "app_milliwatt", "Digital Milliwatt (mu-law) Test Application", "")
+app_milliwatt:value("yes", "Load")
+app_milliwatt:value("no", "Do Not Load")
+app_milliwatt:value("auto", "Load as Required")
+app_milliwatt.rmempty = true
+
+app_mixmonitor = module:option(ListValue, "app_mixmonitor", "Record a call and mix the audio during the recording", "")
+app_mixmonitor:value("yes", "Load")
+app_mixmonitor:value("no", "Do Not Load")
+app_mixmonitor:value("auto", "Load as Required")
+app_mixmonitor.rmempty = true
+
+app_parkandannounce = module:option(ListValue, "app_parkandannounce", "Call Parking and Announce Application", "")
+app_parkandannounce:value("yes", "Load")
+app_parkandannounce:value("no", "Do Not Load")
+app_parkandannounce:value("auto", "Load as Required")
+app_parkandannounce.rmempty = true
+
+app_playback = module:option(ListValue, "app_playback", "Trivial Playback Application", "")
+app_playback:value("yes", "Load")
+app_playback:value("no", "Do Not Load")
+app_playback:value("auto", "Load as Required")
+app_playback.rmempty = true
+
+app_privacy = module:option(ListValue, "app_privacy", "Require phone number to be entered", "")
+app_privacy:value("yes", "Load")
+app_privacy:value("no", "Do Not Load")
+app_privacy:value("auto", "Load as Required")
+app_privacy.rmempty = true
+
+app_queue = module:option(ListValue, "app_queue", "True Call Queueing", "")
+app_queue:value("yes", "Load")
+app_queue:value("no", "Do Not Load")
+app_queue:value("auto", "Load as Required")
+app_queue.rmempty = true
+
+app_random = module:option(ListValue, "app_random", "Random goto", "")
+app_random:value("yes", "Load")
+app_random:value("no", "Do Not Load")
+app_random:value("auto", "Load as Required")
+app_random.rmempty = true
+
+app_read = module:option(ListValue, "app_read", "Read Variable Application", "")
+app_read:value("yes", "Load")
+app_read:value("no", "Do Not Load")
+app_read:value("auto", "Load as Required")
+app_read.rmempty = true
+
+app_readfile = module:option(ListValue, "app_readfile", "Read in a file", "")
+app_readfile:value("yes", "Load")
+app_readfile:value("no", "Do Not Load")
+app_readfile:value("auto", "Load as Required")
+app_readfile.rmempty = true
+
+app_realtime = module:option(ListValue, "app_realtime", "Realtime Data Lookup/Rewrite", "")
+app_realtime:value("yes", "Load")
+app_realtime:value("no", "Do Not Load")
+app_realtime:value("auto", "Load as Required")
+app_realtime.rmempty = true
+
+app_record = module:option(ListValue, "app_record", "Trivial Record Application", "")
+app_record:value("yes", "Load")
+app_record:value("no", "Do Not Load")
+app_record:value("auto", "Load as Required")
+app_record.rmempty = true
+
+app_sayunixtime = module:option(ListValue, "app_sayunixtime", "Say time", "")
+app_sayunixtime:value("yes", "Load")
+app_sayunixtime:value("no", "Do Not Load")
+app_sayunixtime:value("auto", "Load as Required")
+app_sayunixtime.rmempty = true
+
+app_senddtmf = module:option(ListValue, "app_senddtmf", "Send DTMF digits Application", "")
+app_senddtmf:value("yes", "Load")
+app_senddtmf:value("no", "Do Not Load")
+app_senddtmf:value("auto", "Load as Required")
+app_senddtmf.rmempty = true
+
+app_sendtext = module:option(ListValue, "app_sendtext", "Send Text Applications", "")
+app_sendtext:value("yes", "Load")
+app_sendtext:value("no", "Do Not Load")
+app_sendtext:value("auto", "Load as Required")
+app_sendtext.rmempty = true
+
+app_setcallerid = module:option(ListValue, "app_setcallerid", "Set CallerID Application", "")
+app_setcallerid:value("yes", "Load")
+app_setcallerid:value("no", "Do Not Load")
+app_setcallerid:value("auto", "Load as Required")
+app_setcallerid.rmempty = true
+
+app_setcdruserfield = module:option(ListValue, "app_setcdruserfield", "CDR user field apps", "")
+app_setcdruserfield:value("yes", "Load")
+app_setcdruserfield:value("no", "Do Not Load")
+app_setcdruserfield:value("auto", "Load as Required")
+app_setcdruserfield.rmempty = true
+
+app_setcidname = module:option(ListValue, "app_setcidname", "load => .so ; Set CallerID Name", "")
+app_setcidname:value("yes", "Load")
+app_setcidname:value("no", "Do Not Load")
+app_setcidname:value("auto", "Load as Required")
+app_setcidname.rmempty = true
+
+app_setcidnum = module:option(ListValue, "app_setcidnum", "load => .so ; Set CallerID Number", "")
+app_setcidnum:value("yes", "Load")
+app_setcidnum:value("no", "Do Not Load")
+app_setcidnum:value("auto", "Load as Required")
+app_setcidnum.rmempty = true
+
+app_setrdnis = module:option(ListValue, "app_setrdnis", "Set RDNIS Number", "")
+app_setrdnis:value("yes", "Load")
+app_setrdnis:value("no", "Do Not Load")
+app_setrdnis:value("auto", "Load as Required")
+app_setrdnis.rmempty = true
+
+app_settransfercapability = module:option(ListValue, "app_settransfercapability", "Set ISDN Transfer Capability", "")
+app_settransfercapability:value("yes", "Load")
+app_settransfercapability:value("no", "Do Not Load")
+app_settransfercapability:value("auto", "Load as Required")
+app_settransfercapability.rmempty = true
+
+app_sms = module:option(ListValue, "app_sms", "SMS/PSTN handler", "")
+app_sms:value("yes", "Load")
+app_sms:value("no", "Do Not Load")
+app_sms:value("auto", "Load as Required")
+app_sms.rmempty = true
+
+app_softhangup = module:option(ListValue, "app_softhangup", "Hangs up the requested channel", "")
+app_softhangup:value("yes", "Load")
+app_softhangup:value("no", "Do Not Load")
+app_softhangup:value("auto", "Load as Required")
+app_softhangup.rmempty = true
+
+app_stack = module:option(ListValue, "app_stack", "Stack Routines", "")
+app_stack:value("yes", "Load")
+app_stack:value("no", "Do Not Load")
+app_stack:value("auto", "Load as Required")
+app_stack.rmempty = true
+
+app_system = module:option(ListValue, "app_system", "Generic System() application", "")
+app_system:value("yes", "Load")
+app_system:value("no", "Do Not Load")
+app_system:value("auto", "Load as Required")
+app_system.rmempty = true
+
+app_talkdetect = module:option(ListValue, "app_talkdetect", "Playback with Talk Detection", "")
+app_talkdetect:value("yes", "Load")
+app_talkdetect:value("no", "Do Not Load")
+app_talkdetect:value("auto", "Load as Required")
+app_talkdetect.rmempty = true
+
+app_test = module:option(ListValue, "app_test", "Interface Test Application", "")
+app_test:value("yes", "Load")
+app_test:value("no", "Do Not Load")
+app_test:value("auto", "Load as Required")
+app_test.rmempty = true
+
+app_transfer = module:option(ListValue, "app_transfer", "Transfer", "")
+app_transfer:value("yes", "Load")
+app_transfer:value("no", "Do Not Load")
+app_transfer:value("auto", "Load as Required")
+app_transfer.rmempty = true
+
+app_txtcidname = module:option(ListValue, "app_txtcidname", "TXTCIDName", "")
+app_txtcidname:value("yes", "Load")
+app_txtcidname:value("no", "Do Not Load")
+app_txtcidname:value("auto", "Load as Required")
+app_txtcidname.rmempty = true
+
+app_url = module:option(ListValue, "app_url", "Send URL Applications", "")
+app_url:value("yes", "Load")
+app_url:value("no", "Do Not Load")
+app_url:value("auto", "Load as Required")
+app_url.rmempty = true
+
+app_userevent = module:option(ListValue, "app_userevent", "Custom User Event Application", "")
+app_userevent:value("yes", "Load")
+app_userevent:value("no", "Do Not Load")
+app_userevent:value("auto", "Load as Required")
+app_userevent.rmempty = true
+
+app_verbose = module:option(ListValue, "app_verbose", "Send verbose output", "")
+app_verbose:value("yes", "Load")
+app_verbose:value("no", "Do Not Load")
+app_verbose:value("auto", "Load as Required")
+app_verbose.rmempty = true
+
+app_voicemail = module:option(ListValue, "app_voicemail", "Voicemail", "")
+app_voicemail:value("yes", "Load")
+app_voicemail:value("no", "Do Not Load")
+app_voicemail:value("auto", "Load as Required")
+app_voicemail.rmempty = true
+
+app_waitforring = module:option(ListValue, "app_waitforring", "Waits until first ring after time", "")
+app_waitforring:value("yes", "Load")
+app_waitforring:value("no", "Do Not Load")
+app_waitforring:value("auto", "Load as Required")
+app_waitforring.rmempty = true
+
+app_waitforsilence = module:option(ListValue, "app_waitforsilence", "Wait For Silence Application", "")
+app_waitforsilence:value("yes", "Load")
+app_waitforsilence:value("no", "Do Not Load")
+app_waitforsilence:value("auto", "Load as Required")
+app_waitforsilence.rmempty = true
+
+app_while = module:option(ListValue, "app_while", "While Loops and Conditional Execution", "")
+app_while:value("yes", "Load")
+app_while:value("no", "Do Not Load")
+app_while:value("auto", "Load as Required")
+app_while.rmempty = true
+
+
+return cbimap

+ 58 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-cdr.lua

@@ -0,0 +1,58 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+module = cbimap:section(TypedSection, "module", "Modules", "")
+module.anonymous = true
+
+cdr_csv = module:option(ListValue, "cdr_csv", "Comma Separated Values CDR Backend", "")
+cdr_csv:value("yes", "Load")
+cdr_csv:value("no", "Do Not Load")
+cdr_csv:value("auto", "Load as Required")
+cdr_csv.rmempty = true
+
+cdr_custom = module:option(ListValue, "cdr_custom", "Customizable Comma Separated Values CDR Backend", "")
+cdr_custom:value("yes", "Load")
+cdr_custom:value("no", "Do Not Load")
+cdr_custom:value("auto", "Load as Required")
+cdr_custom.rmempty = true
+
+cdr_manager = module:option(ListValue, "cdr_manager", "Asterisk Call Manager CDR Backend", "")
+cdr_manager:value("yes", "Load")
+cdr_manager:value("no", "Do Not Load")
+cdr_manager:value("auto", "Load as Required")
+cdr_manager.rmempty = true
+
+cdr_mysql = module:option(ListValue, "cdr_mysql", "MySQL CDR Backend", "")
+cdr_mysql:value("yes", "Load")
+cdr_mysql:value("no", "Do Not Load")
+cdr_mysql:value("auto", "Load as Required")
+cdr_mysql.rmempty = true
+
+cdr_pgsql = module:option(ListValue, "cdr_pgsql", "PostgreSQL CDR Backend", "")
+cdr_pgsql:value("yes", "Load")
+cdr_pgsql:value("no", "Do Not Load")
+cdr_pgsql:value("auto", "Load as Required")
+cdr_pgsql.rmempty = true
+
+cdr_sqlite = module:option(ListValue, "cdr_sqlite", "SQLite CDR Backend", "")
+cdr_sqlite:value("yes", "Load")
+cdr_sqlite:value("no", "Do Not Load")
+cdr_sqlite:value("auto", "Load as Required")
+cdr_sqlite.rmempty = true
+
+
+return cbimap

+ 56 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-chan.lua

@@ -0,0 +1,56 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+
+module = cbimap:section(TypedSection, "module", "Modules", "")
+module.anonymous = true
+
+chan_agent = module:option(ListValue, "chan_agent", "Agent Proxy Channel", "")
+chan_agent:value("yes", "Load")
+chan_agent:value("no", "Do Not Load")
+chan_agent:value("auto", "Load as Required")
+chan_agent.rmempty = true
+
+chan_alsa = module:option(ListValue, "chan_alsa", "Channel driver for GTalk", "")
+chan_alsa:value("yes", "Load")
+chan_alsa:value("no", "Do Not Load")
+chan_alsa:value("auto", "Load as Required")
+chan_alsa.rmempty = true
+
+chan_gtalk = module:option(ListValue, "chan_gtalk", "Channel driver for GTalk", "")
+chan_gtalk:value("yes", "Load")
+chan_gtalk:value("no", "Do Not Load")
+chan_gtalk:value("auto", "Load as Required")
+chan_gtalk.rmempty = true
+
+chan_iax2 = module:option(Flag, "chan_iax2", "Option chan_iax2", "")
+chan_iax2.rmempty = true
+
+chan_local = module:option(ListValue, "chan_local", "Local Proxy Channel", "")
+chan_local:value("yes", "Load")
+chan_local:value("no", "Do Not Load")
+chan_local:value("auto", "Load as Required")
+chan_local.rmempty = true
+
+chan_sip = module:option(ListValue, "chan_sip", "Session Initiation Protocol (SIP)", "")
+chan_sip:value("yes", "Load")
+chan_sip:value("no", "Do Not Load")
+chan_sip:value("auto", "Load as Required")
+chan_sip.rmempty = true
+
+
+return cbimap

+ 64 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-codec.lua

@@ -0,0 +1,64 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+module = cbimap:section(TypedSection, "module", "Modules", "")
+module.anonymous = true
+
+codec_a_mu = module:option(ListValue, "codec_a_mu", "A-law and Mulaw direct Coder/Decoder", "")
+codec_a_mu:value("yes", "Load")
+codec_a_mu:value("no", "Do Not Load")
+codec_a_mu:value("auto", "Load as Required")
+codec_a_mu.rmempty = true
+
+codec_adpcm = module:option(ListValue, "codec_adpcm", "Adaptive Differential PCM Coder/Decoder", "")
+codec_adpcm:value("yes", "Load")
+codec_adpcm:value("no", "Do Not Load")
+codec_adpcm:value("auto", "Load as Required")
+codec_adpcm.rmempty = true
+
+codec_alaw = module:option(ListValue, "codec_alaw", "A-law Coder/Decoder", "")
+codec_alaw:value("yes", "Load")
+codec_alaw:value("no", "Do Not Load")
+codec_alaw:value("auto", "Load as Required")
+codec_alaw.rmempty = true
+
+codec_g726 = module:option(ListValue, "codec_g726", "ITU G.726-32kbps G726 Transcoder", "")
+codec_g726:value("yes", "Load")
+codec_g726:value("no", "Do Not Load")
+codec_g726:value("auto", "Load as Required")
+codec_g726.rmempty = true
+
+codec_gsm = module:option(ListValue, "codec_gsm", "GSM/PCM16 (signed linear) Codec Translation", "")
+codec_gsm:value("yes", "Load")
+codec_gsm:value("no", "Do Not Load")
+codec_gsm:value("auto", "Load as Required")
+codec_gsm.rmempty = true
+
+codec_speex = module:option(ListValue, "codec_speex", "Speex/PCM16 (signed linear) Codec Translator", "")
+codec_speex:value("yes", "Load")
+codec_speex:value("no", "Do Not Load")
+codec_speex:value("auto", "Load as Required")
+codec_speex.rmempty = true
+
+codec_ulaw = module:option(ListValue, "codec_ulaw", "Mu-law Coder/Decoder", "")
+codec_ulaw:value("yes", "Load")
+codec_ulaw:value("no", "Do Not Load")
+codec_ulaw:value("auto", "Load as Required")
+codec_ulaw.rmempty = true
+
+
+return cbimap

+ 100 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-format.lua

@@ -0,0 +1,100 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+module = cbimap:section(TypedSection, "module", "Modules", "")
+module.anonymous = true
+
+format_au = module:option(ListValue, "format_au", "Sun Microsystems AU format (signed linear)", "")
+format_au:value("yes", "Load")
+format_au:value("no", "Do Not Load")
+format_au:value("auto", "Load as Required")
+format_au.rmempty = true
+
+format_g723 = module:option(ListValue, "format_g723", "G.723.1 Simple Timestamp File Format", "")
+format_g723:value("yes", "Load")
+format_g723:value("no", "Do Not Load")
+format_g723:value("auto", "Load as Required")
+format_g723.rmempty = true
+
+format_g726 = module:option(ListValue, "format_g726", "Raw G.726 (16/24/32/40kbps) data", "")
+format_g726:value("yes", "Load")
+format_g726:value("no", "Do Not Load")
+format_g726:value("auto", "Load as Required")
+format_g726.rmempty = true
+
+format_g729 = module:option(ListValue, "format_g729", "Raw G729 data", "")
+format_g729:value("yes", "Load")
+format_g729:value("no", "Do Not Load")
+format_g729:value("auto", "Load as Required")
+format_g729.rmempty = true
+
+format_gsm = module:option(ListValue, "format_gsm", "Raw GSM data", "")
+format_gsm:value("yes", "Load")
+format_gsm:value("no", "Do Not Load")
+format_gsm:value("auto", "Load as Required")
+format_gsm.rmempty = true
+
+format_h263 = module:option(ListValue, "format_h263", "Raw h263 data", "")
+format_h263:value("yes", "Load")
+format_h263:value("no", "Do Not Load")
+format_h263:value("auto", "Load as Required")
+format_h263.rmempty = true
+
+format_jpeg = module:option(ListValue, "format_jpeg", "JPEG (Joint Picture Experts Group) Image", "")
+format_jpeg:value("yes", "Load")
+format_jpeg:value("no", "Do Not Load")
+format_jpeg:value("auto", "Load as Required")
+format_jpeg.rmempty = true
+
+format_pcm = module:option(ListValue, "format_pcm", "Raw uLaw 8khz Audio support (PCM)", "")
+format_pcm:value("yes", "Load")
+format_pcm:value("no", "Do Not Load")
+format_pcm:value("auto", "Load as Required")
+format_pcm.rmempty = true
+
+format_pcm_alaw = module:option(ListValue, "format_pcm_alaw", "load => .so ; Raw aLaw 8khz PCM Audio support", "")
+format_pcm_alaw:value("yes", "Load")
+format_pcm_alaw:value("no", "Do Not Load")
+format_pcm_alaw:value("auto", "Load as Required")
+format_pcm_alaw.rmempty = true
+
+format_sln = module:option(ListValue, "format_sln", "Raw Signed Linear Audio support (SLN)", "")
+format_sln:value("yes", "Load")
+format_sln:value("no", "Do Not Load")
+format_sln:value("auto", "Load as Required")
+format_sln.rmempty = true
+
+format_vox = module:option(ListValue, "format_vox", "Dialogic VOX (ADPCM) File Format", "")
+format_vox:value("yes", "Load")
+format_vox:value("no", "Do Not Load")
+format_vox:value("auto", "Load as Required")
+format_vox.rmempty = true
+
+format_wav = module:option(ListValue, "format_wav", "Microsoft WAV format (8000hz Signed Line", "")
+format_wav:value("yes", "Load")
+format_wav:value("no", "Do Not Load")
+format_wav:value("auto", "Load as Required")
+format_wav.rmempty = true
+
+format_wav_gsm = module:option(ListValue, "format_wav_gsm", "Microsoft WAV format (Proprietary GSM)", "")
+format_wav_gsm:value("yes", "Load")
+format_wav_gsm:value("no", "Do Not Load")
+format_wav_gsm:value("auto", "Load as Required")
+format_wav_gsm.rmempty = true
+
+
+return cbimap

+ 40 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-func.lua

@@ -0,0 +1,40 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+module = cbimap:section(TypedSection, "module", "Modules", "")
+module.anonymous = true
+
+func_callerid = module:option(ListValue, "func_callerid", "Caller ID related dialplan functions", "")
+func_callerid:value("yes", "Load")
+func_callerid:value("no", "Do Not Load")
+func_callerid:value("auto", "Load as Required")
+func_callerid.rmempty = true
+
+func_enum = module:option(ListValue, "func_enum", "ENUM Functions", "")
+func_enum:value("yes", "Load")
+func_enum:value("no", "Do Not Load")
+func_enum:value("auto", "Load as Required")
+func_enum.rmempty = true
+
+func_uri = module:option(ListValue, "func_uri", "URI encoding / decoding functions", "")
+func_uri:value("yes", "Load")
+func_uri:value("no", "Do Not Load")
+func_uri:value("auto", "Load as Required")
+func_uri.rmempty = true
+
+
+return cbimap

+ 64 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-pbx.lua

@@ -0,0 +1,64 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+module = cbimap:section(TypedSection, "module", "Modules", "")
+module.anonymous = true
+
+pbx_ael = module:option(ListValue, "pbx_ael", "Asterisk Extension Language Compiler", "")
+pbx_ael:value("yes", "Load")
+pbx_ael:value("no", "Do Not Load")
+pbx_ael:value("auto", "Load as Required")
+pbx_ael.rmempty = true
+
+pbx_config = module:option(ListValue, "pbx_config", "Text Extension Configuration", "")
+pbx_config:value("yes", "Load")
+pbx_config:value("no", "Do Not Load")
+pbx_config:value("auto", "Load as Required")
+pbx_config.rmempty = true
+
+pbx_functions = module:option(ListValue, "pbx_functions", "load => .so ; Builtin dialplan functions", "")
+pbx_functions:value("yes", "Load")
+pbx_functions:value("no", "Do Not Load")
+pbx_functions:value("auto", "Load as Required")
+pbx_functions.rmempty = true
+
+pbx_loopback = module:option(ListValue, "pbx_loopback", "Loopback Switch", "")
+pbx_loopback:value("yes", "Load")
+pbx_loopback:value("no", "Do Not Load")
+pbx_loopback:value("auto", "Load as Required")
+pbx_loopback.rmempty = true
+
+pbx_realtime = module:option(ListValue, "pbx_realtime", "Realtime Switch", "")
+pbx_realtime:value("yes", "Load")
+pbx_realtime:value("no", "Do Not Load")
+pbx_realtime:value("auto", "Load as Required")
+pbx_realtime.rmempty = true
+
+pbx_spool = module:option(ListValue, "pbx_spool", "Outgoing Spool Support", "")
+pbx_spool:value("yes", "Load")
+pbx_spool:value("no", "Do Not Load")
+pbx_spool:value("auto", "Load as Required")
+pbx_spool.rmempty = true
+
+pbx_wilcalu = module:option(ListValue, "pbx_wilcalu", "Wil Cal U (Auto Dialer)", "")
+pbx_wilcalu:value("yes", "Load")
+pbx_wilcalu:value("no", "Do Not Load")
+pbx_wilcalu:value("auto", "Load as Required")
+pbx_wilcalu.rmempty = true
+
+
+return cbimap

+ 111 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-res-feature.lua

@@ -0,0 +1,111 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+featuremap = cbimap:section(TypedSection, "featuremap", "Feature Key maps", "")
+featuremap.anonymous = true
+featuremap.addremove = true
+
+atxfer = featuremap:option(Value, "atxfer", "Attended transfer key", "")
+atxfer.rmempty = true
+
+blindxfer = featuremap:option(Value, "blindxfer", "Blind transfer key", "")
+blindxfer.rmempty = true
+
+disconnect = featuremap:option(Value, "disconnect", "Key to Disconnect call", "")
+disconnect.rmempty = true
+
+parkcall = featuremap:option(Value, "parkcall", "Key to Park call", "")
+parkcall.rmempty = true
+
+
+featurepark = cbimap:section(TypedSection, "featurepark", "Parking Feature", "")
+featurepark.anonymous = true
+
+parkenabled = featurepark:option(Flag, "parkenabled", "Enable Parking", "")
+
+adsipark = featurepark:option(Flag, "adsipark", "ADSI Park", "")
+adsipark.rmempty = true
+adsipark:depends({ parkenabled = "1" })
+
+atxfernoanswertimeout = featurepark:option(Value, "atxfernoanswertimeout", "Attended transfer timeout (sec)", "")
+atxfernoanswertimeout.rmempty = true
+atxfernoanswertimeout:depends({ parkenabled = "1" })
+
+automon = featurepark:option(Value, "automon", "One touch record key", "")
+automon.rmempty = true
+automon:depends({ parkenabled = "1" })
+
+context = featurepark:option(Value, "context", "Name of call context for parking", "")
+context.rmempty = true
+context:depends({ parkenabled = "1" })
+
+courtesytone = featurepark:option(Value, "courtesytone", "Sound file to play to parked caller", "")
+courtesytone.rmempty = true
+courtesytone:depends({ parkenabled = "1" })
+
+featuredigittimeout = featurepark:option(Value, "featuredigittimeout", "Max time (ms) between digits for feature activation", "")
+featuredigittimeout.rmempty = true
+featuredigittimeout:depends({ parkenabled = "1" })
+
+findslot = featurepark:option(ListValue, "findslot", "Method to Find Parking slot", "")
+findslot:value("first", "First available slot")
+findslot:value("next", "Next free parking space")
+findslot.rmempty = true
+findslot:depends({ parkenabled = "1" })
+
+parkedmusicclass = featurepark:option(ListValue, "parkedmusicclass", "Music on Hold class for the parked channel", "")
+parkedmusicclass.titleref = luci.dispatcher.build_url( "admin", "services", "asterisk" )
+parkedmusicclass:depends({ parkenabled = "1" })
+cbimap.uci:foreach( "asterisk", "moh", function(s) parkedmusicclass:value(s['.name']) end )
+
+parkedplay = featurepark:option(ListValue, "parkedplay", "Play courtesy tone to", "")
+parkedplay:value("caller", "Caller")
+parkedplay:value("parked", "Parked user")
+parkedplay:value("both", "Both")
+parkedplay.rmempty = true
+parkedplay:depends({ parkenabled = "1" })
+
+parkext = featurepark:option(Value, "parkext", "Extension to dial to park", "")
+parkext.rmempty = true
+parkext:depends({ parkenabled = "1" })
+
+parkingtime = featurepark:option(Value, "parkingtime", "Parking time (secs)", "")
+parkingtime.rmempty = true
+parkingtime:depends({ parkenabled = "1" })
+
+parkpos = featurepark:option(Value, "parkpos", "Range of extensions for call parking", "")
+parkpos.rmempty = true
+parkpos:depends({ parkenabled = "1" })
+
+pickupexten = featurepark:option(Value, "pickupexten", "Pickup extension", "")
+pickupexten.rmempty = true
+pickupexten:depends({ parkenabled = "1" })
+
+transferdigittimeout = featurepark:option(Value, "transferdigittimeout", "Seconds to wait bewteen digits when transferring", "")
+transferdigittimeout.rmempty = true
+transferdigittimeout:depends({ parkenabled = "1" })
+
+xferfailsound = featurepark:option(Value, "xferfailsound", "sound when attended transfer is complete", "")
+xferfailsound.rmempty = true
+xferfailsound:depends({ parkenabled = "1" })
+
+xfersound = featurepark:option(Value, "xfersound", "Sound when attended transfer fails", "")
+xfersound.rmempty = true
+xfersound:depends({ parkenabled = "1" })
+
+
+return cbimap

+ 88 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-mod-res.lua

@@ -0,0 +1,88 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+module = cbimap:section(TypedSection, "module", "Modules", "")
+module.anonymous = true
+
+res_config_mysql = module:option(ListValue, "res_config_mysql", "MySQL Config Resource", "")
+res_config_mysql:value("yes", "Load")
+res_config_mysql:value("no", "Do Not Load")
+res_config_mysql:value("auto", "Load as Required")
+res_config_mysql.rmempty = true
+
+res_config_odbc = module:option(ListValue, "res_config_odbc", "ODBC Config Resource", "")
+res_config_odbc:value("yes", "Load")
+res_config_odbc:value("no", "Do Not Load")
+res_config_odbc:value("auto", "Load as Required")
+res_config_odbc.rmempty = true
+
+res_config_pgsql = module:option(ListValue, "res_config_pgsql", "PGSQL Module", "")
+res_config_pgsql:value("yes", "Load")
+res_config_pgsql:value("no", "Do Not Load")
+res_config_pgsql:value("auto", "Load as Required")
+res_config_pgsql.rmempty = true
+
+res_crypto = module:option(ListValue, "res_crypto", "Cryptographic Digital Signatures", "")
+res_crypto:value("yes", "Load")
+res_crypto:value("no", "Do Not Load")
+res_crypto:value("auto", "Load as Required")
+res_crypto.rmempty = true
+
+res_features = module:option(ListValue, "res_features", "Call Parking Resource", "")
+res_features:value("yes", "Load")
+res_features:value("no", "Do Not Load")
+res_features:value("auto", "Load as Required")
+res_features.rmempty = true
+
+res_indications = module:option(ListValue, "res_indications", "Indications Configuration", "")
+res_indications:value("yes", "Load")
+res_indications:value("no", "Do Not Load")
+res_indications:value("auto", "Load as Required")
+res_indications.rmempty = true
+
+res_monitor = module:option(ListValue, "res_monitor", "Call Monitoring Resource", "")
+res_monitor:value("yes", "Load")
+res_monitor:value("no", "Do Not Load")
+res_monitor:value("auto", "Load as Required")
+res_monitor.rmempty = true
+
+res_musiconhold = module:option(ListValue, "res_musiconhold", "Music On Hold Resource", "")
+res_musiconhold:value("yes", "Load")
+res_musiconhold:value("no", "Do Not Load")
+res_musiconhold:value("auto", "Load as Required")
+res_musiconhold.rmempty = true
+
+res_odbc = module:option(ListValue, "res_odbc", "ODBC Resource", "")
+res_odbc:value("yes", "Load")
+res_odbc:value("no", "Do Not Load")
+res_odbc:value("auto", "Load as Required")
+res_odbc.rmempty = true
+
+res_smdi = module:option(ListValue, "res_smdi", "SMDI Module", "")
+res_smdi:value("yes", "Load")
+res_smdi:value("no", "Do Not Load")
+res_smdi:value("auto", "Load as Required")
+res_smdi.rmempty = true
+
+res_snmp = module:option(ListValue, "res_snmp", "SNMP Module", "")
+res_snmp:value("yes", "Load")
+res_snmp:value("no", "Do Not Load")
+res_snmp:value("auto", "Load as Required")
+res_snmp.rmempty = true
+
+
+return cbimap

+ 109 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-sip-connections.lua

@@ -0,0 +1,109 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+sip = cbimap:section(TypedSection, "sip", "SIP Connection", "")
+sip.addremove = true
+
+alwaysinternational = sip:option(Flag, "alwaysinternational", "Always Dial International", "")
+alwaysinternational.optional = true
+
+canreinvite = sip:option(ListValue, "canreinvite", "Reinvite/redirect media connections", "")
+canreinvite:value("yes", "Yes")
+canreinvite:value("nonat", "Yes when not behind NAT")
+canreinvite:value("update", "Use UPDATE rather than INVITE for path redirection")
+canreinvite:value("no", "No")
+canreinvite.optional = true
+
+context = sip:option(ListValue, "context", "Context to use", "")
+context.titleref = luci.dispatcher.build_url( "admin", "services", "asterisk", "dialplans" )
+cbimap.uci:foreach( "asterisk", "dialplan", function(s) context:value(s['.name']) end )
+cbimap.uci:foreach( "asterisk", "dialzone", function(s) context:value(s['.name']) end )
+
+countrycode = sip:option(Value, "countrycode", "Country Code for connection", "")
+countrycode.optional = true
+
+dtmfmode = sip:option(ListValue, "dtmfmode", "DTMF mode", "")
+dtmfmode:value("info", "Use RFC2833 or INFO for the BudgeTone")
+dtmfmode:value("rfc2833", "Use RFC2833 for the BudgeTone")
+dtmfmode:value("inband", "Use Inband (only with ulaw/alaw)")
+dtmfmode.optional = true
+
+extension = sip:option(Value, "extension", "Add as Extension", "")
+extension.optional = true
+
+fromdomain = sip:option(Value, "fromdomain", "Primary domain identity for From: headers", "")
+fromdomain.optional = true
+
+fromuser = sip:option(Value, "fromuser", "From user (required by many SIP providers)", "")
+fromuser.optional = true
+
+host = sip:option(Value, "host", "Host name (or blank)", "")
+host.optional = true
+
+incoming = sip:option(DynamicList, "incoming", "Ring on incoming dialplan contexts", "")
+incoming.optional = true
+
+insecure = sip:option(ListValue, "insecure", "Allow Insecure for", "")
+insecure:value("port", "Allow mismatched port number")
+insecure:value("invite", "Do not require auth of incoming INVITE")
+insecure:value("port,invite", "Allow mismatched port and Do not require auth of incoming INVITE")
+insecure.optional = true
+
+internationalprefix = sip:option(Value, "internationalprefix", "International Dial Prefix", "")
+internationalprefix.optional = true
+
+mailbox = sip:option(Value, "mailbox", "Mailbox for MWI", "")
+mailbox.optional = true
+
+nat = sip:option(Flag, "nat", "NAT between phone and Asterisk", "")
+nat.optional = true
+
+pedantic = sip:option(Flag, "pedantic", "Check tags in headers", "")
+pedantic.optional = true
+
+port = sip:option(Value, "port", "SIP Port", "")
+port.optional = true
+
+prefix = sip:option(Value, "prefix", "Dial Prefix (for external line)", "")
+prefix.optional = true
+
+qualify = sip:option(Value, "qualify", "Reply Timeout (ms) for down connection", "")
+qualify.optional = true
+
+register = sip:option(Flag, "register", "Register connection", "")
+register.optional = true
+
+secret = sip:option(Value, "secret", "Secret", "")
+secret.optional = true
+
+selfmailbox = sip:option(Flag, "selfmailbox", "Dial own extension for mailbox", "")
+selfmailbox.optional = true
+
+timeout = sip:option(Value, "timeout", "Dial Timeout (sec)", "")
+timeout.optional = true
+
+type = sip:option(ListValue, "type", "Client Type", "")
+type:value("friend", "Friend (outbound/inbound)")
+type:value("user", "User (inbound - authenticate by \"from\")")
+type:value("peer", "Peer (outbound - match by host)")
+type.optional = true
+
+username = sip:option(Value, "username", "Username", "")
+username.optional = true
+
+
+return cbimap

+ 52 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk-voice.lua

@@ -0,0 +1,52 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+voicegeneral = cbimap:section(TypedSection, "voicegeneral", "Voicemail general options", "")
+
+serveremail = voicegeneral:option(Value, "serveremail", "From Email address of server", "")
+
+
+voicemail = cbimap:section(TypedSection, "voicemail", "Voice Mail boxes", "")
+voicemail.addremove = true
+
+attach = voicemail:option(Flag, "attach", "Email contains attachment", "")
+attach.rmempty = true
+
+email = voicemail:option(Value, "email", "Email", "")
+email.rmempty = true
+
+name = voicemail:option(Value, "name", "Display Name", "")
+name.rmempty = true
+
+password = voicemail:option(Value, "password", "Password", "")
+password.rmempty = true
+
+zone = voicemail:option(ListValue, "zone", "Voice Zone", "")
+cbimap.uci:foreach( "asterisk", "voicezone", function(s) zone:value(s['.name']) end )
+
+
+voicezone = cbimap:section(TypedSection, "voicezone", "Voice Zone settings", "")
+voicezone.addremove = true
+
+message = voicezone:option(Value, "message", "Message Format", "")
+message.rmempty = true
+
+zone = voicezone:option(Value, "zone", "Time Zone", "")
+zone.rmempty = true
+
+
+return cbimap

+ 162 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk.lua

@@ -0,0 +1,162 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "asterisk", "")
+
+asterisk = cbimap:section(TypedSection, "asterisk", "Asterisk General Options", "")
+asterisk.anonymous = true
+
+agidir = asterisk:option(Value, "agidir", "AGI directory", "")
+agidir.rmempty = true
+
+cache_record_files = asterisk:option(Flag, "cache_record_files", "Cache recorded sound files during recording", "")
+cache_record_files.rmempty = true
+
+debug = asterisk:option(Value, "debug", "Debug Level", "")
+debug.rmempty = true
+
+dontwarn = asterisk:option(Flag, "dontwarn", "Disable some warnings", "")
+dontwarn.rmempty = true
+
+dumpcore = asterisk:option(Flag, "dumpcore", "Dump core on crash", "")
+dumpcore.rmempty = true
+
+highpriority = asterisk:option(Flag, "highpriority", "High Priority", "")
+highpriority.rmempty = true
+
+initcrypto = asterisk:option(Flag, "initcrypto", "Initialise Crypto", "")
+initcrypto.rmempty = true
+
+internal_timing = asterisk:option(Flag, "internal_timing", "Use Internal Timing", "")
+internal_timing.rmempty = true
+
+logdir = asterisk:option(Value, "logdir", "Log directory", "")
+logdir.rmempty = true
+
+maxcalls = asterisk:option(Value, "maxcalls", "Maximum number of calls allowed", "")
+maxcalls.rmempty = true
+
+maxload = asterisk:option(Value, "maxload", "Maximum load to stop accepting new calls", "")
+maxload.rmempty = true
+
+nocolor = asterisk:option(Flag, "nocolor", "Disable console colors", "")
+nocolor.rmempty = true
+
+record_cache_dir = asterisk:option(Value, "record_cache_dir", "Sound files Cache directory", "")
+record_cache_dir.rmempty = true
+record_cache_dir:depends({ ["cache_record_files"] = "true" })
+
+rungroup = asterisk:option(Flag, "rungroup", "The Group to run as", "")
+rungroup.rmempty = true
+
+runuser = asterisk:option(Flag, "runuser", "The User to run as", "")
+runuser.rmempty = true
+
+spooldir = asterisk:option(Value, "spooldir", "Voicemail Spool directory", "")
+spooldir.rmempty = true
+
+systemname = asterisk:option(Value, "systemname", "Prefix UniquID with system name", "")
+systemname.rmempty = true
+
+transcode_via_sln = asterisk:option(Flag, "transcode_via_sln", "Build transcode paths via SLINEAR, not directly", "")
+transcode_via_sln.rmempty = true
+
+transmit_silence_during_record = asterisk:option(Flag, "transmit_silence_during_record", "Transmit SLINEAR silence while recording a channel", "")
+transmit_silence_during_record.rmempty = true
+
+verbose = asterisk:option(Value, "verbose", "Verbose Level", "")
+verbose.rmempty = true
+
+zone = asterisk:option(Value, "zone", "Time Zone", "")
+zone.rmempty = true
+
+
+hardwarereboot = cbimap:section(TypedSection, "hardwarereboot", "Reload Hardware Config", "")
+
+method = hardwarereboot:option(ListValue, "method", "Reboot Method", "")
+method:value("web", "Web URL (wget)")
+method:value("system", "program to run")
+method.rmempty = true
+
+param = hardwarereboot:option(Value, "param", "Parameter", "")
+param.rmempty = true
+
+
+iaxgeneral = cbimap:section(TypedSection, "iaxgeneral", "IAX General Options", "")
+iaxgeneral.anonymous = true
+iaxgeneral.addremove = true
+
+allow = iaxgeneral:option(MultiValue, "allow", "Allow Codecs", "")
+allow:value("alaw", "alaw")
+allow:value("gsm", "gsm")
+allow:value("g726", "g726")
+allow.rmempty = true
+
+canreinvite = iaxgeneral:option(ListValue, "canreinvite", "Reinvite/redirect media connections", "")
+canreinvite:value("yes", "Yes")
+canreinvite:value("nonat", "Yes when not behind NAT")
+canreinvite:value("update", "Use UPDATE rather than INVITE for path redirection")
+canreinvite:value("no", "No")
+canreinvite.rmempty = true
+
+static = iaxgeneral:option(Flag, "static", "Static", "")
+static.rmempty = true
+
+writeprotect = iaxgeneral:option(Flag, "writeprotect", "Write Protect", "")
+writeprotect.rmempty = true
+
+
+sipgeneral = cbimap:section(TypedSection, "sipgeneral", "Section sipgeneral", "")
+sipgeneral.anonymous = true
+sipgeneral.addremove = true
+
+allow = sipgeneral:option(MultiValue, "allow", "Allow codecs", "")
+allow:value("ulaw", "ulaw")
+allow:value("alaw", "alaw")
+allow:value("gsm", "gsm")
+allow:value("g726", "g726")
+allow.rmempty = true
+
+port = sipgeneral:option(Value, "port", "SIP Port", "")
+port.rmempty = true
+
+realm = sipgeneral:option(Value, "realm", "SIP realm", "")
+realm.rmempty = true
+
+
+moh = cbimap:section(TypedSection, "moh", "Music On Hold", "")
+
+application = moh:option(Value, "application", "Application", "")
+application.rmempty = true
+application:depends({ ["asterisk.moh.mode"] = "custom" })
+
+directory = moh:option(Value, "directory", "Directory of Music", "")
+directory.rmempty = true
+
+mode = moh:option(ListValue, "mode", "Option mode", "")
+mode:value("system", "program to run")
+mode:value("files", "Read files from directory")
+mode:value("quietmp3", "Quite MP3")
+mode:value("mp3", "Loud MP3")
+mode:value("mp3nb", "unbuffered MP3")
+mode:value("quietmp3nb", "Quiet Unbuffered MP3")
+mode:value("custom", "Run a custom application")
+mode.rmempty = true
+
+random = moh:option(Flag, "random", "Random Play", "")
+random.rmempty = true
+
+
+return cbimap

+ 137 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/dialplan_out.lua

@@ -0,0 +1,137 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+]]--
+
+local ast = require("luci.asterisk")
+
+local function find_outgoing_contexts(uci)
+	local c = { }
+	local h = { }
+
+--	uci:foreach("asterisk", "dialplan",
+--		function(s)
+--			if not h[s['.name']] then
+--				c[#c+1] = { s['.name'], "Dialplan: %s" % s['.name'] }
+--				h[s['.name']] = true
+--			end
+--		end)
+
+	uci:foreach("asterisk", "dialzone",
+		function(s)
+			if not h[s['.name']] then
+				c[#c+1] = { s['.name'], "Dialzone: %s" % s['.name'] }
+				h[s['.name']] = true
+			end
+		end)
+
+	return c
+end
+
+local function find_incoming_contexts(uci)
+	local c = { }
+	local h = { }
+
+	uci:foreach("asterisk", "sip",
+		function(s)
+			if s.context and not h[s.context] and
+			   uci:get_bool("asterisk", s['.name'], "provider")
+			then
+				c[#c+1] = { s.context, "Incoming: %s" % s['.name'] or s.context }
+				h[s.context] = true
+			end
+		end)
+
+	return c
+end
+
+local function find_trunks(uci)
+	local t = { }
+
+	uci:foreach("asterisk", "sip",
+		function(s)
+			if uci:get_bool("asterisk", s['.name'], "provider") then
+				t[#t+1] = {
+					"SIP/%s" % s['.name'],
+					"SIP: %s" % s['.name']
+				}
+			end
+		end)
+
+	uci:foreach("asterisk", "iax",
+		function(s)
+			t[#t+1] = {
+				"IAX/%s" % s['.name'],
+				"IAX: %s" % s.extension or s['.name']
+			}
+		end)
+
+	return t
+end
+
+--[[
+
+dialzone {name} - Outgoing zone.
+	uses          - Outgoing line to use: TYPE/Name
+	match (list)  - Number to match
+	countrycode   - The effective country code of this dialzone
+	international (list) - International prefix to match
+	localzone     - dialzone for local numbers
+	addprefix     - Prexix required to dial out.
+	localprefix   - Prefix for a local call
+
+]]
+
+
+--
+-- SIP dialzone configuration
+--
+if arg[1] then
+	cbimap = Map("asterisk", "Edit Dialplan Entry")
+
+	entry = cbimap:section(NamedSection, arg[1])
+
+	back = entry:option(DummyValue, "_overview", "Back to dialplan overview")
+	back.value = ""
+	back.titleref = luci.dispatcher.build_url("admin", "asterisk", "dialplans")
+
+	desc = entry:option(Value, "description", "Description")
+	function desc.cfgvalue(self, s, ...)
+		return Value.cfgvalue(self, s, ...) or s
+	end
+
+	match = entry:option(DynamicList, "match", "Number matches")
+
+	intl = entry:option(DynamicList, "international", "Intl. prefix matches (optional)")
+
+	trunk = entry:option(MultiValue, "uses", "Used trunk")
+	for _, v in ipairs(find_trunks(cbimap.uci)) do
+		trunk:value(unpack(v))
+	end
+
+	aprefix = entry:option(Value, "addprefix", "Add prefix to dial out (optional)")
+	--ast.idd.cbifill(aprefix)
+
+	ccode = entry:option(Value, "countrycode", "Effective countrycode (optional)")
+	ast.cc.cbifill(ccode)
+
+	lzone = entry:option(ListValue, "localzone", "Dialzone for local numbers")
+	lzone:value("", "no special treatment of local numbers")
+	for _, v in ipairs(find_outgoing_contexts(cbimap.uci)) do
+		lzone:value(unpack(v))
+	end
+
+	lprefix = entry:option(Value, "localprefix", "Prefix for local calls (optional)")
+
+	return cbimap
+end

+ 115 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/dialplans.lua

@@ -0,0 +1,115 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+]]--
+
+local ast = require("luci.asterisk")
+
+cbimap = Map("asterisk", "Registered Trunks")
+cbimap.pageaction = false
+
+local sip_peers = { }
+cbimap.uci:foreach("asterisk", "sip",
+	function(s)
+		if s.type == "peer" then
+			s.name = s['.name']
+			s.info = ast.sip.peer(s.name)
+			sip_peers[s.name] = s
+		end
+	end)
+
+
+sip_table = cbimap:section(TypedSection, "sip", "SIP Trunks")
+sip_table.template    = "cbi/tblsection"
+sip_table.extedit     = luci.dispatcher.build_url("admin", "asterisk", "trunks", "sip", "%s")
+sip_table.addremove   = true
+sip_table.sectionhead = "Extension"
+
+function sip_table.filter(self, s)
+	return s and (
+		cbimap.uci:get("asterisk", s, "type") == nil or
+		cbimap.uci:get_bool("asterisk", s, "provider")
+	)
+end
+
+function sip_table.create(self, section)
+	if TypedSection.create(self, section) then
+		created = section
+	else
+		self.invalid_cts = true
+	end
+end
+
+function sip_table.parse(self, ...)
+	TypedSection.parse(self, ...)
+	if created then
+		cbimap.uci:tset("asterisk", created, {
+			type     = "friend",
+			qualify  = "yes",
+			provider = "yes"
+		})
+
+		cbimap.uci:save("asterisk")
+		luci.http.redirect(luci.dispatcher.build_url(
+			"admin", "asterisk", "trunks", "sip", created
+		))
+	end
+end
+
+
+user = sip_table:option(DummyValue, "username", "Username")
+
+host = sip_table:option(DummyValue, "host", "Hostname")
+function host.cfgvalue(self, s)
+	if sip_peers[s] and sip_peers[s].info.address then
+		return "%s:%i" %{ sip_peers[s].info.address, sip_peers[s].info.port }
+	else
+		return "n/a"
+	end
+end
+
+context = sip_table:option(DummyValue, "context", "Dialplan")
+context.href = luci.dispatcher.build_url("admin", "asterisk", "dialplan")
+function context.cfgvalue(...)
+	return AbstractValue.cfgvalue(...) or "(default)"
+end
+
+online = sip_table:option(DummyValue, "online", "Registered")
+function online.cfgvalue(self, s)
+	if sip_peers[s] and sip_peers[s].info.online == nil then
+		return "n/a"
+	else
+		return sip_peers[s] and sip_peers[s].info.online
+			and "yes" or "no (%s)" %{
+				sip_peers[s] and sip_peers[s].info.Status:lower() or "unknown"
+			}
+	end
+end
+
+delay = sip_table:option(DummyValue, "delay", "Delay")
+function delay.cfgvalue(self, s)
+	if sip_peers[s] and sip_peers[s].info.online then
+		return "%i ms" % sip_peers[s].info.delay
+	else
+		return "n/a"
+	end
+end
+
+info = sip_table:option(Button, "_info", "Info")
+function info.write(self, s)
+	luci.http.redirect(luci.dispatcher.build_url(
+		"admin", "asterisk", "trunks", "sip", s, "info"
+	))
+end
+
+return cbimap

+ 135 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/dialzones.lua

@@ -0,0 +1,135 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id: trunks.lua 4025 2009-01-11 23:37:21Z jow $
+
+]]--
+
+local ast = require("luci.asterisk")
+local uci = require("luci.model.uci").cursor()
+
+--[[
+	Dialzone overview table
+]]
+
+if not arg[1] then
+	zonemap = Map("asterisk", "Dial Zones", [[
+		Dial zones hold patterns of dialed numbers to match.
+		Each zone has one or more trunks assigned. If the first trunk is
+		congested, Asterisk will try to use the next available connection.
+		If all trunks fail, then the following zones in the parent dialplan
+		are tried.
+	]])
+
+	local zones, znames = ast.dialzone.zones()
+
+	zonetbl = zonemap:section(Table, zones, "Zone Overview")
+	zonetbl.sectionhead = "Zone"
+	zonetbl.addremove   = true
+	zonetbl.anonymous   = false
+	zonetbl.extedit     = luci.dispatcher.build_url(
+		"admin", "asterisk", "dialplans", "zones", "%s"
+	)
+
+	function zonetbl.cfgsections(self)
+		return znames
+	end
+
+	function zonetbl.parse(self)
+		for k, v in pairs(self.map:formvaluetable(
+			luci.cbi.REMOVE_PREFIX .. self.config
+		) or {}) do
+			if k:sub(-2) == ".x" then k = k:sub(1, #k - 2) end
+			uci:delete("asterisk", k)
+			uci:save("asterisk")
+			self.data[k] = nil
+			for i = 1,#znames do
+				if znames[i] == k then
+					table.remove(znames, i)
+					break
+				end
+			end
+		end
+
+		Table.parse(self)
+	end
+
+	zonetbl:option(DummyValue, "description", "Description")
+	zonetbl:option(DummyValue, "addprefix")
+
+	match = zonetbl:option(DummyValue, "matches")
+	function match.cfgvalue(self, s)
+		return table.concat(zones[s].matches, ", ")
+	end
+
+	trunks = zonetbl:option(DummyValue, "trunk")
+	trunks.template = "asterisk/cbi/cell"
+	function trunks.cfgvalue(self, s)
+		return ast.tools.hyperlinks(zones[s].trunks)
+	end
+
+	return zonemap
+
+--[[
+	Zone edit form
+]]
+
+else
+	zoneedit = Map("asterisk", "Edit Dialzone")
+
+	entry = zoneedit:section(NamedSection, arg[1])
+	entry.title = "Zone %q" % arg[1];
+
+	back = entry:option(DummyValue, "_overview", "Back to dialzone overview")
+	back.value = ""
+	back.titleref = luci.dispatcher.build_url(
+		"admin", "asterisk", "dialplans", "zones"
+	)
+
+	desc = entry:option(Value, "description", "Description")
+	function desc.cfgvalue(self, s, ...)
+		return Value.cfgvalue(self, s, ...) or s
+	end
+
+	trunks = entry:option(MultiValue, "uses", "Used trunks")
+	trunks.widget = "checkbox"
+	uci:foreach("asterisk", "sip",
+		function(s)
+			if s.provider == "yes" then
+				trunks:value(
+					"SIP/%s" % s['.name'],
+					"SIP/%s (%s)" %{ s['.name'], s.host or 'n/a' }
+				)
+			end
+		end)
+
+
+	match = entry:option(DynamicList, "match", "Number matches")
+
+	intl = entry:option(DynamicList, "international", "Intl. prefix matches (optional)")
+
+	aprefix = entry:option(Value, "addprefix", "Add prefix to dial out (optional)")
+	ccode = entry:option(Value, "countrycode", "Effective countrycode (optional)")
+
+	lzone = entry:option(ListValue, "localzone", "Dialzone for local numbers")
+	lzone:value("", "no special treatment of local numbers")
+	for _, z in ipairs(ast.dialzone.zones()) do
+		lzone:value(z.name, "%q (%s)" %{ z.name, z.description })
+	end
+	--for _, v in ipairs(find_outgoing_contexts(zoneedit.uci)) do
+	--	lzone:value(unpack(v))
+	--end
+
+	lprefix = entry:option(Value, "localprefix", "Prefix for local calls (optional)")
+
+	return zoneedit
+
+end

+ 49 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/meetme.lua

@@ -0,0 +1,49 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2009 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+local ast = require "luci.asterisk"
+
+cbimap = Map("asterisk", "MeetMe - Rooms")
+
+meetme = cbimap:section(TypedSection, "meetme", "MeetMe Rooms")
+meetme.addremove = true
+meetme.anonymous = true
+meetme.template = "cbi/tblsection"
+meetme:option(Value, "_description", "Description", "Short room description")
+
+room = meetme:option(Value, "room", "Room Number", "Unique room identifier")
+
+function room.write(self, s, val)
+	if val and #val > 0 then
+		local old = self:cfgvalue(s)
+		self.map.uci:foreach("asterisk", "dialplanmeetme",
+			function(v)
+				if v.room == old then
+					self.map:set(v['.name'], "room", val)
+				end
+			end)
+		Value.write(self, s, val)
+	end
+end
+
+
+meetme:option(Value, "pin", "PIN", "PIN required to access")
+meetme:option(Value, "adminpin", "Admin PIN", "PIN required for administration")
+
+function meetme.remove(self, s)
+	return ast.meetme.remove(self.map:get(s, "room"), self.map.uci)
+end
+
+
+return cbimap

+ 28 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/meetme_settings.lua

@@ -0,0 +1,28 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2009 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+cbimap = Map("asterisk", "MeetMe - Common Settings",
+	"Common settings for MeetMe phone conferences.")
+
+meetme = cbimap:section(TypedSection, "meetmegeneral", "General MeetMe Options")
+meetme.addremove = false
+meetme.anonymous = true
+
+audiobuffers = meetme:option(ListValue, "audiobuffers",
+	"Number of 20ms audio buffers to use for conferences")
+
+for i = 2, 32 do audiobuffers:value(i) end
+
+
+return cbimap

+ 157 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/phone_sip.lua

@@ -0,0 +1,157 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+]]--
+
+local ast = require("luci.asterisk")
+
+local function find_outgoing_contexts(uci)
+	local c = { }
+	local h = { }
+
+	uci:foreach("asterisk", "dialplan",
+		function(s)
+			if not h[s['.name']] then
+				c[#c+1] = { s['.name'], "Dialplan: %s" % s['.name'] }
+				h[s['.name']] = true
+			end
+		end)
+
+	return c
+end
+
+local function find_incoming_contexts(uci)
+	local c = { }
+	local h = { }
+
+	uci:foreach("asterisk", "sip",
+		function(s)
+			if s.context and not h[s.context] and
+			   uci:get_bool("asterisk", s['.name'], "provider")
+			then
+				c[#c+1] = { s.context, "Incoming: %s" % s['.name'] or s.context }
+				h[s.context] = true
+			end
+		end)
+
+	return c
+end
+
+
+--
+-- SIP phone info
+--
+if arg[2] == "info" then
+	form = SimpleForm("asterisk", "SIP Phone Information")
+	form.reset  = false
+	form.submit = "Back to overview"
+
+	local info, keys = ast.sip.peer(arg[1])
+	local data = { }
+
+	for _, key in ipairs(keys) do
+		data[#data+1] = {
+			key = key,
+			val = type(info[key]) == "boolean"
+				and ( info[key] and "yes" or "no" )
+				or  ( info[key] == nil or #info[key] == 0 )
+					and "(none)"
+					or  tostring(info[key])
+		}
+	end
+
+	itbl = form:section(Table, data, "SIP Phone %q" % arg[1])
+	itbl:option(DummyValue, "key", "Key")
+	itbl:option(DummyValue, "val", "Value")
+
+	function itbl.parse(...)
+		luci.http.redirect(
+			luci.dispatcher.build_url("admin", "asterisk", "phones")
+		)
+	end
+
+	return form
+
+--
+-- SIP phone configuration
+--
+elseif arg[1] then
+	cbimap = Map("asterisk", "Edit SIP Client")
+
+	peer = cbimap:section(NamedSection, arg[1])
+	peer.hidden = {
+		type        = "friend",
+		qualify     = "yes",
+		host        = "dynamic",
+		nat         = "no",
+		canreinvite = "no"
+	}
+
+	back = peer:option(DummyValue, "_overview", "Back to phone overview")
+	back.value = ""
+	back.titleref = luci.dispatcher.build_url("admin", "asterisk", "phones")
+
+	active = peer:option(Flag, "disable", "Account enabled")
+	active.enabled  = "yes"
+	active.disabled = "no"
+	function active.cfgvalue(...)
+		return AbstractValue.cfgvalue(...) or "yes"
+	end
+
+	exten = peer:option(Value, "extension", "Extension Number")
+	cbimap.uci:foreach("asterisk", "dialplanexten",
+		function(s)
+			exten:value(
+				s.extension,
+				"%s (via %s/%s)" %{ s.extension, s.type:upper(), s.target }
+			)
+		end)
+
+	display = peer:option(Value, "callerid", "Display Name")
+
+	username  = peer:option(Value, "username", "Authorization ID")
+	password  = peer:option(Value, "secret", "Authorization Password")
+	password.password = true
+
+	regtimeout = peer:option(Value, "registertimeout", "Registration Time Value")
+	function regtimeout.cfgvalue(...)
+		return AbstractValue.cfgvalue(...) or "60"
+	end
+
+	sipport = peer:option(Value, "port", "SIP Port")
+	function sipport.cfgvalue(...)
+		return AbstractValue.cfgvalue(...) or "5060"
+	end
+
+	linekey = peer:option(ListValue, "_linekey", "Linekey Mode (broken)")
+	linekey:value("", "Off")
+	linekey:value("trunk", "Trunk Appearance")
+	linekey:value("call", "Call Appearance")
+
+	dialplan = peer:option(ListValue, "context", "Assign Dialplan")
+	dialplan.titleref = luci.dispatcher.build_url("admin", "asterisk", "dialplans")
+	for _, v in ipairs(find_outgoing_contexts(cbimap.uci)) do
+		dialplan:value(unpack(v))
+	end
+
+	incoming = peer:option(StaticList, "incoming", "Receive incoming calls from")
+	for _, v in ipairs(find_incoming_contexts(cbimap.uci)) do
+		incoming:value(unpack(v))
+	end
+
+	--function incoming.cfgvalue(...)
+		--error(table.concat(MultiValue.cfgvalue(...),"."))
+	--end
+
+	return cbimap
+end

+ 116 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/phones.lua

@@ -0,0 +1,116 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+]]--
+
+local ast = require("luci.asterisk")
+
+cbimap = Map("asterisk", "Registered Phones")
+cbimap.pageaction = false
+
+local sip_peers = { }
+cbimap.uci:foreach("asterisk", "sip",
+	function(s)
+		if s.type ~= "peer" then
+			s.name = s['.name']
+			s.info = ast.sip.peer(s.name)
+			sip_peers[s.name] = s
+		end
+	end)
+
+
+sip_table = cbimap:section(TypedSection, "sip", "SIP Phones")
+sip_table.template  = "cbi/tblsection"
+sip_table.extedit   = luci.dispatcher.build_url("admin", "asterisk", "phones", "sip", "%s")
+sip_table.addremove = true
+
+function sip_table.filter(self, s)
+	return s and not cbimap.uci:get_bool("asterisk", s, "provider")
+end
+
+function sip_table.create(self, section)
+	if TypedSection.create(self, section) then
+		created = section
+		cbimap.uci:tset("asterisk", section, {
+			type        = "friend",
+			qualify     = "yes",
+			provider    = "no",
+			host        = "dynamic",
+			nat         = "no",
+			canreinvite = "no",
+			extension   = section:match("^%d+$") and section or "",
+			username    = section:match("^%d+$") and section or ""
+		})
+	else
+		self.invalid_cts = true
+	end
+end
+
+function sip_table.parse(self, ...)
+	TypedSection.parse(self, ...)
+	if created then
+		cbimap.uci:save("asterisk")
+		luci.http.redirect(luci.dispatcher.build_url(
+			"admin", "asterisk", "phones", "sip", created
+		))
+	end
+end
+
+
+user = sip_table:option(DummyValue, "username", "Username")
+function user.cfgvalue(self, s)
+	return sip_peers[s] and sip_peers[s].callerid or
+		AbstractValue.cfgvalue(self, s)
+end
+
+host = sip_table:option(DummyValue, "host", "Hostname")
+function host.cfgvalue(self, s)
+	if sip_peers[s] and sip_peers[s].info.address then
+		return "%s:%i" %{ sip_peers[s].info.address, sip_peers[s].info.port }
+	else
+		return "n/a"
+	end
+end
+
+context = sip_table:option(DummyValue, "context", "Dialplan")
+context.href = luci.dispatcher.build_url("admin", "asterisk", "dialplan")
+
+online = sip_table:option(DummyValue, "online", "Registered")
+function online.cfgvalue(self, s)
+	if sip_peers[s] and sip_peers[s].info.online == nil then
+		return "n/a"
+	else
+		return sip_peers[s] and sip_peers[s].info.online
+			and "yes" or "no (%s)" % {
+				sip_peers[s] and sip_peers[s].info.Status:lower() or "unknown"
+			}
+	end
+end
+
+delay = sip_table:option(DummyValue, "delay", "Delay")
+function delay.cfgvalue(self, s)
+	if sip_peers[s] and sip_peers[s].info.online then
+		return "%i ms" % sip_peers[s].info.delay
+	else
+		return "n/a"
+	end
+end
+
+info = sip_table:option(Button, "_info", "Info")
+function info.write(self, s)
+	luci.http.redirect(luci.dispatcher.build_url(
+		"admin", "asterisk", "phones", "sip", s, "info"
+	))
+end
+
+return cbimap

+ 98 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/trunk_sip.lua

@@ -0,0 +1,98 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+]]--
+
+local ast = require("luci.asterisk")
+
+--
+-- SIP trunk info
+--
+if arg[2] == "info" then
+	form = SimpleForm("asterisk", "SIP Trunk Information")
+	form.reset  = false
+	form.submit = "Back to overview"
+
+	local info, keys = ast.sip.peer(arg[1])
+	local data = { }
+
+	for _, key in ipairs(keys) do
+		data[#data+1] = {
+			key = key,
+			val = type(info[key]) == "boolean"
+				and ( info[key] and "yes" or "no" )
+				or  ( info[key] == nil or #info[key] == 0 )
+					and "(none)"
+					or  tostring(info[key])
+		}
+	end
+
+	itbl = form:section(Table, data, "SIP Trunk %q" % arg[1])
+	itbl:option(DummyValue, "key", "Key")
+	itbl:option(DummyValue, "val", "Value")
+
+	function itbl.parse(...)
+		luci.http.redirect(
+			luci.dispatcher.build_url("admin", "asterisk", "trunks")
+		)
+	end
+
+	return form
+
+--
+-- SIP trunk config
+--
+elseif arg[1] then
+	cbimap = Map("asterisk", "Edit SIP Trunk")
+
+	peer = cbimap:section(NamedSection, arg[1])
+	peer.hidden = {
+		type    = "peer",
+		qualify = "yes",
+	}
+
+	back = peer:option(DummyValue, "_overview", "Back to trunk overview")
+	back.value = ""
+	back.titleref = luci.dispatcher.build_url("admin", "asterisk", "trunks")
+
+	sipdomain = peer:option(Value, "host", "SIP Domain")
+	sipport   = peer:option(Value, "port", "SIP Port")
+	function sipport.cfgvalue(...)
+		return AbstractValue.cfgvalue(...) or "5060"
+	end
+
+	username  = peer:option(Value, "username", "Authorization ID")
+	password  = peer:option(Value, "secret", "Authorization Password")
+	password.password = true
+
+	outboundproxy = peer:option(Value, "outboundproxy", "Outbound Proxy")
+	outboundport  = peer:option(Value, "outboundproxyport", "Outbound Proxy Port")
+
+	register = peer:option(Flag, "register", "Register with peer")
+	register.enabled  = "yes"
+	register.disabled = "no"
+
+	regext = peer:option(Value, "registerextension", "Extension to register (optional)")
+	regext:depends({register="1"})
+
+	didval = peer:option(ListValue, "_did", "Number of assigned DID numbers")
+	didval:value("", "(none)")
+	for i=1,24 do didval:value(i) end
+
+	dialplan = peer:option(ListValue, "context", "Dialplan Context")
+	dialplan:value(arg[1] .. "_inbound", "(default)")
+	cbimap.uci:foreach("asterisk", "dialplan",
+		function(s) dialplan:value(s['.name']) end)
+
+	return cbimap
+end

+ 106 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/trunks.lua

@@ -0,0 +1,106 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+]]--
+
+local ast = require("luci.asterisk")
+
+cbimap = Map("asterisk", "Trunks")
+cbimap.pageaction = false
+
+local sip_peers = { }
+cbimap.uci:foreach("asterisk", "sip",
+	function(s)
+		if s.type == "peer" then
+			s.name = s['.name']
+			s.info = ast.sip.peer(s.name)
+			sip_peers[s.name] = s
+		end
+	end)
+
+
+sip_table = cbimap:section(TypedSection, "sip", "SIP Trunks")
+sip_table.template    = "cbi/tblsection"
+sip_table.extedit     = luci.dispatcher.build_url("admin", "asterisk", "trunks", "sip", "%s")
+sip_table.addremove   = true
+sip_table.sectionhead = "Extension"
+
+function sip_table.filter(self, s)
+	return s and (
+		cbimap.uci:get("asterisk", s, "type") == nil or
+		cbimap.uci:get_bool("asterisk", s, "provider")
+	)
+end
+
+function sip_table.create(self, section)
+	if TypedSection.create(self, section) then
+		created = section
+	else
+		self.invalid_cts = true
+	end
+end
+
+function sip_table.parse(self, ...)
+	TypedSection.parse(self, ...)
+	if created then
+		cbimap.uci:tset("asterisk", created, {
+			type     = "friend",
+			qualify  = "yes",
+			provider = "yes"
+		})
+
+		cbimap.uci:save("asterisk")
+		luci.http.redirect(luci.dispatcher.build_url(
+			"admin", "asterisk", "trunks", "sip", created
+		))
+	end
+end
+
+
+user = sip_table:option(DummyValue, "username", "Username")
+
+context = sip_table:option(DummyValue, "context", "Dialplan")
+context.href = luci.dispatcher.build_url("admin", "asterisk", "dialplan")
+function context.cfgvalue(...)
+	return AbstractValue.cfgvalue(...) or "(default)"
+end
+
+online = sip_table:option(DummyValue, "online", "Registered")
+function online.cfgvalue(self, s)
+	if sip_peers[s] and sip_peers[s].info.online == nil then
+		return "n/a"
+	else
+		return sip_peers[s] and sip_peers[s].info.online
+			and "yes" or "no (%s)" %{
+				sip_peers[s] and sip_peers[s].info.Status:lower() or "unknown"
+			}
+	end
+end
+
+delay = sip_table:option(DummyValue, "delay", "Delay")
+function delay.cfgvalue(self, s)
+	if sip_peers[s] and sip_peers[s].info.online then
+		return "%i ms" % sip_peers[s].info.delay
+	else
+		return "n/a"
+	end
+end
+
+info = sip_table:option(Button, "_info", "Info")
+function info.write(self, s)
+	luci.http.redirect(luci.dispatcher.build_url(
+		"admin", "asterisk", "trunks", "sip", s, "info"
+	))
+end
+
+return cbimap

+ 59 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/voicemail.lua

@@ -0,0 +1,59 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2009 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+local ast = require "luci.asterisk"
+
+cbimap = Map("asterisk", "Voicemail - Mailboxes")
+
+voicemail = cbimap:section(TypedSection, "voicemail", "Voicemail Boxes")
+voicemail.addremove = true
+voicemail.anonymous = true
+voicemail.template = "cbi/tblsection"
+
+context = voicemail:option(ListValue, "context", "Context")
+context:value("default")
+
+number = voicemail:option(Value, "number",
+	"Mailbox Number", "Unique mailbox identifier")
+
+function number.write(self, s, val)
+	if val and #val > 0 then
+		local old = self:cfgvalue(s)
+		self.map.uci:foreach("asterisk", "dialplanvoice",
+			function(v)
+				if v.voicebox == old then
+					self.map:set(v['.name'], "voicebox", val)
+				end
+			end)
+		Value.write(self, s, val)
+	end
+end
+
+
+voicemail:option(Value, "name", "Ownername", "Human readable display name")
+voicemail:option(Value, "password", "Password", "Access protection")
+voicemail:option(Value, "email", "eMail", "Where to send voice messages")
+voicemail:option(Value, "page", "Pager", "Pager number")
+
+zone = voicemail:option(ListValue, "zone", "Timezone", "Used time format")
+zone.titleref = luci.dispatcher.build_url("admin/asterisk/voicemail/settings")
+cbimap.uci:foreach("asterisk", "voicezone",
+	function(s) zone:value(s['.name']) end)
+
+function voicemail.remove(self, s)
+	return ast.voicemail.remove(self.map:get(s, "number"), self.map.uci)
+end
+
+
+return cbimap

+ 62 - 0
luci/applications/luci-asterisk/luasrc/model/cbi/asterisk/voicemail_settings.lua

@@ -0,0 +1,62 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2009 Jo-Philipp Wich <xm@subsignal.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+require "luci.sys.zoneinfo"
+
+
+cbimap = Map("asterisk", "Voicemail - Common Settings")
+
+voicegeneral = cbimap:section(TypedSection, "voicegeneral",
+	"General Voicemail Options", "Common settings for all mailboxes are " ..
+	"defined here. Most of them are optional. The storage format should " ..
+	"never be changed once set.")
+
+voicegeneral.anonymous = true
+voicegeneral.addremove = false
+
+format = voicegeneral:option(MultiValue, "Used storage formats")
+format.widget = "checkbox"
+format:value("wav49")
+format:value("gsm")
+format:value("wav")
+
+voicegeneral:option(Flag, "sendvoicemail", "Enable sending of emails")
+voicegeneral:option(Flag, "attach", "Attach voice messages to emails")
+voicegeneral:option(Value, "serveremail", "Used email sender address")
+voicegeneral:option(Value, "emaildateformat", "Date format used in emails").optional = true
+voicegeneral:option(Value, "maxlogins", "Max. failed login attempts").optional = true
+voicegeneral:option(Value, "maxmsg", "Max. allowed messages per mailbox").optional = true
+voicegeneral:option(Value, "minmessage", "Min. number of seconds for voicemail").optional = true
+voicegeneral:option(Value, "maxmessage", "Max. number of seconds for voicemail").optional = true
+voicegeneral:option(Value, "maxsilence", "Seconds of silence until stop recording").optional = true
+voicegeneral:option(Value, "maxgreet", "Max. number of seconds for greetings").optional = true
+voicegeneral:option(Value, "skipms", "Milliseconds to skip for rew./ff.").optional = true
+voicegeneral:option(Value, "silencethreshold", "Threshold to detect silence").optional = true
+
+
+voicezone = cbimap:section(TypedSection, "voicezone", "Time Zones",
+	"Time zones define how dates and times are expressen when used in " ..
+	"an voice mails. Refer to the asterisk manual for placeholder values.")
+
+voicezone.addremove = true
+voicezone.sectionhead = "Name"
+voicezone.template = "cbi/tblsection"
+
+tz = voicezone:option(ListValue, "zone", "Location")
+for _, z in ipairs(luci.sys.zoneinfo.TZ) do tz:value(z[1]) end
+
+voicezone:option(Value, "message", "Date Format")
+
+
+return cbimap

+ 22 - 0
luci/applications/luci-asterisk/luasrc/view/asterisk/cbi/cell.htm

@@ -0,0 +1,22 @@
+<%#
+LuCI - Lua Configuration Interface
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id: dvalue.htm 3367 2008-09-19 10:42:02Z Cyrus $
+
+-%>
+
+<%+cbi/valueheader%>
+<% if self.href then %><a href="<%=self.href%>"><% end -%>
+	<%=self:cfgvalue(section)%>
+<%- if self.href then %></a><%end%>
+&#160;
+<input type="hidden" id="<%=cbid%>" value="<%=luci.util.pcdata(self:cfgvalue(section))%>" />
+<%+cbi/valuefooter%>

+ 254 - 0
luci/applications/luci-asterisk/luasrc/view/asterisk/dialplans.htm

@@ -0,0 +1,254 @@
+<%#
+LuCI - Lua Configuration Interface
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+-%>
+
+<%+header%>
+
+<%
+	local uci = luci.model.uci.cursor_state()
+	local ast = require "luci.asterisk"
+
+	function digit_pattern(s,t)
+		return "<code style='padding: 2px; border:1px solid #CCCCCC; background-color: #FFFFFF'%s>%s</code>"
+			%{ t and " title='" .. t .. "'" or "", s }
+	end
+
+	function rowstyle(i)
+		return "cbi-rowstyle-%i" %{
+			( i % 2 ) == 0 and 2 or 1
+		}
+	end
+
+	function format_matches(z)
+		local html = { }
+
+		if type(z) ~= "table" then
+			z = { matches = { z } }
+		end
+
+		if z.localprefix then
+			for _, m in ipairs(z.matches) do
+				html[#html+1] =
+					digit_pattern(z.localprefix, "local prefix") .. " " ..
+					digit_pattern(m)
+			end
+		end
+
+		if z.intlmatches and #z.intlmatches > 0 then
+			for _, i in ipairs(z.intlmatches) do
+				for _, m in ipairs(z.matches) do
+					html[#html+1] = "%s %s" %{
+						digit_pattern("(%s)" % i, "intl. prefix"),
+						digit_pattern(m)
+					}
+				end
+			end
+		else
+			for _, m in ipairs(z.matches) do
+				html[#html+1] = digit_pattern(m)
+			end
+		end
+
+		return table.concat(html, "; ")
+	end
+%>
+
+
+<form method="post" action="<%=luci.dispatcher.build_url("admin", "asterisk", "dialplans")%>" enctype="multipart/form-data">
+	<div>
+		<script type="text/javascript" src="/luci-static/resources/cbi.js"></script>
+		<input type="hidden" name="cbi.submit" value="1" />
+		<input type="submit" value="Save" class="hidden" />
+	</div>
+
+<div class="cbi-map" id="cbi-asterisk">
+	<h2><a id="content" name="content">Outgoing Call Routing</a></h2>
+	<div class="cbi-map-descr">
+		Here you can manage your dial plans which are used to route outgoing calls from your local extensions.<br /><br />
+		Related tasks:<br />
+		<a href="<%=luci.dispatcher.build_url('admin/asterisk/dialplans/zones')%>" class="cbi-title-ref">Manage dialzones</a> |
+		<a href="<%=luci.dispatcher.build_url('admin/asterisk/voicemail/mailboxes')%>" class="cbi-title-ref">Manage voicemail boxes</a> |
+		<a href="<%=luci.dispatcher.build_url('admin/asterisk/meetme/rooms')%>" class="cbi-title-ref">Manage meetme rooms</a>
+	</div>
+	<!-- tblsection -->
+	<fieldset class="cbi-section" id="cbi-asterisk-sip">
+		<!--<legend>Dialplans</legend>-->
+		<div class="cbi-section-descr"></div>
+
+		<% for i, plan in pairs(ast.dialplan.plans()) do %>
+		<div class="cbi-section-node">
+			<table class="cbi-section-table">
+				<tr class="cbi-section-table-titles">
+					<th style="text-align: left; padding: 3px" class="cbi-section-table-cell">
+						<big>Dialplan <em><%=plan.name%></em></big>
+					</th>
+					<td>
+						<a href="<%=luci.dispatcher.build_url('admin', 'asterisk', 'dialplans')%>?delplan=<%=plan.name%>">
+							<img style="border:none" alt="Remove this dialplan" title="Remove this dialplan" src="/luci-static/resources/cbi/remove.gif" />
+						</a>
+					</td>
+				</tr>
+
+				<!-- dialzones -->
+				<% local zones_used = { }; local row = 0 %>
+				<% for i, zone in ipairs(plan.zones) do zones_used[zone.name] = true %>
+				<tr class="cbi-section-table-row <%=rowstyle(row)%>">
+					<td style="text-align: left; padding: 3px" class="cbi-section-table-cell">
+						<strong>&#x2514; Dialzone <em><%=zone.name%></em></strong> (<%=zone.description%>)
+						<p style="padding-left: 1em; margin-bottom:0">
+							Lines:
+							<%=ast.tools.hyperlinks(
+								zone.trunks, function(v)
+									return luci.dispatcher.build_url("admin", "asterisk", "trunks", "%s") % v:lower()
+								end
+							)%><br />
+							Matches:
+							<%=format_matches(zone)%>
+						</p>
+					</td>
+					<td style="width:5%" class="cbi-value-field">
+						<a href="<%=luci.dispatcher.build_url('admin/asterisk/dialplans/out', zone.name)%>">
+							<img style="border:none" alt="Edit dialzone" title="Edit dialzone" src="/luci-static/resources/cbi/edit.gif" />
+						</a>
+						<a href="<%=luci.dispatcher.build_url('admin/asterisk/dialplans')%>?delzone.<%=plan.name%>=<%=zone.name%>">
+							<img style="border:none" alt="Remove from this dialplan" title="Remove from this dialplan" src="/luci-static/resources/cbi/remove.gif" />
+						</a>
+					</td>
+				</tr>
+				<% row = row + 1; end %>
+				<!-- /dialzones -->
+
+				<!-- voicemail -->
+				<% local boxes_used = { } %>
+				<% for ext, box in luci.util.kspairs(plan.voicemailboxes) do boxes_used[box.id] = true %>
+				<tr class="cbi-section-table-row <%=rowstyle(row)%>">
+					<td style="text-align: left; padding: 3px" class="cbi-section-table-cell">
+						<strong>&#x2514; Voicemailbox <em><%=box.id%></em></strong> (<%=box.name%>)
+						<p style="padding-left: 1em; margin-bottom:0">
+							Owner: <%=box.name%> |
+							eMail: <%=#box.email > 0 and box.email or 'n/a'%> |
+							Pager: <%=#box.page  > 0 and box.page  or 'n/a'%><br />
+							Matches: <%=format_matches(ext)%>
+						</p>
+					</td>
+					<td style="width:5%" class="cbi-value-field">
+						<a href="<%=luci.dispatcher.build_url('admin/asterisk/voicemail/mailboxes')%>">
+							<img style="border:none" alt="Manage mailboxes ..." title="Manage mailboxes ..." src="/luci-static/resources/cbi/edit.gif" />
+						</a>
+						<a href="<%=luci.dispatcher.build_url('admin/asterisk/dialplans')%>?delvbox.<%=plan.name%>=<%=ext%>">
+							<img style="border:none" alt="Remove from this dialplan" title="Remove from this dialplan" src="/luci-static/resources/cbi/remove.gif" />
+						</a>
+					</td>
+				</tr>
+				<% row = row + 1; end %>
+				<!-- /voicemail -->
+
+				<!-- meetme -->
+				<% local rooms_used = { } %>
+				<% for ext, room in luci.util.kspairs(plan.meetmerooms) do rooms_used[room.room] = true %>
+				<tr class="cbi-section-table-row <%=rowstyle(row)%>">
+					<td style="text-align: left; padding: 3px" class="cbi-section-table-cell">
+						<strong>&#x2514; MeetMe Room <em><%=room.room%></em></strong>
+						<% if room.description and #room.description > 0 then %> (<%=room.description%>)<% end %>
+						<p style="padding-left: 1em; margin-bottom:0">
+							Matches: <%=format_matches(ext)%>
+						</p>
+					</td>
+					<td style="width:5%" class="cbi-value-field">
+						<a href="<%=luci.dispatcher.build_url('admin/asterisk/meetme/rooms')%>">
+							<img style="border:none" alt="Manage conferences ..." title="Manage conferences ..." src="/luci-static/resources/cbi/edit.gif" />
+						</a>
+						<a href="<%=luci.dispatcher.build_url('admin/asterisk/dialplans')%>?delmeetme.<%=plan.name%>=<%=ext%>">
+							<img style="border:none" alt="Remove from this dialplan" title="Remove from this dialplan" src="/luci-static/resources/cbi/remove.gif" />
+						</a>
+					</td>
+				</tr>
+				<% row = row + 1; end %>
+				<!-- /meetme -->
+
+				<tr class="cbi-section-table-row">
+					<td style="text-align: left; padding: 3px" class="cbi-section-table-cell" colspan="2">
+						<hr style="margin-bottom:0.5em; border-width:0 0 1px 0" />
+
+						Add Dialzone:<br />
+						<select style="width:30%" name="addzone.<%=plan.name%>">
+							<option value="">-- please select --</option>
+							<% for _, zone in luci.util.kspairs(ast.dialzone.zones()) do %>
+								<% if not zones_used[zone.name] then %>
+									<option value="<%=zone.name%>"><%=zone.name%> (<%=zone.description%>)</option>
+								<% end %>
+							<% end %>
+						</select>
+						<br /><br />
+
+						Add Voicemailbox:<br />
+						<select style="width:20%" name="addvbox.<%=plan.name%>" onchange="this.form['addvboxext.<%=plan.name%>'].value=this.options[this.selectedIndex].value.split('@')[0]">
+							<option value="">-- please select --</option>
+							<% for ext, box in luci.util.kspairs(ast.voicemail.boxes()) do %>
+								<% if not boxes_used[box.id] then %>
+									<option value="<%=box.id%>"><%=box.id%> (<%=box.name%>)</option>
+								<% end %>
+							<% end %>
+						</select>
+						as extension
+						<input type="text" style="width:5%" name="addvboxext.<%=plan.name%>" />
+						<br /><br />
+
+						Add MeetMe Conference:<br />
+						<select style="width:20%" name="addmeetme.<%=plan.name%>" onchange="this.form['addmeetmeext.<%=plan.name%>'].value=this.options[this.selectedIndex].value">
+							<option value="">-- please select --</option>
+							<% for ext, room in luci.util.kspairs(ast.meetme.rooms()) do %>
+								<%# if not rooms_used[room.room] then %>
+									<option value="<%=room.room%>">
+										<%=room.room%>
+										<% if room.description and #room.description > 0 then %>(<%=room.description%>)<% end %>
+									</option>
+								<%# end %>
+							<% end %>
+						</select>
+						as extension
+						<input type="text" style="width:5%" name="addmeetmeext.<%=plan.name%>" />
+						<br /><br />
+
+						<input type="submit" class="cbi-button cbi-button-add" value="Add item &raquo;" title="Add item ..."/>
+					</td>
+				</tr>
+
+			</table>
+
+			<div class="cbi-section-create cbi-tblsection-create"></div>
+		</div>
+		<br />
+		<% end %>
+
+		<div class="cbi-section-node">
+			<div class="cbi-section-create cbi-tblsection-create" style="padding: 3px">
+				<h3>Create a new dialplan</h3>
+				The name is required and must be unique. It may only contain the characters A-Z, a-z, 0-9 and _ .<br />
+
+				<%- if create_error then %>
+					<br /><span style="color:red">Invalid name given!</span><br />
+				<% end -%>
+
+				<br />
+				<input type="text" class="cbi-section-create-name" name="addplan" style="width:200px" />
+				<input type="submit" class="cbi-button cbi-button-add" value="Add dialplan" title="Add dialplan"/>
+			</div>
+		</div>
+
+	</fieldset>
+</div>
+</form>
+<div class="clear"></div>
+<%+footer%>

+ 174 - 0
luci/applications/luci-asterisk/luasrc/view/asterisk/dialzones.htm

@@ -0,0 +1,174 @@
+<%#
+LuCI - Lua Configuration Interface
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+-%>
+
+<%+header%>
+
+<%
+	local uci = luci.model.uci.cursor_state()
+	local ast = require("luci.asterisk")
+
+	function digit_pattern(s)
+		return "<code style='padding: 2px; border:1px solid #CCCCCC; background-color: #FFFFFF'>%s</code>" % s
+	end
+
+	function rowstyle(i)
+		return "cbi-rowstyle-%i" %{
+			( i % 2 ) == 0 and 2 or 1
+		}
+	end
+
+	local function find_trunks()
+		local t = { }
+
+		uci:foreach("asterisk", "sip",
+			function(s)
+				if uci:get_bool("asterisk", s['.name'], "provider") then
+					t[#t+1] = {
+						"SIP/%s" % s['.name'],
+						"SIP: %s" % s['.name']
+					}
+				end
+			end)
+
+		uci:foreach("asterisk", "iax",
+			function(s)
+				t[#t+1] = {
+					"IAX/%s" % s['.name'],
+					"IAX: %s" % s.extension or s['.name']
+				}
+			end)
+
+		return t
+	end
+
+%>
+
+
+<form method="post" action="<%=luci.dispatcher.build_url("admin", "asterisk", "dialplans", "zones")%>" enctype="multipart/form-data">
+	<div>
+		<script type="text/javascript" src="/luci-static/resources/cbi.js"></script>
+		<input type="hidden" name="cbi.submit" value="1" />
+		<input type="submit" value="Save" class="hidden" />
+	</div>
+
+<div class="cbi-map" id="cbi-asterisk">
+	<h2><a id="content" name="content">Dial Zone Management</a></h2>
+	<div class="cbi-map-descr">
+		<a href="<%=luci.dispatcher.build_url("admin/asterisk/dialplans")%>" class="cbi-title-ref">Back to dialplan overview</a><br /><br />
+		Here you can manage your dial zones. The zones are used to route outgoing calls to the destination.
+		Each zone groups multiple trunks and number matches to represent a logical destination. Zones can
+		also be used to enforce certain dial restrictions on selected extensions.
+	</div>
+
+	<!-- tblsection -->
+	<fieldset class="cbi-section" id="cbi-asterisk-sip">
+		<div class="cbi-section-node">
+			<table class="cbi-section-table">
+				<tr class="cbi-section-table-titles">
+					<th style="text-align: left; padding: 3px" class="cbi-section-table-cell" colspan="6">
+						<h3>Dialzone Overview</h3>
+					</th>
+				</tr>
+
+				<tr class="cbi-section-table-descr">
+					<th style="width: 5%; text-align:right" class="cbi-section-table-cell">Name</th>
+					<th style="width: 5%; text-align:right" class="cbi-section-table-cell">Prepend</th>
+					<th style="width: 20%; text-align:left" class="cbi-section-table-cell">- Match</th>
+					<th style="text-align:left" class="cbi-section-table-cell">Trunk</th>
+					<th style="width: 35%; text-align:left" class="cbi-section-table-cell">Description</th>
+					<th style="width: 4%; text-align:left" class="cbi-section-table-cell"></th>
+				</tr>
+
+				<% for i, rule in pairs(ast.dialzone.zones()) do %>
+				<tr class="cbi-section-table-row <%=rowstyle(i)%>">
+					<td style="text-align:right" class="cbi-value-field">
+						<%=rule.name%>
+					</td>
+					<td style="text-align:right" class="cbi-value-field">
+						<% for _ in ipairs(rule.matches) do %>
+							<%=rule.addprefix and digit_pattern(rule.addprefix)%>&#160;<br />
+						<% end %>
+					</td>
+					<td style="text-align:left" class="cbi-value-field">
+						<% for _, m in ipairs(rule.matches) do %>
+							<%=rule.localprefix and "%s " % digit_pattern(rule.localprefix)%>
+							<%=digit_pattern(m)%><br />
+						<% end %>
+					</td>
+					<td style="text-align:left" class="cbi-value-field">
+						<%=ast.tools.hyperlinks(
+							rule.trunks, function(v)
+								return luci.dispatcher.build_url("admin", "asterisk", "trunks", "%s") % v:lower()
+							end
+						)%>
+					</td>
+					<td style="text-align:left" class="cbi-value-field">
+						<%=rule.description or rule.name%>
+					</td>
+					<td style="text-align:left" class="cbi-value-field">
+						<a href="<%=luci.dispatcher.build_url('admin', 'asterisk', 'dialplans', 'out', rule.name)%>">
+							<img style="border:none" alt="Edit entry" title="Edit entry" src="/luci-static/resources/cbi/edit.gif" />
+						</a>
+						<a href="<%=luci.dispatcher.build_url('admin', 'asterisk', 'dialplans', 'zones')%>?delzone=<%=rule.name%>">
+							<img style="border:none" alt="Delete entry" title="Delete entry" src="/luci-static/resources/cbi/remove.gif" />
+						</a>
+					</td>
+				</tr>
+				<% end %>
+			</table>
+			<div class="cbi-section-create cbi-tblsection-create"></div>
+		</div>
+		<br />
+
+		<div class="cbi-section-node">
+			<div class="cbi-section-create cbi-tblsection-create" style="padding: 3px">
+				<h3>Create a new dialzone</h3>
+				The name is required and must be unique. It may only contain the characters A-Z, a-z, 0-9 and _ .<br />
+				You can specifiy multiple number matches by separating them with spaces.<br />
+
+				<%- if create_error then %>
+					<br /><span style="color:red">Invalid name given!</span><br />
+				<% end -%>
+
+				<table>
+					<tr>
+						<td style="padding:3px">
+							<label for="create1">1) Name</label><br />
+							<input type="text" class="cbi-section-create-name" id="create1" name="newzone_name" style="width:200px" />
+							<br /><br />
+
+							<label for="create2">2) Number Match</label><br />
+							<input type="text" class="cbi-section-create-name" id="create2" name="newzone_match" style="width:200px" />
+						</td>
+						<td style="padding:3px">
+							<label for="create3">3) Trunks</label><br />
+							<select class="cbi-input-select" multiple="multiple" id="create3" name="newzone_uses" size="4"  style="width:200px">
+								<% for i, t in ipairs(find_trunks()) do %>
+									<option value="<%=t[1]%>"><%=t[2]%></option>
+								<% end %>
+							</select>
+						</td>
+					</tr>
+				</table>
+				<br />
+
+				<input type="submit" class="cbi-button cbi-button-add" name="newzone" value="Add entry" title="Add entry"/>
+			</div>
+		</div>
+	</fieldset>
+</div>
+</form>
+<div class="clear"></div>
+<%+footer%>

+ 231 - 0
luci/applications/luci-asterisk/root/etc/config/asterisk

@@ -0,0 +1,231 @@
+config 'asterisk'
+	option 'verbose' 3
+	option 'debug' 3
+	option 'zone' 'Australia/Perth'
+
+config 'hardware' 'reboot'
+	option 'method' 'web'
+	option 'param' 'http://ata.lan/admin/reboot'
+
+config 'feature' 'park'
+	option 'parkenabled' 'yes'
+	option 'parkext'   '700'
+	option 'parkpos'   '701-720'
+	option 'context'  'parkedcalls'
+	option 'parkingtime'  '45'
+	option 'courtesytone'  'beep'
+	option 'parkedplay'  'caller'
+	option 'adsipark'  'yes'
+	option 'findslot'  'first'
+	option 'parkedmusicclass'  'default'
+	option 'transferdigittimeout'  '3'
+	option 'xfersound'  'beep'
+	option 'xferfailsound'  'beeperr'
+	option 'pickupexten'  '"*8"'
+	option 'featuredigittimeout'  '500'
+	option 'atxfernoanswertimeout'  '15'
+
+config 'feature' 'map'
+	option 'blindxfer' '#1'
+	option 'disconnect' '*0'
+	option 'automon' '*1'
+	option 'atxfer' '#2'
+	option 'parkcall' '#30'
+
+config 'sipgeneral'
+	option 'realm' 'sip.mydomain.net'
+	option 'allow' 'alaw'
+	option 'extension' '102'
+
+config 'module'
+	option 'res_config_mysql' 'no'
+	option 'res_crypto' 'auto'
+	option 'chan_iax2' 'yes'
+
+config 'sip' 'PAP2T'
+	option 'type' 'friend'
+	option 'extension' '101'
+	option 'username' 'PAP2T'
+	option 'secret' 'mypass'
+	option 'port' '5061'
+	option 'host' ''
+	option 'dtmfmode' 'rfc2833'
+	option 'insecure' 'very'
+	option 'context' 'internal'
+	option 'mailbox' '1001@default'
+	option 'nat' 'no'
+	option 'canreinvite' 'nonat'
+	option 'selfmailbox' 'yes'
+	option 'incoming' 'provider_inbound'
+
+config 'sip' 'PAP2T2'
+	option 'type' 'friend'
+	option 'extension' '101'
+	option 'username' 'PAP2T2'
+	option 'secret' 'mysecret'
+	option 'port' '5060'
+	option 'host' ''
+	option 'dtmfmode' 'rfc2833'
+	option 'insecure' 'very'
+	option 'context' 'internal'
+	option 'mailbox' '1001@default'
+	option 'nat' 'no'
+	option 'canreinvite' 'nonat'
+	option 'selfmailbox' 'yes'
+	option 'incoming' 'provider_inbound'
+
+config 'sip' 'providerphone'
+	option 'provider' 'yes'
+	option 'type' 'friend'
+	option 'timeout' '55'
+	option 'internationalprefix' '0011'
+	option 'alwaysinternational' 'no'
+	option 'countrycode' '63'
+	option 'register' 'yes'
+	option 'host' '200.200.200.200'
+	option 'username' '0899999999'
+	option 'fromuser' '0899999999'
+	option 'secret' 'mysecret'
+	option 'fromdomain' 'providerphone.provider.net.au'
+	option 'context' 'provider_inbound'
+	option 'canreinvite' 'no'
+	option 'nat' 'yes'
+	option 'qualify' 'yes'
+	option 'insecure' 'very'
+	option 'pedantic' 'no'
+	option 'qualify' '1000'
+
+config 'iaxgeneral'
+	option 'static' 'yes'
+	option 'writeprotect' 'no'
+	option 'canreinvite' 'no'
+	option 'allow' 'ulaw,gsm'
+
+config 'iax' 'nell'
+	option 'type' 'friend'
+	option 'extension' '108'
+	option 'host' ''
+	option 'username' 'nell'
+	option 'secret' 'mypass'
+	option 'context' 'internal'
+
+config 'iax' 'iax_vista'
+	option 'extension' '106'
+	option 'type' 'friend'
+	option 'host' ''
+	option 'username' 'vista'
+	option 'secret' 'mysecret'
+	option 'context' 'internal'
+
+config 'iax' 'sam'
+	option 'type' 'friend'
+	option 'extension' '103'
+	option 'host' ''
+	option 'username' 'sam'
+	option 'secret' 'mysecret'
+	option 'context' 'internal'
+
+config 'voicegeneral'
+	option 'serveremail' 'voice@sip.mydomain.net'
+
+config 'voicemail'
+	option 'number' '1001'
+	option 'context' 'default'
+	option 'password' '0000'
+	option 'name' 'Family'
+	option 'email' 'us@mydomain.net'
+	option 'zone' 'wa'
+	option 'attach' 'no'
+
+config 'voicezone' 'wa'
+	option 'zone' 'Australia/Perth'
+	option 'message' 'Q IMp'
+
+config 'voicezone' 'military'
+	option 'zone' 'Zulu'
+	option 'message' '"vm-received" q "digits/at" H N "hours" "phonetic/z_p"'
+
+config 'incominggeneral'
+	option 'allowtransfer' 'no'
+	option 'timeout' '20'
+	option 'answerfirst' 'no'
+	option 'mailbox' '1001@default'
+
+config 'dialplangeneral'
+	option 'static' 'yes'
+	option 'writeprotect' 'no'
+	option 'canreinvite' 'no'
+	option 'clearglobalvars' 'no'
+	option 'allowtransfer' 'no'
+
+config 'dialplan' 'internal'
+	option 'include' 'localcall interstate smartnumber emergency extensions'
+
+config 'dialplanvoice'
+	option 'dialplan' 'internal'
+	option 'extension' '1001'
+	option 'voicecontext' 'default'
+	option 'voicebox' '1001'
+
+config 'dialplansaytime'
+	option 'dialplan' 'internal'
+	option 'extension' '108'
+
+config 'dialplanmeetme'
+	option 'dialplan' 'internal'
+	option 'extension' '109'
+	option 'room' '101'
+
+config 'dialplanmeetme'
+	option 'dialplan' 'internal'
+	option 'extension' '1009'
+	option 'room' ''
+
+config 'dialplan' 'localinternational'
+	option 'include' 'mobile interstate'
+
+config 'dialzone' 'interstate'
+	option 'uses' 'SIP/providerphone'
+	option 'match' '0[235-8]NXXXXXXX'
+	option 'localprefix' '0'
+
+config 'dialzone' 'mobile'
+	option 'uses' 'SIP/providerphone'
+	option 'match' '04XXXXXXXX'
+	option 'localprefix' '0'
+
+config 'dialzone' 'smartnumber'
+	option 'uses' 'SIP/providerphone'
+	option 'match' '1[835]00.'
+	option 'match' '13ZXXX'
+
+config 'dialzone' 'emergency'
+	option 'uses' 'SIP/providerphone'
+	option 'match' '000'
+	option 'match' '112'
+
+config 'dialzone' 'localcall'
+	option 'uses' 'SIP/providerphone'
+	option 'match' 'NXXXXXXX'
+	option 'addprefix' '08'
+	option 'localprefix' '0'
+
+config 'dialzone' 'international'
+	option 'uses' 'SIP/providerphone'
+	option 'international' '0011,+'
+	option 'localzone' 'localinternational'
+	option 'localprefix' '0'
+	option 'addprefix' ''
+
+config 'meetmegeneral'
+	option 'audiobuffers' '32'
+
+config 'meetme'
+	option 'room' '101'
+	option 'pin' ''
+	option 'adminpin' ''
+
+config 'moh' 'default'
+	option 'mode' 'files'
+	option 'directory' '/opt/moh'
+	option 'random' 'no'

+ 11 - 0
luci/applications/luci-asterisk/root/etc/uci-defaults/luci-asterisk

@@ -0,0 +1,11 @@
+#!/bin/sh
+
+uci -q batch <<-EOF >/dev/null
+	delete ucitrack.@asterisk[-1]
+	add ucitrack asterisk
+	set ucitrack.@asterisk[-1].init=asterisk
+	commit ucitrack
+EOF
+
+rm -f /tmp/luci-indexcache
+exit 0

+ 4 - 0
luci/applications/luci-commands/Makefile

@@ -0,0 +1,4 @@
+PO = commands
+
+include ../../build/config.mk
+include ../../build/module.mk

+ 237 - 0
luci/applications/luci-commands/luasrc/controller/commands.lua

@@ -0,0 +1,237 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2012 Jo-Philipp Wich <jow@librecmc.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+]]--
+
+module("luci.controller.commands", package.seeall)
+
+function index()
+	entry({"admin", "system", "commands"}, firstchild(), _("Custom Commands"), 80)
+	entry({"admin", "system", "commands", "dashboard"}, template("commands"), _("Dashboard"), 1)
+	entry({"admin", "system", "commands", "config"}, cbi("commands"), _("Configure"), 2)
+	entry({"admin", "system", "commands", "run"}, call("action_run"), nil, 3).leaf = true
+	entry({"admin", "system", "commands", "download"}, call("action_download"), nil, 3).leaf = true
+
+	entry({"command"}, call("action_public"), nil, 1).leaf = true
+end
+
+--- Decode a given string into arguments following shell quoting rules
+--- [[abc \def "foo\"bar" abc'def']] -> [[abc def]] [[foo"bar]] [[abcdef]]
+local function parse_args(str)
+	local args = { }
+
+	local function isspace(c)
+		if c == 9 or c == 10 or c == 11 or c == 12 or c == 13 or c == 32 then
+			return c
+		end
+	end
+
+	local function isquote(c)
+		if c == 34 or c == 39 or c == 96 then
+			return c
+		end
+	end
+
+	local function isescape(c)
+		if c == 92 then
+			return c
+		end
+	end
+
+	local function ismeta(c)
+		if c == 36 or c == 92 or c == 96 then
+			return c
+		end
+	end
+
+	--- Convert given table of byte values into a Lua string and append it to
+	--- the "args" table. Segment byte value sequence into chunks of 256 values
+	--- to not trip over the parameter limit for string.char()
+	local function putstr(bytes)
+		local chunks = { }
+		local csz = 256
+		local upk = unpack
+		local chr = string.char
+		local min = math.min
+		local len = #bytes
+		local off
+
+		for off = 1, len, csz do
+			chunks[#chunks+1] = chr(upk(bytes, off, min(off + csz - 1, len)))
+		end
+
+		args[#args+1] = table.concat(chunks)
+	end
+
+	--- Scan substring defined by the indexes [s, e] of the string "str",
+	--- perform unquoting and de-escaping on the fly and store the result in
+	--- a table of byte values which is passed to putstr()
+	local function unquote(s, e)
+		local off, esc, quote
+		local res = { }
+
+		for off = s, e do
+			local byte = str:byte(off)
+			local q = isquote(byte)
+			local e = isescape(byte)
+			local m = ismeta(byte)
+
+			if e then
+				esc = true
+			elseif esc then
+				if m then res[#res+1] = 92 end
+				res[#res+1] = byte
+				esc = false
+			elseif q and quote and q == quote then
+				quote = nil
+			elseif q and not quote then
+				quote = q
+			else
+				if m then res[#res+1] = 92 end
+				res[#res+1] = byte
+			end
+		end
+
+		putstr(res)
+	end
+
+	--- Find substring boundaries in "str". Ignore escaped or quoted
+	--- whitespace, pass found start- and end-index for each substring
+	--- to unquote()
+	local off, esc, start, quote
+	for off = 1, #str + 1 do
+		local byte = str:byte(off)
+		local q = isquote(byte)
+		local s = isspace(byte) or (off > #str)
+		local e = isescape(byte)
+
+		if esc then
+			esc = false
+		elseif e then
+			esc = true
+		elseif q and quote and q == quote then
+			quote = nil
+		elseif q and not quote then
+			start = start or off
+			quote = q
+		elseif s and not quote then
+			if start then
+				unquote(start, off - 1)
+				start = nil
+			end
+		else
+			start = start or off
+		end
+	end
+
+	--- If the "quote" is still set we encountered an unfinished string
+	if quote then
+		unquote(start, #str)
+	end
+
+	return args
+end
+
+local function parse_cmdline(cmdid, args)
+	local uci = require "luci.model.uci".cursor()
+	if uci:get("luci", cmdid) == "command" then
+		local cmd = uci:get_all("luci", cmdid)
+		local argv = parse_args(cmd.command)
+		local i, v
+
+		if cmd.param == "1" and args then
+			for i, v in ipairs(parse_args(luci.http.urldecode(args))) do
+				argv[#argv+1] = v
+			end
+		end
+
+		for i, v in ipairs(argv) do
+			if v:match("[^%w%.%-i/]") then
+				argv[i] = '"%s"' % v:gsub('"', '\\"')
+			end
+		end
+
+		return argv
+	end
+end
+
+function action_run(...)
+	local fs   = require "nixio.fs"
+	local argv = parse_cmdline(...)
+	if argv then
+		local outfile = os.tmpname()
+		local errfile = os.tmpname()
+
+		local rv = os.execute(table.concat(argv, " ") .. " >%s 2>%s" %{ outfile, errfile })
+		local stdout = fs.readfile(outfile, 1024 * 512) or ""
+		local stderr = fs.readfile(errfile, 1024 * 512) or ""
+
+		fs.unlink(outfile)
+		fs.unlink(errfile)
+
+		local binary = not not (stdout:match("[%z\1-\8\14-\31]"))
+
+		luci.http.prepare_content("application/json")
+		luci.http.write_json({
+			command  = table.concat(argv, " "),
+			stdout   = not binary and stdout,
+			stderr   = stderr,
+			exitcode = rv,
+			binary   = binary
+		})
+	else
+		luci.http.status(404, "No such command")
+	end
+end
+
+function action_download(...)
+	local fs   = require "nixio.fs"
+	local argv = parse_cmdline(...)
+	if argv then
+		local fd = io.popen(table.concat(argv, " ") .. " 2>/dev/null")
+		if fd then
+			local chunk = fd:read(4096) or ""
+			local name
+			if chunk:match("[%z\1-\8\14-\31]") then
+				luci.http.header("Content-Disposition", "attachment; filename=%s"
+				                 % fs.basename(argv[1]):gsub("%W+", ".") .. ".bin")
+				luci.http.prepare_content("application/octet-stream")
+			else
+				luci.http.header("Content-Disposition", "attachment; filename=%s"
+				                 % fs.basename(argv[1]):gsub("%W+", ".") .. ".txt")
+				luci.http.prepare_content("text/plain")
+			end
+
+			while chunk do
+				luci.http.write(chunk)
+				chunk = fd:read(4096)
+			end
+
+			fd:close()
+		else
+			luci.http.status(500, "Failed to execute command")
+		end
+	else
+		luci.http.status(404, "No such command")
+	end
+end
+
+function action_public(cmdid, args)
+	local uci = require "luci.model.uci".cursor()
+	if cmdid and
+	   uci:get("luci", cmdid) == "command" and
+	   uci:get("luci", cmdid, "public") == "1"
+	then
+		action_download(cmdid, args)
+	else
+		luci.http.status(403, "Access to command denied")
+	end
+end

+ 37 - 0
luci/applications/luci-commands/luasrc/model/cbi/commands.lua

@@ -0,0 +1,37 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2012 Jo-Philipp Wich <jow@librecmc.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+]]--
+
+local m, s
+
+m = Map("luci", translate("Custom Commands"),
+	translate("This page allows you to configure custom shell commands which can be easily invoked from the web interface."))
+
+s = m:section(TypedSection, "command", "")
+s.template = "cbi/tblsection"
+s.anonymous = true
+s.addremove = true
+
+
+s:option(Value, "name", translate("Description"),
+         translate("A short textual description of the configured command"))
+
+s:option(Value, "command", translate("Command"),
+         translate("Command line to execute"))
+
+s:option(Flag, "param", translate("Custom arguments"),
+         translate("Allow the user to provide additional command line arguments"))
+
+s:option(Flag, "public", translate("Public access"),
+         translate("Allow executing the command and downloading its output without prior authentication"))
+
+return m

+ 176 - 0
luci/applications/luci-commands/luasrc/view/commands.htm

@@ -0,0 +1,176 @@
+<%#
+LuCI - Lua Configuration Interface
+Copyright 2012 Jo-Philipp Wich <jow@librecmc.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+-%>
+
+<% css = [[
+
+.commandbox {
+	height: 12em;
+	width: 30%;
+	float: left;
+	height: 12em;
+	margin: 5px;
+	position: relative;
+}
+
+.commandbox h3 {
+	font-size: 1.5em !important;
+	line-height: 2em !important;
+	margin: 0 !important;
+}
+
+.commandbox input[type="text"] {
+	width: 50% !important;
+}
+
+.commandbox div {
+	position: absolute;
+	left: 0;
+	bottom: 1.5em;
+}
+
+]] -%>
+
+<%+header%>
+
+<script type="text/javascript" src="<%=resource%>/cbi.js"></script>
+<script type="text/javascript">//<![CDATA[
+	var stxhr = new XHR();
+
+	function command_run(id)
+	{
+		var args;
+		var field = document.getElementById(id);
+		if (field)
+			args = encodeURIComponent(field.value);
+
+		var legend = document.getElementById('command-rc-legend');
+		var output = document.getElementById('command-rc-output');
+
+		if (legend && output)
+		{
+			output.innerHTML =
+				'<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" /> ' +
+				'<%:Waiting for command to complete...%>'
+			;
+
+			legend.parentNode.style.display = 'block';
+			legend.style.display = 'inline';
+
+			stxhr.get('<%=luci.dispatcher.build_url("admin", "system", "commands", "run")%>/' + id + (args ? '/' + args : ''), null,
+				function(x, st)
+				{
+					if (st)
+					{
+						if (st.binary)
+							st.stdout = '[<%:Binary data not displayed, download instead.%>]';
+
+						legend.style.display = 'none';
+						output.innerHTML = String.format(
+							'<pre><strong># %h\n</strong>%h<span style="color:red">%h</span></pre>' +
+							'<div class="alert-message warning">%s (<%:Code:%> %d)</div>',
+							st.command, st.stdout, st.stderr,
+							(st.exitcode == 0) ? '<%:Command successful%>' : '<%:Command failed%>',
+							st.exitcode);
+					}
+					else
+					{
+						legend.style.display = 'none';
+						output.innerHTML = '<span class="error"><%:Failed to execute command!%></span>';
+					}
+
+					location.hash = '#output';
+				}
+			);
+		}
+	}
+
+	function command_download(id)
+	{
+		var args;
+		var field = document.getElementById(id);
+		if (field)
+			args = encodeURIComponent(field.value);
+
+		location.href = '<%=luci.dispatcher.build_url("admin", "system", "commands", "download")%>/' + id + (args ? '/' + args : '');
+	}
+
+	function command_link(id)
+	{
+		var legend = document.getElementById('command-rc-legend');
+		var output = document.getElementById('command-rc-output');
+
+		var args;
+		var field = document.getElementById(id);
+		if (field)
+			args = encodeURIComponent(field.value);
+
+		if (legend && output)
+		{
+			var link = location.protocol + '//' + location.hostname +
+			           (location.port ? ':' + location.port : '') +
+					   location.pathname.split(';')[0] + 'command/' +
+					   id + (args ? '/' + args : '');
+
+			legend.style.display = 'none';
+			output.parentNode.style.display = 'block';
+			output.innerHTML = String.format(
+				'<div class="alert-message"><%:Access command with%> <a href="%s">%s</a></div>',
+				link, link
+			);
+
+			location.hash = '#output';
+		}
+	}
+
+//]]></script>
+
+<%
+	local uci = require "luci.model.uci".cursor()
+	local commands = { }
+
+	uci:foreach("luci", "command", function(s) commands[#commands+1] = s end)
+%>
+
+<form method="get" action="<%=pcdata(luci.http.getenv("REQUEST_URI"))%>">
+	<div class="cbi-map">
+		<h2><a id="content" name="content"><%:Custom Commands%></a></h2>
+
+		<fieldset class="cbi-section">
+			<% local _, command; for _, command in ipairs(commands) do %>
+			<div class="commandbox">
+				<h3><%=pcdata(command.name)%></h3>
+				<p><%:Command:%> <code><%=pcdata(command.command)%></code></p>
+				<% if command.param == "1" then %>
+					<p><%:Arguments:%> <input type="text" id="<%=command['.name']%>" /></p>
+				<% end %>
+				<div>
+					<input type="button" value="<%:Run%>" class="cbi-button cbi-button-apply" onclick="command_run('<%=command['.name']%>')" />
+					<input type="button" value="<%:Download%>" class="cbi-button cbi-button-download" onclick="command_download('<%=command['.name']%>')" />
+					<% if command.public == "1" then %>
+						<input type="button" value="<%:Link%>" class="cbi-button cbi-button-link" onclick="command_link('<%=command['.name']%>')" />
+					<% end %>
+				</div>
+			</div>
+			<% end %>
+
+			<br style="clear:both" /><br />
+			<a name="output"></a>
+		</fieldset>
+	</div>
+
+	<fieldset class="cbi-section" style="display:none">
+		<legend id="command-rc-legend"><%:Collecting data...%></legend>
+		<span id="command-rc-output"></span>
+	</fieldset>
+</form>
+
+<%+footer%>

+ 4 - 0
luci/applications/luci-coovachilli/Makefile

@@ -0,0 +1,4 @@
+PO = coovachilli
+
+include ../../build/config.mk
+include ../../build/module.mk

+ 26 - 0
luci/applications/luci-coovachilli/luasrc/controller/coovachilli.lua

@@ -0,0 +1,26 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+module("luci.controller.coovachilli", package.seeall)
+
+function index()
+	local cc
+
+	cc = entry( { "admin", "services", "coovachilli" },       cbi("coovachilli"),         _("CoovaChilli"),                90)
+	cc.subindex = true
+
+	entry( { "admin", "services", "coovachilli", "network" }, cbi("coovachilli_network"), _("Network Configuration"),      10)
+	entry( { "admin", "services", "coovachilli", "radius"  }, cbi("coovachilli_radius"),  _("RADIUS configuration"),       20)
+	entry( { "admin", "services", "coovachilli", "auth"    }, cbi("coovachilli_auth"),    _("UAM and MAC Authentication"), 30)
+end

+ 31 - 0
luci/applications/luci-coovachilli/luasrc/model/cbi/coovachilli.lua

@@ -0,0 +1,31 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+
+m = Map("coovachilli")
+
+-- general
+s = m:section(TypedSection, "general")
+s.anonymous = true
+
+s:option( Flag, "debug" )
+s:option( Value, "interval" )
+s:option( Value, "pidfile" ).optional = true
+s:option( Value, "statedir" ).optional = true
+s:option( Value, "cmdsock" ).optional = true
+s:option( Value, "logfacility" ).optional = true
+
+
+return m

+ 76 - 0
luci/applications/luci-coovachilli/luasrc/model/cbi/coovachilli_auth.lua

@@ -0,0 +1,76 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+
+m = Map("coovachilli")
+
+-- uam config
+s1 = m:section(TypedSection, "uam")
+s1.anonymous = true
+
+s1:option( Value, "uamserver" )
+s1:option( Value, "uamsecret" ).password = true
+
+s1:option( Flag, "uamanydns" )
+s1:option( Flag, "nouamsuccess" )
+s1:option( Flag, "nouamwispr" )
+s1:option( Flag, "chillixml" )
+s1:option( Flag, "uamanyip" ).optional = true
+s1:option( Flag, "dnsparanoia" ).optional = true
+s1:option( Flag, "usestatusfile" ).optional = true
+
+s1:option( Value, "uamhomepage" ).optional = true
+s1:option( Value, "uamlisten" ).optional = true
+s1:option( Value, "uamport" ).optional = true
+s1:option( Value, "uamiport" ).optional = true
+s1:option( DynamicList, "uamdomain" ).optional = true
+s1:option( Value, "uamlogoutip" ).optional = true
+s1:option( DynamicList, "uamallowed" ).optional = true
+s1:option( Value, "uamui" ).optional = true
+
+s1:option( Value, "wisprlogin" ).optional = true
+
+s1:option( Value, "defsessiontimeout" ).optional = true
+s1:option( Value, "defidletimeout" ).optional = true
+s1:option( Value, "definteriminterval" ).optional = true
+
+s1:option( Value, "ssid" ).optional = true
+s1:option( Value, "vlan" ).optional = true
+s1:option( Value, "nasip" ).optional = true
+s1:option( Value, "nasmac" ).optional = true
+s1:option( Value, "wwwdir" ).optional = true
+s1:option( Value, "wwwbin" ).optional = true
+
+s1:option( Value, "localusers" ).optional = true
+s1:option( Value, "postauthproxy" ).optional = true
+s1:option( Value, "postauthproxyport" ).optional = true
+s1:option( Value, "locationname" ).optional = true
+
+
+-- mac authentication
+s2 = m:section(TypedSection, "macauth")
+s2.anonymous = true
+
+s2:option( Flag, "macauth" )
+s2:option( Flag, "macallowlocal" )
+s2:option( DynamicList, "macallowed" )
+
+pw = s2:option( Value, "macpasswd" )
+pw.optional = true
+pw.password = true
+
+s2:option( Value, "macsuffix" ).optional = true
+
+return m

+ 67 - 0
luci/applications/luci-coovachilli/luasrc/model/cbi/coovachilli_network.lua

@@ -0,0 +1,67 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+require("luci.sys")
+require("luci.ip")
+
+m = Map("coovachilli")
+
+-- tun
+s1 = m:section(TypedSection, "tun")
+s1.anonymous = true
+
+s1:option( Flag, "usetap" )
+s1:option( Value, "tundev" ).optional = true
+s1:option( Value, "txqlen" ).optional = true
+
+net = s1:option( Value, "net" )
+for _, route in ipairs(luci.sys.net.routes()) do
+	if route.device ~= "lo" and route.dest:prefix() < 32 then
+		net:value( route.dest:string() )
+	end
+end
+
+s1:option( Value, "dynip" ).optional = true
+s1:option( Value, "statip" ).optional = true
+
+s1:option( Value, "dns1" ).optional = true
+s1:option( Value, "dns2" ).optional = true
+s1:option( Value, "domain" ).optional = true
+
+s1:option( Value, "ipup" ).optional = true
+s1:option( Value, "ipdown" ).optional = true
+
+s1:option( Value, "conup" ).optional = true
+s1:option( Value, "condown" ).optional = true
+
+
+-- dhcp config
+s2 = m:section(TypedSection, "dhcp")
+s2.anonymous = true
+
+dif = s2:option( Value, "dhcpif" )
+for _, nif in ipairs(luci.sys.net.devices()) do
+	if nif ~= "lo" then dif:value(nif) end
+end
+
+s2:option( Value, "dhcpmac" ).optional = true
+s2:option( Value, "lease" ).optional = true
+s2:option( Value, "dhcpstart" ).optional = true
+s2:option( Value, "dhcpend" ).optional = true
+
+s2:option( Flag, "eapolenable" )
+
+
+return m

+ 67 - 0
luci/applications/luci-coovachilli/luasrc/model/cbi/coovachilli_radius.lua

@@ -0,0 +1,67 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+
+m = Map("coovachilli")
+
+-- radius server
+s1 = m:section(TypedSection, "radius")
+s1.anonymous = true
+
+s1:option( Value, "radiusserver1" )
+s1:option( Value, "radiusserver2" )
+s1:option( Value, "radiussecret" ).password = true
+
+s1:option( Value, "radiuslisten" ).optional = true
+s1:option( Value, "radiusauthport" ).optional = true
+s1:option( Value, "radiusacctport" ).optional = true
+
+s1:option( Value, "radiusnasid" ).optional = true
+s1:option( Value, "radiusnasip" ).optional = true
+
+s1:option( Value, "radiuscalled" ).optional = true
+s1:option( Value, "radiuslocationid" ).optional = true
+s1:option( Value, "radiuslocationname" ).optional = true
+
+s1:option( Value, "radiusnasporttype" ).optional = true
+
+s1:option( Flag, "radiusoriginalurl" )
+
+s1:option( Value, "adminuser" ).optional = true
+rs = s1:option( Value, "adminpassword" )
+rs.optional = true
+rs.password = true
+
+s1:option( Flag, "swapoctets" )
+s1:option( Flag, "openidauth" )
+s1:option( Flag, "wpaguests" )
+s1:option( Flag, "acctupdate" )
+
+s1:option( Value, "coaport" ).optional = true
+s1:option( Flag, "coanoipcheck" )
+
+
+-- radius proxy
+s2 = m:section(TypedSection, "proxy")
+s2.anonymous = true
+
+s2:option( Value, "proxylisten" ).optional = true
+s2:option( Value, "proxyport" ).optional = true
+s2:option( Value, "proxyclient" ).optional = true
+ps = s2:option( Value, "proxysecret" )
+ps.optional = true
+ps.password = true
+
+return m

+ 243 - 0
luci/applications/luci-coovachilli/root/etc/config/coovachilli

@@ -0,0 +1,243 @@
+##############################################################################
+#
+# Sample CoovaChilli configuration file
+#
+##############################################################################
+
+# General settings
+config general
+
+	# Enable this flag to include debug information.
+	option debug	0
+
+	# Re-read configuration file at this interval. Will also cause new domain
+	# name lookups to be performed. Value is given in seconds.
+	option interval	3600
+
+	# File to store information about the process id of the program.
+	# The program must have write access to this file/directory.
+	option pidfile	/var/run/chilli.pid
+
+	# Directory to use for nonvolatile storage.
+	# The program must have write access to this directory.
+	# This tag is currently ignored
+	#option	statedir	./
+
+
+# TUN parameters
+config tun
+
+	# IP network address of external packet data network
+	# Used to allocate dynamic IP addresses and set up routing.
+	# Normally you do not need to uncomment this tag.
+	option net	192.168.182.0/24
+
+	# Dynamic IP address pool
+	# Used to allocate dynamic IP addresses to clients.
+	# If not set it defaults to the net tag.
+	# Do not uncomment this tag unless you are an experienced user!
+	#option	dynip	192.168.182.0/24
+
+	# Static IP address pool
+	# Used to allocate static IP addresses to clients.
+	# Do not uncomment this tag unless you are an experienced user!
+	#option	statip	192.168.182.0/24
+
+	# Primary DNS server.
+	# Will be suggested to the client.
+	# If omitted the system default will be used.
+	# Normally you do not need to uncomment this tag.
+	#option	dns1	172.16.0.5
+
+	# Secondary DNS server.
+	# Will be suggested to the client.
+	# If omitted the system default will be used.
+	# Normally you do not need to uncomment this tag.
+	#option	dns2	172.16.0.6
+
+	# Domain name
+	# Will be suggested to the client.
+	# Normally you do not need to uncomment this tag.
+	option domain	key.chillispot.org
+
+	# Script executed after network interface has been brought up.
+	# Executed with the following parameters: <devicename> <ip address> <mask>
+	# Normally you do not need to uncomment this tag.
+	#option	ipup	/etc/chilli.ipup
+
+	# Script executed after network interface has been taken down.
+	# Executed with the following parameters: <devicename> <ip address> <mask>
+	# Normally you do not need to uncomment this tag.
+	#option	ipdown	/etc/chilli.ipdown
+
+	# Script executed after a user has been authenticated.
+	# Executed with the following parameters: <devicename> <ip address>
+	# <mask> <user ip address> <user mac address> <filter ID>
+	# Normally you do not need to uncomment this tag.
+	#option	conup	/etc/chilli.conup
+
+	# Script executed after a user has disconnected.
+	# Executed with the following parameters: <devicename> <ip address>
+	# <mask> <user ip address> <user mac address> <filter ID>
+	# Normally you do not need to uncomment this tag.
+	#option	condown	/etc/chilli.condown
+
+
+# DHCP Parameters
+config dhcp
+
+	# Ethernet interface to listen to.
+	# This is the network interface which is connected to the access points.
+	# In a typical configuration this tag should be set to eth1.
+	option dhcpif	eth1
+
+	# Use specified MAC address.
+	# An address in the range  00:00:5E:00:02:00 - 00:00:5E:FF:FF:FF falls
+	# within the IANA range of addresses and is not allocated for other
+	# purposes.
+	# Normally you do not need to uncomment this tag.
+	#option dhcpmac	00:00:5E:00:02:00
+
+	# Time before DHCP lease expires
+	# Normally you do not need to uncomment this tag.
+	#option lease	600
+
+
+# Radius parameters
+config radius
+
+	# IP address to listen to
+	# Normally you do not need to uncomment this tag.
+	#option radiuslisten	127.0.0.1
+
+	# IP address of radius server 1
+	# For most installations you need to modify this tag.
+	option radiusserver1	rad01.chillispot.org
+
+	# IP address of radius server 2
+	# If you have only one radius server you should set radiusserver2 to the
+	# same value as radiusserver1.
+	# For most installations you need to modify this tag.
+	option radiusserver2	rad02.chillispot.org
+
+	# Radius authentication port
+	# The UDP port number to use for radius authentication requests.
+	# The same port number is used for both radiusserver1 and radiusserver2.
+	# Normally you do not need to uncomment this tag.
+	#option radiusauthport	1812
+
+	# Radius accounting port
+	# The UDP port number to use for radius accounting requests.
+	# The same port number is used for both radiusserver1 and radiusserver2.
+	# Normally you do not need to uncomment this tag.
+	#option radiusacctport	1813
+
+	# Radius shared secret for both servers
+	# For all installations you should modify this tag.
+	#option radiussecret	testing123
+
+	# Radius NAS-Identifier
+	# Normally you do not need to uncomment this tag.
+	#option radiusnasid	nas01
+
+	# Radius NAS-IP-Address
+	# Normally you do not need to uncomment this tag.
+	#option radiusnasip	127.0.0.1
+
+	# Radius Called-Station-ID
+	# Normally you do not need to uncomment this tag.
+	#option radiuscalled	00133300
+
+	# WISPr Location ID. Should be in the format: isocc=<ISO_Country_Code>,
+	# cc=<E.164_Country_Code>,ac=<E.164_Area_Code>,network=<ssid/ZONE>
+	# Normally you do not need to uncomment this tag.
+	#option radiuslocationid	isocc=us,cc=1,ac=408,network=ACMEWISP_NewarkAirport
+
+	# WISPr Location Name. Should be in the format:
+	# <HOTSPOT_OPERATOR_NAME>,<LOCATION>
+	# Normally you do not need to uncomment this tag.
+	#option radiuslocationname	ACMEWISP,Gate_14_Terminal_C_of_Newark_Airport
+
+
+# Radius proxy parameters
+config proxy
+
+	# IP address to listen to
+	# Normally you do not need to uncomment this tag.
+	#option proxylisten	10.0.0.1
+
+	# UDP port to listen to.
+	# If not specified a port will be selected by the system
+	# Normally you do not need to uncomment this tag.
+	#option proxyport	1645
+
+	# Client(s) from which we accept radius requests
+	# Normally you do not need to uncomment this tag.
+	#option proxyclient	10.0.0.1/24
+
+	# Radius proxy shared secret for all clients
+	# If not specified defaults to radiussecret
+	# Normally you do not need to uncomment this tag.
+	#option proxysecret	testing123
+
+
+# Universal access method (UAM) parameters
+config uam
+
+	# URL of web server handling authentication.
+	option uamserver	https://radius.chillispot.org/hotspotlogin
+
+	# URL of welcome homepage.
+	# Unauthenticated users will be redirected to this URL. If not specified
+	# users will be redirected to the uamserver instead.
+	# Normally you do not need to uncomment this tag.
+	#option uamhomepage	http://192.168.182.1/welcome.html
+
+	# Shared between chilli and authentication web server
+	#option uamsecret	ht2eb8ej6s4et3rg1ulp
+
+	# IP address to listen to for authentication requests
+	# Do not uncomment this tag unless you are an experienced user!
+	#option uamlisten	192.168.182.1
+
+	# TCP port to listen to for authentication requests
+	# Do not uncomment this tag unless you are an experienced user!
+	#option uamport	3990
+
+	# Comma separated list of domain names, IP addresses or network segments
+	# the client can access without first authenticating.
+	# It is possible to specify this tag multiple times.
+	# Normally you do not need to uncomment this tag.
+	#list uamallowed	www.chillispot.org
+	#list uamallowed	10.11.12.0/24
+
+	# If this flag is given unauthenticated users are allowed to use
+	# any DNS server.
+	# Normally you do not need to uncomment this tag.
+	#uamanydns
+
+
+# MAC authentication
+config macauth
+
+	# If this flag is given users will be authenticated only on their MAC
+	# address.
+	# Normally you do not need to enable this flag.
+	option macauth	0
+
+	# List of MAC addresses.
+	# The MAC addresses specified in this list will be authenticated only on
+	# their MAC address.
+	# This tag is ignored if the macauth tag is given.
+	# It is possible to specify this tag multiple times.
+	# Normally you do not need to uncomment this tag.
+	#list macallowed	00-0A-5E-AC-BE-51
+	#list macallowed	00-30-1B-3C-32-E9
+
+	# Password to use for MAC authentication.
+	# Normally you do not need to uncomment this tag.
+	#option macpasswd	password
+
+	# Suffix to add to MAC address in order to form the username.
+	# Normally you do not need to uncomment this tag.
+	#option macsuffix	suffix

+ 51 - 0
luci/applications/luci-ddns/CHANGELOG

@@ -0,0 +1,51 @@
+Version: 2.1.0-3
+Date: 2014-12-07
+ddns-scripts: 2.1.0-2 or greater needed
+
+- modified controller
+	- remove support for ddns-scripts Version 1.x
+	- ddns-scripts not correctly installed do not start
+	- no config file create an empty one
+- moved log settings to advanced tab
+- set default syslog level "Notice"
+- removed special handling for dynamic_dns_helper.sh in postinst
+
+--------------------------------------------------------------------------------
+Version: 2.1.0-2
+Date: 2014-11-15
+ddns-scripts: 2.1.0-2 or greater needed
+
+- moved /usr/lib/ddns/dynamic_dns_lucihelper.sh to ddns-scripts package
+- fixed error message when validating proxy
+- modified validating ip_script to allow the usage of parameters
+- using ipkg/postinst AND ipkg/postinst-pkg as long as LuCI Makefiles did not
+	fullfil requirements of new libreCMC default_postinst and default_postrm
+	reported in Issue #255
+- cleanup whitespaces at line ends
+
+--------------------------------------------------------------------------------
+Version: 2.1.0-1
+Date: 2014-11-09
+ddns-scripts: 2.1.0-1 or greater needed
+
+fix verify of entry for DNS server Issue #244
+	https://github.com/librecmc/luci/issues/244
+add support for option 'update_script'
+add display of version information when click on "Dynamic DNS" on overview page
+add verify of installed ddns-scripts version and show as hint if not correct version
+modified epoch to date conversation
+cbi object Flag did not set section.changed state, fixed in tools.flag_parse function
+ucitrack entry no longer needed and removed
+minor fixes
+
+--------------------------------------------------------------------------------
+Version: 2.0.1-1
+Date: 2014-09-21
+ddns-scripts: 2.0.1-1 up to 2.0.1-9
+
+New DDNS status in System->Status overview
+New Overview page with option to start/stop a section
+New Detail page with tabbed view incl. logfile viewer
+Extended verify of all entries before save to config
+   incl. connect test to DNS- and Proxy-server
+Support for all available options of ddns-scripts 1.x and 2.x

+ 4 - 0
luci/applications/luci-ddns/Makefile

@@ -0,0 +1,4 @@
+PO = ddns
+
+include ../../build/config.mk
+include ../../build/module.mk

+ 5 - 0
luci/applications/luci-ddns/ipkg/postinst

@@ -0,0 +1,5 @@
+#!/bin/sh
+[ -n "${IPKG_INSTROOT}" ] || {
+	( . /etc/uci-defaults/luci-ddns ) && rm -f /etc/uci-defaults/luci-ddns
+	exit 0
+}

+ 5 - 0
luci/applications/luci-ddns/ipkg/postinst-pkg

@@ -0,0 +1,5 @@
+#!/bin/sh
+[ -n "${IPKG_INSTROOT}" ] || {
+	( . /etc/uci-defaults/luci-ddns ) && rm -f /etc/uci-defaults/luci-ddns
+	exit 0
+}

+ 261 - 0
luci/applications/luci-ddns/luasrc/controller/ddns.lua

@@ -0,0 +1,261 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+Copyright 2014 Christian Schoenebeck <christian dot schoenebeck at gmail dot com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+module("luci.controller.ddns", package.seeall)
+
+local NX   = require "nixio"
+local NXFS = require "nixio.fs"
+local DISP = require "luci.dispatcher"
+local HTTP = require "luci.http"
+local UCI  = require "luci.model.uci"
+local SYS  = require "luci.sys"
+local DDNS = require "luci.tools.ddns"		-- ddns multiused functions
+local UTIL = require "luci.util"
+
+local luci_ddns_version = "2.1.0-3"	-- luci-app-ddns / librecmc Makefile compatible version
+local ddns_scripts_min  = "2.1.0-2"	-- minimum version of ddns-scripts required
+
+function index()
+	-- no services_ipv6 file or no dynamic_dns_lucihelper.sh
+	-- do NOT start
+	if not nixio.fs.access("/usr/lib/ddns/services_ipv6") 
+	or not nixio.fs.access("/usr/lib/ddns/dynamic_dns_lucihelper.sh") then
+		return
+	end
+	-- no config create an empty one
+	if not nixio.fs.access("/etc/config/ddns") then
+		nixio.fs.writefile("/etc/config/ddns", "")
+	end
+
+	entry( {"admin", "services", "ddns"}, cbi("ddns/overview"), _("Dynamic DNS"), 59)
+	entry( {"admin", "services", "ddns", "detail"}, cbi("ddns/detail"), nil ).leaf = true
+	entry( {"admin", "services", "ddns", "hints"}, cbi("ddns/hints",
+		{hideapplybtn=true, hidesavebtn=true, hideresetbtn=true}), nil ).leaf = true
+	entry( {"admin", "services", "ddns", "logview"}, call("logread") ).leaf = true
+	entry( {"admin", "services", "ddns", "startstop"}, call("startstop") ).leaf = true
+	entry( {"admin", "services", "ddns", "status"}, call("status") ).leaf = true
+end
+
+-- function to read all sections status and return data array
+local function _get_status()
+	local uci	 = UCI.cursor()
+	local service	 = SYS.init.enabled("ddns") and 1 or 0
+	local url_start	 = DISP.build_url("admin", "system", "startup")
+	local luci_build = DDNS.ipkg_version("luci-app-ddns").version
+	local ddns_act   = DDNS.ipkg_version("ddns-scripts").version
+	local data	 = {}	-- Array to transfer data to javascript
+
+	data[#data+1] 	= {
+		enabled	   = service,		-- service enabled
+		url_up	   = url_start,		-- link to enable DDS (System-Startup)
+		luci_ver   = luci_ddns_version,	-- luci-app-ddns / librecmc Makefile compatible version
+		luci_build = luci_build,	-- installed luci build
+		script_min = ddns_scripts_min,	-- minimum version of ddns-scripts needed
+		script_ver = ddns_act		-- installed ddns-scripts
+	}
+
+	uci:foreach("ddns", "service", function (s)
+
+		-- Get section we are looking at
+		-- and enabled state
+		local section	= s[".name"]
+		local enabled	= tonumber(s["enabled"]) or 0
+		local datelast	= "_empty_"	-- formated date of last update
+		local datenext	= "_empty_"	-- formated date of next update
+
+		-- get force seconds
+		local force_seconds = DDNS.calc_seconds(
+				tonumber(s["force_interval"]) or 72 ,
+				s["force_unit"] or "hours" )
+		-- get/validate pid and last update
+		local pid      = DDNS.get_pid(section)
+		local uptime   = SYS.uptime()
+		local lasttime = DDNS.get_lastupd(section)
+		if lasttime > uptime then 	-- /var might not be linked to /tmp
+			lasttime = 0 		-- and/or not cleared on reboot
+		end
+
+		-- no last update happen
+		if lasttime == 0 then
+			datelast = "_never_"
+
+		-- we read last update
+		else
+			-- calc last update
+			--             sys.epoch - sys uptime   + lastupdate(uptime)
+			local epoch = os.time() - uptime + lasttime
+			-- use linux date to convert epoch
+			datelast = DDNS.epoch2date(epoch)
+			-- calc and fill next update
+			datenext = DDNS.epoch2date(epoch + force_seconds)
+		end
+
+		-- process running but update needs to happen
+		-- problems it force_seconds > uptime
+		force_seconds = (force_seconds > uptime) and uptime or force_seconds
+		if pid > 0 and ( lasttime + force_seconds - uptime ) <= 0 then
+			datenext = "_verify_"
+
+		-- run once
+		elseif force_seconds == 0 then
+			datenext = "_runonce_"
+
+		-- no process running and NOT enabled
+		elseif pid == 0 and enabled == 0 then
+			datenext  = "_disabled_"
+
+		-- no process running and NOT
+		elseif pid == 0 and enabled ~= 0 then
+			datenext = "_stopped_"
+		end
+
+		-- get/set monitored interface and IP version
+		local iface	= s["interface"] or "_nonet_"
+		local use_ipv6	= tonumber(s["use_ipv6"]) or 0
+		if iface ~= "_nonet_" then
+			local ipv = (use_ipv6 == 1) and "IPv6" or "IPv4"
+			iface = ipv .. " / " .. iface
+		end
+
+		-- try to get registered IP
+		local domain	= s["domain"] or "_nodomain_"
+		local dnsserver	= s["dns_server"] or ""
+		local force_ipversion = tonumber(s["force_ipversion"] or 0)
+		local force_dnstcp = tonumber(s["force_dnstcp"] or 0)
+		local command = [[/usr/lib/ddns/dynamic_dns_lucihelper.sh]]
+		command = command .. [[ get_registered_ip ]] .. domain .. [[ ]] .. use_ipv6 ..
+			[[ ]] .. force_ipversion .. [[ ]] .. force_dnstcp .. [[ ]] .. dnsserver
+		local reg_ip = SYS.exec(command)
+		if reg_ip == "" then
+			reg_ip = "_nodata_"
+		end
+
+		-- fill transfer array
+		data[#data+1]	= {
+			section  = section,
+			enabled  = enabled,
+			iface    = iface,
+			domain   = domain,
+			reg_ip   = reg_ip,
+			pid      = pid,
+			datelast = datelast,
+			datenext = datenext
+		}
+	end)
+
+	uci:unload("ddns")
+	return data
+end
+
+-- called by XHR.get from detail_logview.htm
+function logread(section)
+	-- read application settings
+	local uci	  = UCI.cursor()
+	local log_dir	  = uci:get("ddns", "global", "log_dir") or "/var/log/ddns"
+	local lfile=log_dir .. "/" .. section .. ".log"
+
+	local ldata=NXFS.readfile(lfile)
+	if not ldata or #ldata == 0 then
+		ldata="_nodata_"
+	end
+	uci:unload("ddns")
+	HTTP.write(ldata)
+end
+
+-- called by XHR.get from overview_status.htm
+function startstop(section, enabled)
+	local uci  = UCI.cursor()
+	local data = {}		-- Array to transfer data to javascript
+
+	-- if process running we want to stop and return
+	local pid = DDNS.get_pid(section)
+	if pid > 0 then
+		local tmp = NX.kill(pid, 15)	-- terminate
+		NX.nanosleep(2)	-- 2 second "show time"
+		-- status changed so return full status
+		data = _get_status()
+		HTTP.prepare_content("application/json")
+		HTTP.write_json(data)
+		return
+	end
+
+	-- read uncommited changes
+	-- we don't save and commit data from other section or other options
+	-- only enabled will be done
+	local exec	  = true
+	local changed     = uci:changes("ddns")
+	for k_config, v_section in pairs(changed) do
+		-- security check because uci.changes only gets our config
+		if k_config ~= "ddns" then
+			exec = false
+			break
+		end
+		for k_section, v_option in pairs(v_section) do
+			-- check if only section of button was changed
+			if k_section ~= section then
+				exec = false
+				break
+			end
+			for k_option, v_value in pairs(v_option) do
+				-- check if only enabled was changed
+				if k_option ~= "enabled" then
+					exec = false
+					break
+				end
+			end
+		end
+	end
+
+	-- we can not execute because other
+	-- uncommited changes pending, so exit here
+	if not exec then
+		HTTP.write("_uncommited_")
+		return
+	end
+
+	-- save enable state
+	uci:set("ddns", section, "enabled", ( (enabled == "true") and "1" or "0") )
+	uci:save("ddns")
+	uci:commit("ddns")
+	uci:unload("ddns")
+
+	-- start dynamic_dns_updater.sh script
+	os.execute ([[/usr/lib/ddns/dynamic_dns_updater.sh %s 0 > /dev/null 2>&1 &]] % section)
+	NX.nanosleep(3)	-- 3 seconds "show time"
+
+	-- status changed so return full status
+	data = _get_status()
+	HTTP.prepare_content("application/json")
+	HTTP.write_json(data)
+end
+
+-- called by XHR.poll from overview_status.htm
+function status()
+	local data = _get_status()
+	HTTP.prepare_content("application/json")
+	HTTP.write_json(data)
+end
+
+-- check if installed ddns-scripts version < required version
+function update_needed()
+	local sver = DDNS.ipkg_version("ddns-scripts")
+	local rver = UTIL.split(ddns_scripts_min, "[%.%-]", nil, true)
+	return (sver.major < (tonumber(rver[1]) or 0))
+	    or (sver.minor < (tonumber(rver[2]) or 0))
+	    or (sver.patch < (tonumber(rver[3]) or 0))
+	    or (sver.build < (tonumber(rver[4]) or 0))
+end
+

+ 1224 - 0
luci/applications/luci-ddns/luasrc/model/cbi/ddns/detail.lua

@@ -0,0 +1,1224 @@
+--[[
+LuCI - Lua Configuration Interface
+
+A lot of code taken from original ddns.lua cbi-model made by
+Copyright 2008 Steven Barth <steven@midlink.org>
+Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
+Copyright 2013 Manuel Munz <freifunk at somakoma dot de>
+
+modified to use as detail page together with new overview page and
+extensions for IPv6, HTTPS settings, syslog and log settings,
+optional Proxy-Support, optional DNS-Server, optional use of TCP requests to DNS server,
+optional force of IP protocol version usage
+Copyright 2014 Christian Schoenebeck <christian dot schoenebeck at gmail dot com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+local NX   = require "nixio"
+local FS   = require "nixio.fs"
+local SYS  = require "luci.sys"
+local UTIL = require "luci.util"
+local DISP = require "luci.dispatcher"
+local WADM = require "luci.tools.webadmin"
+local DTYP = require "luci.cbi.datatypes"
+local DDNS = require "luci.tools.ddns"		-- ddns multiused functions
+
+-- takeover arguments -- #######################################################
+section = arg[1]
+
+-- check supported options -- ##################################################
+-- saved to local vars here because doing multiple os calls slow down the system
+has_ipv6   = DDNS.check_ipv6()	-- IPv6 support
+has_ssl    = DDNS.check_ssl()	-- HTTPS support
+has_proxy  = DDNS.check_proxy()	-- Proxy support
+has_dnstcp = DDNS.check_bind_host()	-- DNS TCP support
+has_force  = has_ssl and has_dnstcp		-- Force IP Protocoll
+
+-- html constants -- ###########################################################
+font_red = "<font color='red'>"
+font_off = "</font>"
+bold_on  = "<strong>"
+bold_off = "</strong>"
+
+-- error text constants -- #####################################################
+err_ipv6_plain = translate("IPv6 not supported") .. " - " ..
+		translate("please select 'IPv4' address version")
+err_ipv6_basic = bold_on ..
+			font_red ..
+				translate("IPv6 not supported") ..
+			font_off ..
+			"<br />" .. translate("please select 'IPv4' address version") ..
+		 bold_off
+err_ipv6_other = bold_on ..
+			font_red ..
+				translate("IPv6 not supported") ..
+			font_off ..
+			"<br />" .. translate("please select 'IPv4' address version in") .. " " ..
+			[[<a href="]] ..
+					DISP.build_url("admin", "services", "ddns", "detail", section) ..
+					"?tab.dns." .. section .. "=basic" ..
+				[[">]] ..
+				translate("Basic Settings") ..
+			[[</a>]] ..
+		 bold_off
+
+function err_tab_basic(self)
+	return translate("Basic Settings") .. " - " .. self.title .. ": "
+end
+function err_tab_adv(self)
+	return translate("Advanced Settings") .. " - " .. self.title .. ": "
+end
+function err_tab_timer(self)
+	return translate("Timer Settings") .. " - " .. self.title .. ": "
+end
+
+-- function to verify settings around ip_source
+-- will use dynamic_dns_lucihelper to check if
+-- local IP can be read
+local function _verify_ip_source()
+	-- section is globally defined here be calling agrument (see above)
+	local _network   = "-"
+	local _url       = "-"
+	local _interface = "-"
+	local _script    = "-"
+	local _proxy     = ""
+
+	local _ipv6   = usev6:formvalue(section)
+	local _source = (_ipv6 == "1")
+			and src6:formvalue(section)
+			or  src4:formvalue(section)
+	if _source == "network" then
+		_network = (_ipv6 == "1")
+			and ipn6:formvalue(section)
+			or  ipn4:formvalue(section)
+	elseif _source == "web" then
+		_url = (_ipv6 == "1")
+			and iurl6:formvalue(section)
+			or  iurl4:formvalue(section)
+		-- proxy only needed for checking url
+		_proxy = (pxy) and pxy:formvalue(section) or ""
+	elseif _source == "interface" then
+		_interface = ipi:formvalue(section)
+	elseif _source == "script" then
+		_script = ips:formvalue(section)
+	end
+
+	local command = [[/usr/lib/ddns/dynamic_dns_lucihelper.sh get_local_ip ]] ..
+		_ipv6 .. [[ ]] .. _source .. [[ ]] .. _network .. [[ ]] ..
+		_url .. [[ ]] .. _interface .. [[ ']] .. _script.. [[' ]] .. _proxy
+	local ret = SYS.call(command)
+
+	if ret == 0 then
+		return true	-- valid
+	else
+		return nil	-- invalid
+	end
+end
+
+-- cbi-map definition -- #######################################################
+m = Map("ddns")
+
+-- first need to close <a> from cbi map template our <a> closed by template
+m.title = [[</a><a href="]] .. DISP.build_url("admin", "services", "ddns") .. [[">]] ..
+		translate("Dynamic DNS")
+
+m.description = translate("Dynamic DNS allows that your router can be reached with " ..
+			"a fixed hostname while having a dynamically changing " ..
+			"IP address.")
+
+m.redirect = DISP.build_url("admin", "services", "ddns")
+
+m.on_after_commit = function(self)
+	if self.changed then	-- changes ?
+		local pid = DDNS.get_pid(section)
+		if pid > 0 then	-- running ?
+			local tmp = NX.kill(pid, 1)	-- send SIGHUP
+		end
+	end
+end
+
+-- read application settings -- ################################################
+-- date format; if not set use ISO format
+date_format = m.uci:get(m.config, "global", "date_format") or "%F %R"
+-- log directory
+log_dir = m.uci:get(m.config, "global", "log_dir") or "/var/log/ddns"
+
+-- cbi-section definition -- ###################################################
+ns = m:section( NamedSection, section, "service",
+	translate("Details for") .. ([[: <strong>%s</strong>]] % section),
+	translate("Configure here the details for selected Dynamic DNS service") )
+ns.instance = section	-- arg [1]
+ns:tab("basic", translate("Basic Settings"), nil )
+ns:tab("advanced", translate("Advanced Settings"), nil )
+ns:tab("timer", translate("Timer Settings"), nil )
+ns:tab("logview", translate("Log File Viewer"), nil )
+
+-- TAB: Basic  #####################################################################################
+-- enabled  -- #################################################################
+en = ns:taboption("basic", Flag, "enabled",
+	translate("Enabled"),
+	translate("If this service section is disabled it could not be started." .. "<br />" ..
+		"Neither from LuCI interface nor from console") )
+en.orientation = "horizontal"
+function en.parse(self, section)
+	DDNS.flag_parse(self, section)
+end
+
+-- use_ipv6 (NEW)  -- ##########################################################
+usev6 = ns:taboption("basic", ListValue, "use_ipv6",
+	translate("IP address version"),
+	translate("Defines which IP address 'IPv4/IPv6' is send to the DDNS provider") )
+usev6.widget  = "radio"
+usev6.default = "0"
+usev6:value("0", translate("IPv4-Address") )
+function usev6.cfgvalue(self, section)
+	local value = AbstractValue.cfgvalue(self, section)
+	if has_ipv6 or (value == "1" and not has_ipv6) then
+		self:value("1", translate("IPv6-Address") )
+	end
+	if value == "1" and not has_ipv6 then
+		self.description = err_ipv6_basic
+	end
+	return value
+end
+function usev6.validate(self, value)
+	if (value == "1" and has_ipv6) or value == "0" then
+		return value
+	end
+	return nil, err_tab_basic(self) .. err_ipv6_plain
+end
+function usev6.write(self, section, value)
+	if value == "0" then	-- force rmempty
+		return self.map:del(section, self.option)
+	else
+		return self.map:set(section, self.option, value)
+	end
+end
+
+-- IPv4 - service_name -- ######################################################
+svc4 = ns:taboption("basic", ListValue, "ipv4_service_name",
+	translate("DDNS Service provider") .. " [IPv4]" )
+svc4.default	= "-"
+svc4:depends("use_ipv6", "0")	-- only show on IPv4
+
+local services4 = { }
+local fd4 = io.open("/usr/lib/ddns/services", "r")
+
+if fd4 then
+	local ln
+	repeat
+		ln = fd4:read("*l")
+		local s = ln and ln:match('^%s*"([^"]+)"')
+		if s then services4[#services4+1] = s end
+	until not ln
+	fd4:close()
+end
+
+for _, v in UTIL.vspairs(services4) do svc4:value(v) end
+svc4:value("-", translate("-- custom --") )
+
+function svc4.cfgvalue(self, section)
+	local v =  DDNS.read_value(self, section, "service_name")
+	if not v or #v == 0 then
+		return "-"
+	else
+		return v
+	end
+end
+function svc4.validate(self, value)
+	if usev6:formvalue(section) == "0" then	-- do only on IPv4
+		return value
+	else
+		return ""	-- supress validate error
+	end
+end
+function svc4.write(self, section, value)
+	if usev6:formvalue(section) == "0" then	-- do only IPv4 here
+		self.map:del(section, self.option)	-- to be shure
+		if value ~= "-" then			-- and write "service_name
+			self.map:del(section, "update_url")	-- delete update_url
+			return self.map:set(section, "service_name", value)
+		else
+			return self.map:del(section, "service_name")
+		end
+	end
+end
+
+-- IPv6 - service_name -- ######################################################
+svc6 = ns:taboption("basic", ListValue, "ipv6_service_name",
+	translate("DDNS Service provider") .. " [IPv6]" )
+svc6.default	= "-"
+svc6:depends("use_ipv6", "1")	-- only show on IPv6
+if not has_ipv6 then
+	svc6.description = err_ipv6_basic
+end
+
+local services6 = { }
+local fd6 = io.open("/usr/lib/ddns/services_ipv6", "r")
+
+if fd6 then
+	local ln
+	repeat
+		ln = fd6:read("*l")
+		local s = ln and ln:match('^%s*"([^"]+)"')
+		if s then services6[#services6+1] = s end
+	until not ln
+	fd6:close()
+end
+
+for _, v in UTIL.vspairs(services6) do svc6:value(v) end
+svc6:value("-", translate("-- custom --") )
+
+function svc6.cfgvalue(self, section)
+	local v =  DDNS.read_value(self, section, "service_name")
+	if not v or #v == 0 then
+		return "-"
+	else
+		return v
+	end
+end
+function svc6.validate(self, value)
+	if usev6:formvalue(section) == "1" then	-- do only on IPv6
+		if has_ipv6 then return value end
+		return nil, err_tab_basic(self) .. err_ipv6_plain
+	else
+		return ""	-- supress validate error
+	end
+end
+function svc6.write(self, section, value)
+	if usev6:formvalue(section) == "1" then	-- do only when IPv6
+		self.map:del(section, self.option)	-- delete "ipv6_service_name" helper
+		if value ~= "-" then			-- and write "service_name
+			self.map:del(section, "update_url")	-- delete update_url
+			return self.map:set(section, "service_name", value)
+		else
+			return self.map:del(section, "service_name")
+		end
+	end
+end
+
+-- IPv4/IPv6 - update_url -- ###################################################
+uurl = ns:taboption("basic", Value, "update_url",
+	translate("Custom update-URL"),
+	translate("Update URL to be used for updating your DDNS Provider." .. "<br />" ..
+		"Follow instructions you will find on their WEB page.") )
+uurl:depends("ipv4_service_name", "-")
+uurl:depends("ipv6_service_name", "-")
+function uurl.validate(self, value)
+	local script = ush:formvalue(section)
+
+	if (usev6:formvalue(section) == "0" and svc4:formvalue(section) ~= "-") or
+	   (usev6:formvalue(section) == "1" and svc6:formvalue(section) ~= "-") then
+		return ""	-- suppress validate error
+	elseif not value then
+		if not script or not (#script > 0) then
+			return nil, err_tab_basic(self) .. translate("missing / required")
+		else
+			return ""	-- suppress validate error / update_script is given
+		end
+	elseif (#script > 0) then
+		return nil, err_tab_basic(self) .. translate("either url or script could be set")
+	end
+
+	local url = DDNS.parse_url(value)
+	if not url.scheme == "http" then
+		return nil, err_tab_basic(self) .. translate("must start with 'http://'")
+	elseif not url.query then
+		return nil, err_tab_basic(self) .. "<QUERY> " .. translate("missing / required")
+	elseif not url.host then
+		return nil, err_tab_basic(self) .. "<HOST> " .. translate("missing / required")
+	elseif SYS.call([[nslookup ]] .. url.host .. [[ >/dev/null 2>&1]]) ~= 0 then
+		return nil, err_tab_basic(self) .. translate("can not resolve host: ") .. url.host
+	end
+
+	return value
+end
+
+-- IPv4/IPv6 - update_script -- ################################################
+ush = ns:taboption("basic", Value, "update_script",
+	translate("Custom update-script"),
+	translate("Custom update script to be used for updating your DDNS Provider.") )
+ush:depends("ipv4_service_name", "-")
+ush:depends("ipv6_service_name", "-")
+function ush.validate(self, value)
+	local url = uurl:formvalue(section)
+
+	if (usev6:formvalue(section) == "0" and svc4:formvalue(section) ~= "-") or
+	   (usev6:formvalue(section) == "1" and svc6:formvalue(section) ~= "-") then
+		return ""	-- suppress validate error
+	elseif not value then
+		if not url or not (#url > 0) then
+			return nil, err_tab_basic(self) .. translate("missing / required")
+		else
+			return ""	-- suppress validate error / update_url is given
+		end
+	elseif (#url > 0) then
+		return nil, err_tab_basic(self) .. translate("either url or script could be set")
+	elseif not FS.access(value) then
+		return nil, err_tab_basic(self) .. translate("File not found")
+	end
+	return value
+end
+
+-- IPv4/IPv6 - domain -- #######################################################
+dom = ns:taboption("basic", Value, "domain",
+		translate("Hostname/Domain"),
+		translate("Replaces [DOMAIN] in Update-URL") )
+dom.rmempty	= false
+dom.placeholder	= "mypersonaldomain.dyndns.org"
+function dom.validate(self, value)
+	if not value
+	or not (#value > 0)
+	or not DTYP.hostname(value) then
+		return nil, err_tab_basic(self) ..	translate("invalid - Sample") .. ": 'mypersonaldomain.dyndns.org'"
+	else
+		return value
+	end
+end
+
+-- IPv4/IPv6 - username -- #####################################################
+user = ns:taboption("basic", Value, "username",
+		translate("Username"),
+		translate("Replaces [USERNAME] in Update-URL") )
+user.rmempty = false
+function user.validate(self, value)
+	if not value then
+		return nil, err_tab_basic(self) .. translate("missing / required")
+	end
+	return value
+end
+
+-- IPv4/IPv6 - password -- #####################################################
+pw = ns:taboption("basic", Value, "password",
+		translate("Password"),
+		translate("Replaces [PASSWORD] in Update-URL") )
+pw.rmempty  = false
+pw.password = true
+function pw.validate(self, value)
+	if not value then
+		return nil, err_tab_basic(self) .. translate("missing / required")
+	end
+	return value
+end
+
+-- IPv4/IPv6 - use_https (NEW) -- ##############################################
+if has_ssl or ( ( m:get(section, "use_https") or "0" ) == "1" ) then
+	https = ns:taboption("basic", Flag, "use_https",
+		translate("Use HTTP Secure") )
+	https.orientation = "horizontal"
+	https.rmempty = false -- force validate function
+	function https.cfgvalue(self, section)
+		local value = AbstractValue.cfgvalue(self, section)
+		if not has_ssl and value == "1" then
+			self.description = bold_on .. font_red ..
+				translate("HTTPS not supported") .. font_off .. "<br />" ..
+				translate("please disable") .. " !" .. bold_off
+		else
+			self.description = translate("Enable secure communication with DDNS provider")
+		end
+		return value
+	end
+	function https.parse(self, section)
+		DDNS.flag_parse(self, section)
+	end
+	function https.validate(self, value)
+		if (value == "1" and has_ssl ) or value == "0" then return value end
+		return nil, err_tab_basic(self) .. translate("HTTPS not supported") .. " !"
+	end
+	function https.write(self, section, value)
+		if value == "1" then
+			return self.map:set(section, self.option, value)
+		else
+			self.map:del(section, "cacert")
+			return self.map:del(section, self.option)
+		end
+	end
+end
+
+-- IPv4/IPv6 - cacert (NEW) -- #################################################
+if has_ssl then
+	cert = ns:taboption("basic", Value, "cacert",
+		translate("Path to CA-Certificate"),
+		translate("directory or path/file") .. "<br />" ..
+		translate("or") .. bold_on .. " IGNORE " .. bold_off ..
+		translate("to run HTTPS without verification of server certificates (insecure)") )
+	cert:depends("use_https", "1")
+	cert.rmempty = false -- force validate function
+	cert.default = "/etc/ssl/certs"
+	function cert.validate(self, value)
+		if https:formvalue(section) == "0" then
+			return ""	-- supress validate error if NOT https
+		end
+		if value then	-- otherwise errors in datatype check
+			if DTYP.directory(value)
+			or DTYP.file(value)
+			or value == "IGNORE" then
+				return value
+			end
+		end
+		return nil, err_tab_basic(self) ..
+			translate("file or directory not found or not 'IGNORE'") .. " !"
+	end
+end
+
+-- TAB: Advanced  ##################################################################################
+-- IPv4 - ip_source -- #########################################################
+src4 = ns:taboption("advanced", ListValue, "ipv4_source",
+	translate("IP address source") .. " [IPv4]",
+	translate("Defines the source to read systems IPv4-Address from, that will be send to the DDNS provider") )
+src4:depends("use_ipv6", "0")	-- IPv4 selected
+src4.default = "network"
+src4:value("network", translate("Network"))
+src4:value("web", translate("URL"))
+src4:value("interface", translate("Interface"))
+src4:value("script", translate("Script"))
+function src4.cfgvalue(self, section)
+	return DDNS.read_value(self, section, "ip_source")
+end
+function src4.validate(self, value)
+	if usev6:formvalue(section) == "1" then
+		return ""	-- ignore on IPv6 selected
+	elseif not _verify_ip_source() then
+		return nil, err_tab_adv(self) ..
+			translate("can not detect local IP. Please select a different Source combination")
+	else
+		return value
+	end
+end
+function src4.write(self, section, value)
+	if usev6:formvalue(section) == "1" then
+		return true	-- ignore on IPv6 selected
+	elseif value == "network" then
+		self.map:del(section, "ip_url")		-- delete not need parameters
+		self.map:del(section, "ip_interface")
+		self.map:del(section, "ip_script")
+	elseif value == "web" then
+		self.map:del(section, "ip_network")	-- delete not need parameters
+		self.map:del(section, "ip_interface")
+		self.map:del(section, "ip_script")
+	elseif value == "interface" then
+		self.map:del(section, "ip_network")	-- delete not need parameters
+		self.map:del(section, "ip_url")
+		self.map:del(section, "ip_script")
+	elseif value == "script" then
+		self.map:del(section, "ip_network")
+		self.map:del(section, "ip_url")		-- delete not need parameters
+		self.map:del(section, "ip_interface")
+	end
+	self.map:del(section, self.option)		 -- delete "ipv4_source" helper
+	return self.map:set(section, "ip_source", value) -- and write "ip_source
+end
+
+-- IPv6 - ip_source -- #########################################################
+src6 = ns:taboption("advanced", ListValue, "ipv6_source",
+	translate("IP address source") .. " [IPv6]",
+	translate("Defines the source to read systems IPv6-Address from, that will be send to the DDNS provider") )
+src6:depends("use_ipv6", 1)	-- IPv6 selected
+src6.default = "network"
+src6:value("network", translate("Network"))
+src6:value("web", translate("URL"))
+src6:value("interface", translate("Interface"))
+src6:value("script", translate("Script"))
+if not has_ipv6 then
+	src6.description = err_ipv6_other
+end
+function src6.cfgvalue(self, section)
+	return DDNS.read_value(self, section, "ip_source")
+end
+function src6.validate(self, value)
+	if usev6:formvalue(section) == "0" then
+		return ""	-- ignore on IPv4 selected
+	elseif not has_ipv6 then
+		return nil, err_tab_adv(self) .. err_ipv6_plain
+	elseif not _verify_ip_source() then
+		return nil, err_tab_adv(self) ..
+			translate("can not detect local IP. Please select a different Source combination")
+	else
+		return value
+	end
+end
+function src6.write(self, section, value)
+	if usev6:formvalue(section) == "0" then
+		return true	-- ignore on IPv4 selected
+	elseif value == "network" then
+		self.map:del(section, "ip_url")		-- delete not need parameters
+		self.map:del(section, "ip_interface")
+		self.map:del(section, "ip_script")
+	elseif value == "web" then
+		self.map:del(section, "ip_network")	-- delete not need parameters
+		self.map:del(section, "ip_interface")
+		self.map:del(section, "ip_script")
+	elseif value == "interface" then
+		self.map:del(section, "ip_network")	-- delete not need parameters
+		self.map:del(section, "ip_url")
+		self.map:del(section, "ip_script")
+	elseif value == "script" then
+		self.map:del(section, "ip_network")
+		self.map:del(section, "ip_url")		-- delete not need parameters
+		self.map:del(section, "ip_interface")
+	end
+	self.map:del(section, self.option)		 -- delete "ipv4_source" helper
+	return self.map:set(section, "ip_source", value) -- and write "ip_source
+end
+
+-- IPv4 - ip_network (default "wan") -- ########################################
+ipn4 = ns:taboption("advanced", ListValue, "ipv4_network",
+	translate("Network") .. " [IPv4]",
+	translate("Defines the network to read systems IPv4-Address from") )
+ipn4:depends("ipv4_source", "network")
+ipn4.default = "wan"
+WADM.cbi_add_networks(ipn4)
+function ipn4.cfgvalue(self, section)
+	return DDNS.read_value(self, section, "ip_network")
+end
+function ipn4.validate(self, value)
+	if usev6:formvalue(section) == "1"
+	 or src4:formvalue(section) ~= "network" then
+		-- ignore if IPv6 selected OR
+		-- ignore everything except "network"
+		return ""
+	else
+		return value
+	end
+end
+function ipn4.write(self, section, value)
+	if usev6:formvalue(section) == "1"
+	 or src4:formvalue(section) ~= "network" then
+		-- ignore if IPv6 selected OR
+		-- ignore everything except "network"
+		return true
+	else
+		-- set also as "interface" for monitoring events changes/hot-plug
+		self.map:set(section, "interface", value)
+		self.map:del(section, self.option)		  -- delete "ipv4_network" helper
+		return self.map:set(section, "ip_network", value) -- and write "ip_network"
+	end
+end
+
+-- IPv6 - ip_network (default "wan6") -- #######################################
+ipn6 = ns:taboption("advanced", ListValue, "ipv6_network",
+	translate("Network") .. " [IPv6]" )
+ipn6:depends("ipv6_source", "network")
+ipn6.default = "wan6"
+WADM.cbi_add_networks(ipn6)
+if has_ipv6 then
+	ipn6.description = translate("Defines the network to read systems IPv6-Address from")
+else
+	ipn6.description = err_ipv6_other
+end
+function ipn6.cfgvalue(self, section)
+	return DDNS.read_value(self, section, "ip_network")
+end
+function ipn6.validate(self, value)
+	if usev6:formvalue(section) == "0"
+	 or src6:formvalue(section) ~= "network" then
+		-- ignore if IPv4 selected OR
+		-- ignore everything except "network"
+		return ""
+	elseif has_ipv6 then
+		return value
+	else
+		return nil, err_tab_adv(self) .. err_ipv6_plain
+	end
+end
+function ipn6.write(self, section, value)
+	if usev6:formvalue(section) == "0"
+	 or src6:formvalue(section) ~= "network" then
+		-- ignore if IPv4 selected OR
+		-- ignore everything except "network"
+		return true
+	else
+		-- set also as "interface" for monitoring events changes/hotplug
+		self.map:set(section, "interface", value)
+		self.map:del(section, self.option)		  -- delete "ipv6_network" helper
+		return self.map:set(section, "ip_network", value) -- and write "ip_network"
+	end
+end
+
+-- IPv4 - ip_url (default "checkip.dyndns.com") -- #############################
+iurl4 = ns:taboption("advanced", Value, "ipv4_url",
+	translate("URL to detect") .. " [IPv4]",
+	translate("Defines the Web page to read systems IPv4-Address from") )
+iurl4:depends("ipv4_source", "web")
+iurl4.default = "http://checkip.dyndns.com"
+function iurl4.cfgvalue(self, section)
+	return DDNS.read_value(self, section, "ip_url")
+end
+function iurl4.validate(self, value)
+	if usev6:formvalue(section) == "1"
+	 or src4:formvalue(section) ~= "web" then
+		-- ignore if IPv6 selected OR
+		-- ignore everything except "web"
+		return ""
+	elseif not value or #value == 0 then
+		return nil, err_tab_adv(self) .. translate("missing / required")
+	end
+
+	local url = DDNS.parse_url(value)
+	if not (url.scheme == "http" or url.scheme == "https") then
+		return nil, err_tab_adv(self) .. translate("must start with 'http://'")
+	elseif not url.host then
+		return nil, err_tab_adv(self) .. "<HOST> " .. translate("missing / required")
+	elseif SYS.call([[nslookup ]] .. url.host .. [[>/dev/null 2>&1]]) ~= 0 then
+		return nil, err_tab_adv(self) .. translate("can not resolve host: ") .. url.host
+	else
+		return value
+	end
+end
+function iurl4.write(self, section, value)
+	if usev6:formvalue(section) == "1"
+	 or src4:formvalue(section) ~= "web" then
+		-- ignore if IPv6 selected OR
+		-- ignore everything except "web"
+		return true
+	else
+		self.map:del(section, self.option)		-- delete "ipv4_url" helper
+		return self.map:set(section, "ip_url", value)	-- and write "ip_url"
+	end
+end
+
+-- IPv6 - ip_url (default "checkipv6.dyndns.com") -- ###########################
+iurl6 = ns:taboption("advanced", Value, "ipv6_url",
+	translate("URL to detect") .. " [IPv6]" )
+iurl6:depends("ipv6_source", "web")
+iurl6.default = "http://checkipv6.dyndns.com"
+if has_ipv6 then
+	iurl6.description = translate("Defines the Web page to read systems IPv6-Address from")
+else
+	iurl6.description = err_ipv6_other
+end
+function iurl6.cfgvalue(self, section)
+	return DDNS.read_value(self, section, "ip_url")
+end
+function iurl6.validate(self, value)
+	if usev6:formvalue(section) == "0"
+	 or src6:formvalue(section) ~= "web" then
+		-- ignore if IPv4 selected OR
+		-- ignore everything except "web"
+		return ""
+	elseif not has_ipv6 then
+		return nil, err_tab_adv(self) .. err_ipv6_plain
+	elseif not value or #value == 0 then
+		return nil, err_tab_adv(self) .. translate("missing / required")
+	end
+
+	local url = DDNS.parse_url(value)
+	if not (url.scheme == "http" or url.scheme == "https") then
+		return nil, err_tab_adv(self) .. translate("must start with 'http://'")
+	elseif not url.host then
+		return nil, err_tab_adv(self) .. "<HOST> " .. translate("missing / required")
+	elseif SYS.call([[nslookup ]] .. url.host .. [[>/dev/null 2>&1]]) ~= 0 then
+		return nil, err_tab_adv(self) .. translate("can not resolve host: ") .. url.host
+	else
+		return value
+	end
+end
+function iurl6.write(self, section, value)
+	if usev6:formvalue(section) == "0"
+	 or src6:formvalue(section) ~= "web" then
+		-- ignore if IPv4 selected OR
+		-- ignore everything except "web"
+		return true
+	else
+		self.map:del(section, self.option)		-- delete "ipv6_url" helper
+		return self.map:set(section, "ip_url", value)	-- and write "ip_url"
+	end
+end
+
+-- IPv4 + IPv6 - ip_interface -- ###############################################
+ipi = ns:taboption("advanced", ListValue, "ip_interface",
+	translate("Interface"),
+	translate("Defines the interface to read systems IP-Address from") )
+ipi:depends("ipv4_source", "interface")	-- IPv4
+ipi:depends("ipv6_source", "interface")	-- or IPv6
+for _, v in pairs(SYS.net.devices()) do
+	-- show only interface set to a network
+	-- and ignore loopback
+	net = WADM.iface_get_network(v)
+	if net and net ~= "loopback" then
+		ipi:value(v)
+	end
+end
+function ipi.validate(self, value)
+	if (usev6:formvalue(section) == "0" and src4:formvalue(section) ~= "interface")
+	or (usev6:formvalue(section) == "1" and src6:formvalue(section) ~= "interface") then
+		return ""
+	else
+		return value
+	end
+end
+function ipi.write(self, section, value)
+	if (usev6:formvalue(section) == "0" and src4:formvalue(section) ~= "interface")
+	or (usev6:formvalue(section) == "1" and src6:formvalue(section) ~= "interface") then
+		return true
+	else
+		-- get network from device to
+		-- set also as "interface" for monitoring events changes/hotplug
+		local net = WADM.iface_get_network(value)
+		self.map:set(section, "interface", net)
+		return self.map:set(section, self.option, value)
+	end
+end
+
+-- IPv4 + IPv6 - ip_script (NEW) -- ############################################
+ips = ns:taboption("advanced", Value, "ip_script",
+	translate("Script"),
+	translate("User defined script to read systems IP-Address") )
+ips:depends("ipv4_source", "script")	-- IPv4
+ips:depends("ipv6_source", "script")	-- or IPv6
+ips.rmempty	= false
+ips.placeholder = "/path/to/script.sh"
+function ips.validate(self, value)
+	local split
+	if value then split = UTIL.split(value, " ") end
+
+	if (usev6:formvalue(section) == "0" and src4:formvalue(section) ~= "script")
+	or (usev6:formvalue(section) == "1" and src6:formvalue(section) ~= "script") then
+		return ""
+	elseif not value or not (#value > 0) or not FS.access(split[1], "x") then
+		return nil, err_tab_adv(self) ..
+			translate("not found or not executable - Sample: '/path/to/script.sh'")
+	else
+		return value
+	end
+end
+function ips.write(self, section, value)
+	if (usev6:formvalue(section) == "0" and src4:formvalue(section) ~= "script")
+	or (usev6:formvalue(section) == "1" and src6:formvalue(section) ~= "script") then
+		return true
+	else
+		return self.map:set(section, self.option, value)
+	end
+end
+
+-- IPv4 - interface - default "wan" -- #########################################
+-- event network to monitor changes/hotplug/dynamic_dns_updater.sh
+-- only needs to be set if "ip_source"="web" or "script"
+-- if "ip_source"="network" or "interface" we use their network
+eif4 = ns:taboption("advanced", ListValue, "ipv4_interface",
+	translate("Event Network") .. " [IPv4]",
+	translate("Network on which the ddns-updater scripts will be started") )
+eif4:depends("ipv4_source", "web")
+eif4:depends("ipv4_source", "script")
+eif4.default = "wan"
+WADM.cbi_add_networks(eif4)
+function eif4.cfgvalue(self, section)
+	return DDNS.read_value(self, section, "interface")
+end
+function eif4.validate(self, value)
+	if usev6:formvalue(section) == "1"
+	 or src4:formvalue(section) == "network"
+	 or src4:formvalue(section) == "interface" then
+		return ""	-- ignore IPv6, network, interface
+	else
+		return value
+	end
+end
+function eif4.write(self, section, value)
+	if usev6:formvalue(section) == "1"
+	 or src4:formvalue(section) == "network"
+	 or src4:formvalue(section) == "interface" then
+		return true	-- ignore IPv6, network, interface
+	else
+		self.map:del(section, self.option)		 -- delete "ipv4_interface" helper
+		return self.map:set(section, "interface", value) -- and write "interface"
+	end
+end
+
+-- IPv6 - interface (NEW) - default "wan6" -- ##################################
+-- event network to monitor changes/hotplug (NEW)
+-- only needs to be set if "ip_source"="web" or "script"
+-- if "ip_source"="network" or "interface" we use their network
+eif6 = ns:taboption("advanced", ListValue, "ipv6_interface",
+	translate("Event Network") .. " [IPv6]" )
+eif6:depends("ipv6_source", "web")
+eif6:depends("ipv6_source", "script")
+eif6.default = "wan6"
+WADM.cbi_add_networks(eif6)
+if not has_ipv6 then
+	eif6.description = err_ipv6_other
+else
+	eif6.description = translate("Network on which the ddns-updater scripts will be started")
+end
+function eif6.cfgvalue(self, section)
+	return DDNS.read_value(self, section, "interface")
+end
+function eif6.validate(self, value)
+	if usev6:formvalue(section) == "0"
+	 or src4:formvalue(section) == "network"
+	 or src4:formvalue(section) == "interface" then
+		return ""	-- ignore IPv4, network, interface
+	elseif not has_ipv6 then
+		return nil, err_tab_adv(self) .. err_ipv6_plain
+	else
+		return value
+	end
+end
+function eif6.write(self, section, value)
+	if usev6:formvalue(section) == "0"
+	 or src4:formvalue(section) == "network"
+	 or src4:formvalue(section) == "interface" then
+		return true	-- ignore IPv4, network, interface
+	else
+		self.map:del(section, self.option)		 -- delete "ipv6_interface" helper
+		return self.map:set(section, "interface", value) -- and write "interface"
+	end
+end
+
+-- IPv4 + IPv6 - force_ipversion (NEW) -- ######################################
+-- optional to force wget/curl and host to use only selected IP version
+-- command parameter "-4" or "-6"
+if has_force or ( ( m:get(section, "force_ipversion") or "0" ) ~= "0" ) then
+	fipv = ns:taboption("advanced", Flag, "force_ipversion",
+		translate("Force IP Version") )
+	fipv.orientation = "horizontal"
+	function fipv.cfgvalue(self, section)
+		local value = AbstractValue.cfgvalue(self, section)
+		if not has_force and value ~= "0" then
+			self.description = bold_on .. font_red ..
+				translate("Force IP Version not supported") .. font_off .. "<br />" ..
+				translate("please disable") .. " !" .. bold_off
+		else
+			self.description = translate("OPTIONAL: Force the usage of pure IPv4/IPv6 only communication.")
+		end
+		return value
+	end
+	function fipv.validate(self, value)
+		if (value == "1" and has_force) or value == "0" then return value end
+		return nil, err_tab_adv(self) .. translate("Force IP Version not supported")
+	end
+	function fipv.parse(self, section)
+		DDNS.flag_parse(self, section)
+	end
+	function fipv.write(self, section, value)
+		if value == "1" then
+			return self.map:set(section, self.option, value)
+		else
+			return self.map:del(section, self.option)
+		end
+	end
+end
+
+-- IPv4 + IPv6 - dns_server (NEW) -- ###########################################
+-- optional DNS Server to use resolving my IP if "ip_source"="web"
+dns = ns:taboption("advanced", Value, "dns_server",
+	translate("DNS-Server"),
+	translate("OPTIONAL: Use non-default DNS-Server to detect 'Registered IP'.") .. "<br />" ..
+	translate("Format: IP or FQDN"))
+dns.placeholder = "mydns.lan"
+function dns.validate(self, value)
+	-- if .datatype is set, then it is checked before calling this function
+	if not value then
+		return ""	-- ignore on empty
+	elseif not DTYP.host(value) then
+		return nil, err_tab_adv(self) .. translate("use hostname, FQDN, IPv4- or IPv6-Address")
+	else
+		local ipv6  = usev6:formvalue(section)
+		local force = (fipv) and fipv:formvalue(section) or "0"
+		local command = [[/usr/lib/ddns/dynamic_dns_lucihelper.sh verify_dns ]] ..
+			value .. [[ ]] .. ipv6 .. [[ ]] .. force
+		local ret = SYS.call(command)
+		if     ret == 0 then return value	-- everything OK
+		elseif ret == 2 then return nil, err_tab_adv(self) .. translate("nslookup can not resolve host")
+		elseif ret == 3 then return nil, err_tab_adv(self) .. translate("nc (netcat) can not connect")
+		elseif ret == 4 then return nil, err_tab_adv(self) .. translate("Forced IP Version don't matched")
+		else                 return nil, err_tab_adv(self) .. translate("unspecific error")
+		end
+	end
+end
+
+-- IPv4 + IPv6 - force_dnstcp (NEW) -- #########################################
+if has_dnstcp or ( ( m:get(section, "force_dnstcp") or "0" ) ~= "0" ) then
+	tcp = ns:taboption("advanced", Flag, "force_dnstcp",
+		translate("Force TCP on DNS") )
+	tcp.orientation = "horizontal"
+	function tcp.cfgvalue(self, section)
+		local value = AbstractValue.cfgvalue(self, section)
+		if not has_dnstcp and value ~= "0" then
+			self.description = bold_on .. font_red ..
+				translate("DNS requests via TCP not supported") .. font_off .. "<br />" ..
+				translate("please disable") .. " !" .. bold_off
+		else
+			self.description = translate("OPTIONAL: Force the use of TCP instead of default UDP on DNS requests.")
+		end
+		return value
+	end
+	function tcp.validate(self, value)
+		if (value == "1" and has_dnstcp ) or value == "0" then
+			return value
+		end
+		return nil, err_tab_adv(self) .. translate("DNS requests via TCP not supported")
+	end
+	function tcp.parse(self, section)
+		DDNS.flag_parse(self, section)
+	end
+end
+
+-- IPv4 + IPv6 - proxy (NEW) -- ################################################
+-- optional Proxy to use for http/https requests  [user:password@]proxyhost[:port]
+if has_proxy or ( ( m:get(section, "proxy") or "" ) ~= "" ) then
+	pxy = ns:taboption("advanced", Value, "proxy",
+		translate("PROXY-Server") )
+	pxy.placeholder="user:password@myproxy.lan:8080"
+	function pxy.cfgvalue(self, section)
+		local value = AbstractValue.cfgvalue(self, section)
+		if not has_proxy and value ~= "" then
+			self.description = bold_on .. font_red ..
+				translate("PROXY-Server not supported") .. font_off .. "<br />" ..
+				translate("please remove entry") .. "!" .. bold_off
+		else
+			self.description = translate("OPTIONAL: Proxy-Server for detection and updates.") .. "<br />" ..
+				translate("Format") .. ": " .. bold_on .. "[user:password@]proxyhost:port" .. bold_off .. "<br />" ..
+				translate("IPv6 address must be given in square brackets") .. ": " ..
+				bold_on .. " [2001:db8::1]:8080" .. bold_off
+		end
+		return value
+	end
+	function pxy.validate(self, value)
+		-- if .datatype is set, then it is checked before calling this function
+		if not value then
+			return ""	-- ignore on empty
+		elseif has_proxy then
+			local ipv6  = usev6:formvalue(section) or "0"
+			local force = (fipv) and fipv:formvalue(section) or "0"
+			local command = [[/usr/lib/ddns/dynamic_dns_lucihelper.sh verify_proxy ]] ..
+				value .. [[ ]] .. ipv6 .. [[ ]] .. force
+			local ret = SYS.call(command)
+			if     ret == 0 then return value
+			elseif ret == 2 then return nil, err_tab_adv(self) .. translate("nslookup can not resolve host")
+			elseif ret == 3 then return nil, err_tab_adv(self) .. translate("nc (netcat) can not connect")
+			elseif ret == 4 then return nil, err_tab_adv(self) .. translate("Forced IP Version don't matched")
+			elseif ret == 5 then return nil, err_tab_adv(self) .. translate("proxy port missing")
+			else                 return nil, err_tab_adv(self) .. translate("unspecific error")
+			end
+		else
+			return nil, err_tab_adv(self) .. translate("PROXY-Server not supported")
+		end
+	end
+end
+
+-- use_syslog -- ###############################################################
+slog = ns:taboption("advanced", ListValue, "use_syslog",
+	translate("Log to syslog"),
+	translate("Writes log messages to syslog. Critical Errors will always be written to syslog.") )
+slog.default = "2"
+slog:value("0", translate("No logging"))
+slog:value("1", translate("Info"))
+slog:value("2", translate("Notice"))
+slog:value("3", translate("Warning"))
+slog:value("4", translate("Error"))
+
+-- use_logfile (NEW) -- ########################################################
+logf = ns:taboption("advanced", Flag, "use_logfile",
+	translate("Log to file"),
+	translate("Writes detailed messages to log file. File will be truncated automatically.") .. "<br />" ..
+	translate("File") .. [[: "]] .. log_dir .. [[/]] .. section .. [[.log"]] )
+logf.orientation = "horizontal"
+logf.rmempty = false	-- we want to save in /etc/config/ddns file on "0" because
+logf.default = "1"	-- if not defined write to log by default
+function logf.parse(self, section)
+	DDNS.flag_parse(self, section)
+end
+
+-- TAB: Timer  #####################################################################################
+-- check_interval -- ###########################################################
+ci = ns:taboption("timer", Value, "check_interval",
+	translate("Check Interval") )
+ci.template = "ddns/detail_value"
+ci.default  = 10
+ci.rmempty = false	-- validate ourselves for translatable error messages
+function ci.validate(self, value)
+	if not DTYP.uinteger(value)
+	or tonumber(value) < 1 then
+		return nil, err_tab_timer(self) .. translate("minimum value 5 minutes == 300 seconds")
+	end
+
+	local secs = DDNS.calc_seconds(value, cu:formvalue(section))
+	if secs >= 300 then
+		return value
+	else
+		return nil, err_tab_timer(self) .. translate("minimum value 5 minutes == 300 seconds")
+	end
+end
+function ci.write(self, section, value)
+	-- simulate rmempty=true remove default
+	local secs = DDNS.calc_seconds(value, cu:formvalue(section))
+	if secs ~= 600 then	--default 10 minutes
+		return self.map:set(section, self.option, value)
+	else
+		self.map:del(section, "check_unit")
+		return self.map:del(section, self.option)
+	end
+end
+
+-- check_unit -- ###############################################################
+cu = ns:taboption("timer", ListValue, "check_unit", "not displayed, but needed otherwise error",
+	translate("Interval to check for changed IP" .. "<br />" ..
+		"Values below 5 minutes == 300 seconds are not supported") )
+cu.template = "ddns/detail_lvalue"
+cu.default  = "minutes"
+cu.rmempty  = false	-- want to control write process
+cu:value("seconds", translate("seconds"))
+cu:value("minutes", translate("minutes"))
+cu:value("hours", translate("hours"))
+--cu:value("days", translate("days"))
+function cu.write(self, section, value)
+	-- simulate rmempty=true remove default
+	local secs = DDNS.calc_seconds(ci:formvalue(section), value)
+	if secs ~= 600 then	--default 10 minutes
+		return self.map:set(section, self.option, value)
+	else
+		return true
+	end
+end
+
+-- force_interval (modified) -- ################################################
+fi = ns:taboption("timer", Value, "force_interval",
+	translate("Force Interval") )
+fi.template = "ddns/detail_value"
+fi.default  = 72 	-- see dynamic_dns_updater.sh script
+fi.rmempty = false	-- validate ourselves for translatable error messages
+function fi.validate(self, value)
+	if not DTYP.uinteger(value)
+	or tonumber(value) < 0 then
+		return nil, err_tab_timer(self) .. translate("minimum value '0'")
+	end
+
+	local force_s = DDNS.calc_seconds(value, fu:formvalue(section))
+	if force_s == 0 then
+		return value
+	end
+
+	local ci_value = ci:formvalue(section)
+	if not DTYP.uinteger(ci_value) then
+		return ""	-- ignore because error in check_interval above
+	end
+
+	local check_s = DDNS.calc_seconds(ci_value, cu:formvalue(section))
+	if force_s >= check_s then
+		return value
+	end
+
+	return nil, err_tab_timer(self) .. translate("must be greater or equal 'Check Interval'")
+end
+function fi.write(self, section, value)
+	-- simulate rmempty=true remove default
+	local secs = DDNS.calc_seconds(value, fu:formvalue(section))
+	if secs ~= 259200 then	--default 72 hours == 3 days
+		return self.map:set(section, self.option, value)
+	else
+		self.map:del(section, "force_unit")
+		return self.map:del(section, self.option)
+	end
+end
+
+-- force_unit -- ###############################################################
+fu = ns:taboption("timer", ListValue, "force_unit", "not displayed, but needed otherwise error",
+	translate("Interval to force updates send to DDNS Provider" .. "<br />" ..
+		"Setting this parameter to 0 will force the script to only run once" .. "<br />" ..
+		"Values lower 'Check Interval' except '0' are not supported") )
+fu.template = "ddns/detail_lvalue"
+fu.default  = "hours"
+fu.rmempty  = false	-- want to control write process
+--fu:value("seconds", translate("seconds"))
+fu:value("minutes", translate("minutes"))
+fu:value("hours", translate("hours"))
+fu:value("days", translate("days"))
+function fu.write(self, section, value)
+	-- simulate rmempty=true remove default
+	local secs = DDNS.calc_seconds(fi:formvalue(section), value)
+	if secs ~= 259200 and secs ~= 0 then	--default 72 hours == 3 days
+		return self.map:set(section, self.option, value)
+	else
+		return true
+	end
+end
+
+-- retry_count (NEW) -- ########################################################
+rc = ns:taboption("timer", Value, "retry_count",
+	translate("Error Retry Counter"),
+	translate("On Error the script will stop execution after given number of retrys") )
+rc.default = 5
+rc.rmempty = false	-- validate ourselves for translatable error messages
+function rc.validate(self, value)
+	if not DTYP.uinteger(value) then
+		return nil, err_tab_timer(self) .. translate("minimum value '0'")
+	else
+		return value
+	end
+end
+function rc.write(self, section, value)
+	-- simulate rmempty=true remove default
+	if tonumber(value) ~= self.default then
+		return self.map:set(section, self.option, value)
+	else
+		return self.map:del(section, self.option)
+	end
+end
+
+-- retry_interval -- ###########################################################
+ri = ns:taboption("timer", Value, "retry_interval",
+	translate("Error Retry Interval") )
+ri.template = "ddns/detail_value"
+ri.default  = 60
+ri.rmempty  = false	-- validate ourselves for translatable error messages
+function ri.validate(self, value)
+	if not DTYP.uinteger(value)
+	or tonumber(value) < 1 then
+		return nil, err_tab_timer(self) .. translate("minimum value '1'")
+	else
+		return value
+	end
+end
+function ri.write(self, section, value)
+	-- simulate rmempty=true remove default
+	local secs = DDNS.calc_seconds(value, ru:formvalue(section))
+	if secs ~= 60 then	--default 60seconds
+		return self.map:set(section, self.option, value)
+	else
+		self.map:del(section, "retry_unit")
+		return self.map:del(section, self.option)
+	end
+end
+
+-- retry_unit -- ###############################################################
+ru = ns:taboption("timer", ListValue, "retry_unit", "not displayed, but needed otherwise error",
+	translate("On Error the script will retry the failed action after given time") )
+ru.template = "ddns/detail_lvalue"
+ru.default  = "seconds"
+ru.rmempty  = false	-- want to control write process
+ru:value("seconds", translate("seconds"))
+ru:value("minutes", translate("minutes"))
+--ru:value("hours", translate("hours"))
+--ru:value("days", translate("days"))
+function ru.write(self, section, value)
+	-- simulate rmempty=true remove default
+	local secs = DDNS.calc_seconds(ri:formvalue(section), value)
+	if secs ~= 60 then	--default 60seconds
+		return self.map:set(section, self.option, value)
+	else
+		return true -- will be deleted by retry_interval
+	end
+end
+
+-- TAB: LogView  (NEW) #############################################################################
+lv = ns:taboption("logview", DummyValue, "_logview")
+lv.template = "ddns/detail_logview"
+lv.inputtitle = translate("Read / Reread log file")
+lv.rows = 50
+function lv.cfgvalue(self, section)
+	local lfile=log_dir .. "/" .. section .. ".log"
+	if FS.access(lfile) then
+		return lfile .. "\n" .. translate("Please press [Read] button")
+	end
+	return lfile .. "\n" .. translate("File not found or empty")
+end
+
+return m

+ 147 - 0
luci/applications/luci-ddns/luasrc/model/cbi/ddns/hints.lua

@@ -0,0 +1,147 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2014 Christian Schoenebeck <christian dot schoenebeck at gmail dot com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+local CTRL = require "luci.controller.ddns"	-- this application's controller
+local DISP = require "luci.dispatcher"
+local SYS  = require "luci.sys"
+local DDNS = require "luci.tools.ddns"		-- ddns multiused functions
+
+-- check supported options -- ##################################################
+-- saved to local vars here because doing multiple os calls slow down the system
+has_ssl     = DDNS.check_ssl()		-- HTTPS support
+has_proxy   = DDNS.check_proxy()	-- Proxy support
+has_dnstcp  = DDNS.check_bind_host()	-- DNS TCP support
+need_update = CTRL.update_needed()	-- correct ddns-scripts version
+
+-- html constants
+font_red = [[<font color="red">]]
+font_off = [[</font>]]
+bold_on  = [[<strong>]]
+bold_off = [[</strong>]]
+
+-- cbi-map definition -- #######################################################
+m = Map("ddns")
+
+-- first need to close <a> from cbi map template our <a> closed by template
+m.title = [[</a><a href="]] .. DISP.build_url("admin", "services", "ddns") .. [[">]] ..
+		translate("Dynamic DNS")
+
+m.description = translate("Dynamic DNS allows that your router can be reached with " ..
+			"a fixed hostname while having a dynamically changing " ..
+			"IP address.")
+
+m.redirect = DISP.build_url("admin", "services", "ddns")
+
+-- SimpleSection definition -- #################################################
+-- show Hints to optimize installation and script usage
+s = m:section( SimpleSection,
+	translate("Hints"),
+	translate("Below a list of configuration tips for your system to run Dynamic DNS updates without limitations") )
+
+-- ddns_scripts needs to be updated for full functionality
+if need_update then
+	local dv = s:option(DummyValue, "_update_needed")
+	dv.titleref = DISP.build_url("admin", "system", "packages")
+	dv.rawhtml  = true
+	dv.title = font_red .. bold_on ..
+		translate("Software update required") .. bold_off .. font_off
+	dv.value = translate("The currently installed 'ddns-scripts' package did not support all available settings.") ..
+			"<br />" ..
+			translate("Please update to the current version!")
+end
+
+-- DDNS Service disabled
+if not SYS.init.enabled("ddns") then
+	local dv = s:option(DummyValue, "_not_enabled")
+	dv.titleref = DISP.build_url("admin", "system", "startup")
+	dv.rawhtml  = true
+	dv.title = bold_on ..
+		translate("DDNS Autostart disabled") .. bold_off
+	dv.value = translate("Currently DDNS updates are not started at boot or on interface events." .. "<br />" ..
+			"This is the default if you run DDNS scripts by yourself (i.e. via cron with force_interval set to '0')" )
+end
+
+-- No IPv6 support
+if not DDNS.check_ipv6() then
+	local dv = s:option(DummyValue, "_no_ipv6")
+	dv.titleref = 'http://www.librecmc.org" target="_blank'
+	dv.rawhtml  = true
+	dv.title = bold_on ..
+		translate("IPv6 not supported") .. bold_off
+	dv.value = translate("IPv6 is currently not (fully) supported by this system" .. "<br />" ..
+			"Please follow the instructions on libreCMC's homepage to enable IPv6 support" .. "<br />" ..
+			"or update your system to the latest libreCMC Release")
+end
+
+-- No HTTPS support
+if not has_ssl then
+	local dv = s:option(DummyValue, "_no_https")
+	dv.titleref = DISP.build_url("admin", "system", "packages")
+	dv.rawhtml  = true
+	dv.title = bold_on ..
+		translate("HTTPS not supported") .. bold_off
+	dv.value = translate("Neither GNU Wget with SSL nor cURL installed to support updates via HTTPS protocol.") ..
+			"<br />- " ..
+			translate("You should install GNU Wget with SSL (prefered) or cURL package.") ..
+			"<br />- " ..
+			translate("In some versions cURL/libcurl in libreCMC is compiled without proxy support.")
+end
+
+-- cURL without proxy support
+if has_ssl and not has_proxy then
+	local dv = s:option(DummyValue, "_no_proxy")
+	dv.titleref = DISP.build_url("admin", "system", "packages")
+	dv.rawhtml  = true
+	dv.title = bold_on ..
+		translate("cURL without Proxy Support") .. bold_off
+	dv.value = translate("cURL is installed, but libcurl was compiled without proxy support.") ..
+			"<br />- " ..
+			translate("You should install GNU Wget with SSL or replace libcurl.") ..
+			"<br />- " ..
+			translate("In some versions cURL/libcurl in libreCMC is compiled without proxy support.")
+end
+
+-- "Force IP Version not supported"
+if not (has_ssl and has_dnstcp) then
+	local dv = s:option(DummyValue, "_no_force_ip")
+	dv.titleref = DISP.build_url("admin", "system", "packages")
+	dv.rawhtml  = true
+	dv.title = bold_on ..
+		translate("Force IP Version not supported") .. bold_off
+	local value = translate("BusyBox's nslookup and Wget do not support to specify " ..
+			"the IP version to use for communication with DDNS Provider.")
+	if not has_ssl then
+		value = value .. "<br />- " ..
+			translate("You should install GNU Wget with SSL (prefered) or cURL package.")
+	end
+	if not has_dnstcp then
+		value = value .. "<br />- " ..
+			translate("You should install BIND host package for DNS requests.")
+	end
+	dv.value = value
+end
+
+-- "DNS requests via TCP not supported"
+if not has_dnstcp then
+	local dv = s:option(DummyValue, "_no_dnstcp")
+	dv.titleref = DISP.build_url("admin", "system", "packages")
+	dv.rawhtml  = true
+	dv.title = bold_on ..
+		translate("DNS requests via TCP not supported") .. bold_off
+	dv.value = translate("BusyBox's nslookup does not support to specify to use TCP instead of default UDP when requesting DNS server") ..
+			"<br />- " ..
+			translate("You should install BIND host package for DNS requests.")
+end
+
+return m

+ 256 - 0
luci/applications/luci-ddns/luasrc/model/cbi/ddns/overview.lua

@@ -0,0 +1,256 @@
+--[[
+LuCI - Lua Configuration Interface
+
+Copyright 2014 Christian Schoenebeck <christian dot schoenebeck at gmail dot com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+local NXFS = require "nixio.fs"
+local CTRL = require "luci.controller.ddns"	-- this application's controller
+local DISP = require "luci.dispatcher"
+local HTTP = require "luci.http"
+local SYS  = require "luci.sys"
+local DDNS = require "luci.tools.ddns"		-- ddns multiused functions
+
+-- show hints ?
+show_hints = not (DDNS.check_ipv6()		-- IPv6 support
+		and DDNS.check_ssl()		-- HTTPS support
+		and DDNS.check_proxy()		-- Proxy support
+		and DDNS.check_bind_host()	-- DNS TCP support
+		)
+need_update = CTRL.update_needed()		-- correct ddns-scripts version
+
+-- html constants
+font_red = [[<font color="red">]]
+font_off = [[</font>]]
+bold_on  = [[<strong>]]
+bold_off = [[</strong>]]
+
+-- cbi-map definition -- #######################################################
+m = Map("ddns")
+
+-- first need to close <a> from cbi map template our <a> closed by template
+--m.title = [[</a><a href="javascript:alert(']] .. CTRL.show_versions() ..[[')">]] ..
+--		translate("Dynamic DNS")
+m.title = [[</a><a href="#" onclick="onclick_maptitle();">]] ..
+		translate("Dynamic DNS")
+
+m.description = translate("Dynamic DNS allows that your router can be reached with " ..
+			"a fixed hostname while having a dynamically changing " ..
+			"IP address.")
+
+m.on_after_commit = function(self)
+	if self.changed then	-- changes ?
+		if SYS.init.enabled("ddns") then	-- ddns service enabled, restart all
+			os.execute("/etc/init.d/ddns restart")
+		else	-- ddns service disabled, send SIGHUP to running
+			os.execute("killall -1 dynamic_dns_updater.sh")
+		end
+	end
+end
+
+-- SimpleSection definiton -- ##################################################
+-- with all the JavaScripts we need for "a good Show"
+a = m:section( SimpleSection )
+a.template = "ddns/overview_status"
+
+-- SimpleSection definition -- #################################################
+-- show Hints to optimize installation and script usage
+-- only show if 	service not enabled
+--		or	no IPv6 support
+--		or	not GNU Wget and not cURL	(for https support)
+--		or	not GNU Wget but cURL without proxy support
+--		or	not BIND's host
+--		or	ddns-scripts package need update
+if show_hints or need_update or not SYS.init.enabled("ddns") then
+	s = m:section( SimpleSection, translate("Hints") )
+
+	-- ddns_scripts needs to be updated for full functionality
+	if need_update then
+		local dv = s:option(DummyValue, "_update_needed")
+		dv.titleref = DISP.build_url("admin", "system", "packages")
+		dv.rawhtml  = true
+		dv.title = font_red .. bold_on ..
+			translate("Software update required") .. bold_off .. font_off
+		dv.value = translate("The currently installed 'ddns-scripts' package did not support all available settings.") ..
+				"<br />" ..
+				translate("Please update to the current version!")
+	end
+
+	-- DDNS Service disabled
+	if not SYS.init.enabled("ddns") then
+		local dv = s:option(DummyValue, "_not_enabled")
+		dv.titleref = DISP.build_url("admin", "system", "startup")
+		dv.rawhtml  = true
+		dv.title = bold_on ..
+			translate("DDNS Autostart disabled") .. bold_off
+		dv.value = translate("Currently DDNS updates are not started at boot or on interface events." .. "<br />" ..
+				"You can start/stop each configuration here. It will run until next reboot.")
+	end
+
+	-- Show more hints on a separate page
+	if show_hints then
+		local dv = s:option(DummyValue, "_separate")
+		dv.titleref = DISP.build_url("admin", "services", "ddns", "hints")
+		dv.rawhtml  = true
+		dv.title = bold_on ..
+			translate("Show more") .. bold_off
+		dv.value = translate("Follow this link" .. "<br />" ..
+				"You will find more hints to optimize your system to run DDNS scripts with all options")
+	end
+end
+
+-- TableSection definition -- ##################################################
+ts = m:section( TypedSection, "service",
+	translate("Overview"),
+	translate("Below is a list of configured DDNS configurations and their current state." .. "<br />" ..
+		"If you want to send updates for IPv4 and IPv6 you need to define two separate Configurations " ..
+		"i.e. 'myddns_ipv4' and 'myddns_ipv6'") )
+ts.sectionhead = translate("Configuration")
+ts.template = "cbi/tblsection"
+ts.addremove = true
+ts.extedit = DISP.build_url("admin", "services", "ddns", "detail", "%s")
+function ts.create(self, name)
+	AbstractSection.create(self, name)
+	HTTP.redirect( self.extedit:format(name) )
+end
+
+-- Domain and registered IP -- #################################################
+dom = ts:option(DummyValue, "_domainIP",
+	translate("Hostname/Domain") .. "<br />" .. translate("Registered IP") )
+dom.template = "ddns/overview_doubleline"
+function dom.set_one(self, section)
+	local domain = self.map:get(section, "domain") or ""
+	if domain ~= "" then
+		return domain
+	else
+		return [[<em>]] .. translate("config error") .. [[</em>]]
+	end
+end
+function dom.set_two(self, section)
+	local domain = self.map:get(section, "domain") or ""
+	if domain == "" then return "" end
+	local dnsserver = self.map:get(section, "dnsserver") or ""
+	local use_ipv6 = tonumber(self.map:get(section, "use_ipv6") or 0)
+	local force_ipversion = tonumber(self.map:get(section, "force_ipversion") or 0)
+	local force_dnstcp = tonumber(self.map:get(section, "force_dnstcp") or 0)
+	local command = [[/usr/lib/ddns/dynamic_dns_lucihelper.sh]]
+	if not NXFS.access(command, "rwx", "rx", "rx") then
+		NXFS.chmod(command, 755)
+	end
+	command = command .. [[ get_registered_ip ]] .. domain .. [[ ]] .. use_ipv6 ..
+		[[ ]] .. force_ipversion .. [[ ]] .. force_dnstcp .. [[ ]] .. dnsserver
+	local ip = SYS.exec(command)
+	if ip == "" then ip = translate("no data") end
+	return ip
+end
+
+-- enabled
+ena = ts:option( Flag, "enabled",
+	translate("Enabled"))
+ena.template = "ddns/overview_enabled"
+ena.rmempty = false
+function ena.parse(self, section)
+	DDNS.flag_parse(self, section)
+end
+
+-- show PID and next update
+upd = ts:option( DummyValue, "_update",
+	translate("Last Update") .. "<br />" .. translate("Next Update"))
+upd.template = "ddns/overview_doubleline"
+function upd.set_one(self, section)	-- fill Last Update
+	-- get/validate last update
+	local uptime   = SYS.uptime()
+	local lasttime = DDNS.get_lastupd(section)
+	if lasttime > uptime then 	-- /var might not be linked to /tmp and cleared on reboot
+		lasttime = 0
+	end
+
+	-- no last update happen
+	if lasttime == 0 then
+		return translate("never")
+
+	-- we read last update
+	else
+		-- calc last update
+		--            os.epoch  - sys.uptime + lastupdate(uptime)
+		local epoch = os.time() - uptime + lasttime
+		-- use linux date to convert epoch
+		return DDNS.epoch2date(epoch)
+	end
+end
+function upd.set_two(self, section)	-- fill Next Update
+	-- get enabled state
+	local enabled	= tonumber(self.map:get(section, "enabled") or 0)
+	local datenext	= translate("unknown error")	-- formatted date of next update
+
+	-- get force seconds
+	local force_interval = tonumber(self.map:get(section, "force_interval") or 72)
+	local force_unit = self.map:get(section, "force_unit") or "hours"
+	local force_seconds = DDNS.calc_seconds(force_interval, force_unit)
+
+	-- get last update and get/validate PID
+	local uptime   = SYS.uptime()
+	local lasttime = DDNS.get_lastupd(section)
+	if lasttime > uptime then 	-- /var might not be linked to /tmp and cleared on reboot
+		lasttime = 0
+	end
+	local pid      = DDNS.get_pid(section)
+
+	-- calc next update
+	if lasttime > 0 then
+		local epoch = os.time() - uptime + lasttime + force_seconds
+		-- use linux date to convert epoch
+		datelast = DDNS.epoch2date(epoch)
+	end
+
+	-- process running but update needs to happen
+	if pid > 0 and ( lasttime + force_seconds - uptime ) < 0 then
+		datenext = translate("Verify")
+
+	-- run once
+	elseif force_seconds == 0 then
+		datenext = translate("Run once")
+
+	-- no process running and NOT enabled
+	elseif pid == 0 and enabled == 0 then
+		datenext  = translate("Disabled")
+
+	-- no process running and NOT
+	elseif pid == 0 and enabled ~= 0 then
+		datenext = translate("Stopped")
+	end
+
+	return datenext
+end
+
+-- start/stop button
+btn = ts:option( Button, "_startstop",
+	translate("Process ID") .. "<br />" .. translate("Start / Stop") )
+btn.template = "ddns/overview_startstop"
+function btn.cfgvalue(self, section)
+	local pid = DDNS.get_pid(section)
+	if pid > 0 then
+		btn.inputtitle	= "PID: " .. pid
+		btn.inputstyle	= "reset"
+		btn.disabled	= false
+	elseif (self.map:get(section, "enabled") or "0") ~= "0" then
+		btn.inputtitle	= translate("Start")
+		btn.inputstyle	= "apply"
+		btn.disabled	= false
+	else
+		btn.inputtitle	= "----------"
+		btn.inputstyle	= "button"
+		btn.disabled	= true
+	end
+	return true
+end
+
+return m

+ 295 - 0
luci/applications/luci-ddns/luasrc/tools/ddns.lua

@@ -0,0 +1,295 @@
+--[[
+LuCI - Lua Configuration Interface
+
+shared module for luci-app-ddns
+Copyright 2014 Christian Schoenebeck <christian dot schoenebeck at gmail dot com>
+
+function parse_url copied from https://svn.nmap.org/nmap/nselib/url.lua
+Parses a URL and returns a table with all its parts according to RFC 2396.
+@author Diego Nehab	@author Eddie Bell <ejlbell@gmail.com>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+]]--
+
+module("luci.tools.ddns", package.seeall)
+
+local NX   = require "nixio"
+local NXFS = require "nixio.fs"
+local OPKG = require "luci.model.ipkg"
+local UCI  = require "luci.model.uci"
+local SYS  = require "luci.sys"
+local UTIL = require "luci.util"
+
+-- function to calculate seconds from given interval and unit
+function calc_seconds(interval, unit)
+	if not tonumber(interval) then
+		return nil
+	elseif unit == "days" then
+		return (tonumber(interval) * 86400)	-- 60 sec * 60 min * 24 h
+	elseif unit == "hours" then
+		return (tonumber(interval) * 3600)	-- 60 sec * 60 min
+	elseif unit == "minutes" then
+		return (tonumber(interval) * 60)	-- 60 sec
+	elseif unit == "seconds" then
+		return tonumber(interval)
+	else
+		return nil
+	end
+end
+
+-- check if IPv6 supported by libreCMC
+function check_ipv6()
+	return NXFS.access("/proc/net/ipv6_route")
+	   and NXFS.access("/usr/sbin/ip6tables")
+end
+
+-- check if Wget with SSL support or cURL installed
+function check_ssl()
+	if (SYS.call([[ grep -iq "\+ssl" /usr/bin/wget 2>/dev/null ]]) == 0) then
+		return true
+	else
+		return NXFS.access("/usr/bin/curl")
+	end
+end
+
+-- check if Wget with SSL or cURL with proxy support installed
+function check_proxy()
+	-- we prefere GNU Wget for communication
+	if (SYS.call([[ grep -iq "\+ssl" /usr/bin/wget 2>/dev/null ]]) == 0) then
+		return true
+
+	-- if not installed cURL must support proxy
+	elseif NXFS.access("/usr/bin/curl") then
+		return (SYS.call([[ grep -iq all_proxy /usr/lib/libcurl.so* 2>/dev/null ]]) == 0)
+
+	-- only BusyBox Wget is installed
+	else
+		return NXFS.access("/usr/bin/wget")
+	end
+end
+
+-- check if BIND host installed
+function check_bind_host()
+	return NXFS.access("/usr/bin/host")
+end
+
+-- convert epoch date to given format
+function epoch2date(epoch, format)
+	if not format or #format < 2 then
+		local uci = UCI.cursor()
+		format    = uci:get("ddns", "global", "date_format") or "%F %R"
+		uci:unload("ddns")
+	end
+	format = format:gsub("%%n", "<br />")	-- replace newline
+	format = format:gsub("%%t", "    ")	-- replace tab
+	return os.date(format, epoch)
+end
+
+-- read lastupdate from [section].update file
+function get_lastupd(section)
+	local uci     = UCI.cursor()
+	local run_dir = uci:get("ddns", "global", "run_dir") or "/var/run/ddns"
+	local etime   = tonumber(NXFS.readfile("%s/%s.update" % { run_dir, section } ) or 0 )
+	uci:unload("ddns")
+	return etime
+end
+
+-- read PID from run file and verify if still running
+function get_pid(section)
+	local uci     = UCI.cursor()
+	local run_dir = uci:get("ddns", "global", "run_dir") or "/var/run/ddns"
+	local pid     = tonumber(NXFS.readfile("%s/%s.pid" % { run_dir, section } ) or 0 )
+	if pid > 0 and not NX.kill(pid, 0) then
+		pid = 0
+	end
+	uci:unload("ddns")
+	return pid
+end
+
+-- read version information for given package if installed
+function ipkg_version(package)
+	if not package then
+		return nil
+	end
+	local info = OPKG.info(package)
+	local data = {}
+	local version = ""
+	local i = 0
+	for k, v in pairs(info) do
+		if v.Package == package and v.Status.installed then
+			version = v.Version
+			i = i + 1
+		end
+	end
+	if i > 1 then	-- more then one valid record
+		return data
+	end
+	local sver = UTIL.split(version, "[%.%-]", nil, true)
+	data = {
+		version = version,
+		major   = tonumber(sver[1]) or 0,
+		minor   = tonumber(sver[2]) or 0,
+		patch   = tonumber(sver[3]) or 0,
+		build   = tonumber(sver[4]) or 0
+	}
+	return data
+end
+
+-- replacement of build-in read of UCI option
+-- modified AbstractValue.cfgvalue(self, section) from cbi.lua
+-- needed to read from other option then current value definition
+function read_value(self, section, option)
+	local value
+	if self.tag_error[section] then
+		value = self:formvalue(section)
+	else
+		value = self.map:get(section, option)
+	end
+
+	if not value then
+		return nil
+	elseif not self.cast or self.cast == type(value) then
+		return value
+	elseif self.cast == "string" then
+		if type(value) == "table" then
+			return value[1]
+		end
+	elseif self.cast == "table" then
+		return { value }
+	end
+end
+
+-- replacement of build-in Flag.parse of cbi.lua
+-- modified to mark section as changed if value changes
+-- current parse did not do this, but it is done AbstaractValue.parse()
+function flag_parse(self, section)
+	local fexists = self.map:formvalue(
+		luci.cbi.FEXIST_PREFIX .. self.config .. "." .. section .. "." .. self.option)
+
+	if fexists then
+		local fvalue = self:formvalue(section) and self.enabled or self.disabled
+		local cvalue = self:cfgvalue(section)
+		if fvalue ~= self.default or (not self.optional and not self.rmempty) then
+			self:write(section, fvalue)
+		else
+			self:remove(section)
+		end
+		if (fvalue ~= cvalue) then self.section.changed = true end
+	else
+		self:remove(section)
+		self.section.changed = true
+	end
+end
+
+-----------------------------------------------------------------------------
+-- copied from https://svn.nmap.org/nmap/nselib/url.lua
+-- @author Diego Nehab
+-- @author Eddie Bell <ejlbell@gmail.com>
+--[[
+    URI parsing, composition and relative URL resolution
+    LuaSocket toolkit.
+    Author: Diego Nehab
+    RCS ID: $Id: url.lua,v 1.37 2005/11/22 08:33:29 diego Exp $
+    parse_query and build_query added For nmap (Eddie Bell <ejlbell@gmail.com>)
+]]--
+---
+-- Parses a URL and returns a table with all its parts according to RFC 2396.
+--
+-- The following grammar describes the names given to the URL parts.
+-- <code>
+-- <url> ::= <scheme>://<authority>/<path>;<params>?<query>#<fragment>
+-- <authority> ::= <userinfo>@<host>:<port>
+-- <userinfo> ::= <user>[:<password>]
+-- <path> :: = {<segment>/}<segment>
+-- </code>
+--
+-- The leading <code>/</code> in <code>/<path></code> is considered part of
+-- <code><path></code>.
+-- @param url URL of request.
+-- @param default Table with default values for each field.
+-- @return A table with the following fields, where RFC naming conventions have
+--   been preserved:
+--     <code>scheme</code>, <code>authority</code>, <code>userinfo</code>,
+--     <code>user</code>, <code>password</code>, <code>host</code>,
+--     <code>port</code>, <code>path</code>, <code>params</code>,
+--     <code>query</code>, and <code>fragment</code>.
+-----------------------------------------------------------------------------
+function parse_url(url)	--, default)
+	-- initialize default parameters
+	local parsed = {}
+--	for i,v in base.pairs(default or parsed) do
+--		parsed[i] = v
+--	end
+
+	-- remove whitespace
+--	url = string.gsub(url, "%s", "")
+	-- get fragment
+	url = string.gsub(url, "#(.*)$",
+		function(f)
+			parsed.fragment = f
+			return ""
+		end)
+	-- get scheme. Lower-case according to RFC 3986 section 3.1.
+	url = string.gsub(url, "^([%w][%w%+%-%.]*)%:",
+		function(s)
+			parsed.scheme = string.lower(s);
+			return ""
+		end)
+	-- get authority
+	url = string.gsub(url, "^//([^/]*)",
+		function(n)
+			parsed.authority = n
+			return ""
+		end)
+	-- get query stringing
+	url = string.gsub(url, "%?(.*)",
+		function(q)
+			parsed.query = q
+			return ""
+		end)
+	-- get params
+	url = string.gsub(url, "%;(.*)",
+		function(p)
+			parsed.params = p
+			return ""
+		end)
+	-- path is whatever was left
+	parsed.path = url
+
+	local authority = parsed.authority
+	if not authority then
+		return parsed
+	end
+	authority = string.gsub(authority,"^([^@]*)@",
+		function(u)
+			parsed.userinfo = u;
+			return ""
+		end)
+	authority = string.gsub(authority, ":([0-9]*)$",
+		function(p)
+			if p ~= "" then
+				parsed.port = p
+			end;
+			return ""
+		end)
+	if authority ~= "" then
+		parsed.host = authority
+	end
+
+	local userinfo = parsed.userinfo
+	if not userinfo then
+		return parsed
+	end
+	userinfo = string.gsub(userinfo, ":([^:]*)$",
+		function(p)
+			parsed.password = p;
+			return ""
+		end)
+	parsed.user = userinfo
+	return parsed
+end

+ 1 - 0
luci/applications/luci-ddns/luasrc/view/admin_status/index/ddns.htm

@@ -0,0 +1 @@
+<%+ddns/system_status%>

+ 56 - 0
luci/applications/luci-ddns/luasrc/view/ddns/detail_logview.htm

@@ -0,0 +1,56 @@
+
+<!-- ++ BEGIN ++ Dynamic DNS ++ detail_logview.htm ++ -->
+<script type="text/javascript">//<![CDATA[
+	function onclick_logview(section, bottom)  {
+		// get elements
+		var txt = document.getElementById("cbid.ddns." + section + "._logview.txt");	// TextArea
+		if ( !txt ) { return; }	// security check
+
+		XHR.get('<%=luci.dispatcher.build_url("admin", "services", "ddns", "logview")%>/' + section, null,
+			function(x) {
+				if (x.responseText == "_nodata_")
+					txt.value = "<%:File not found or empty%>";
+				else
+					txt.value = x.responseText;
+				if (bottom)
+					txt.scrollTop = txt.scrollHeight;
+				else
+					txt.scrollTop = 0;			}
+		);
+	}
+//]]></script>
+
+<%+cbi/valueheader%>
+
+<br />
+
+<%
+-- one button on top, one at the buttom
+%>
+<input class="cbi-button cbi-input-button" style="align: center; width: 100%" type="button" onclick="onclick_logview(this.name, false)"
+<%=
+attr("name", section) .. attr("id", cbid .. ".btn1") .. attr("value", self.inputtitle)
+%> />
+
+<br /><br />
+
+<%
+-- set a readable style taken from librecmc theme for textarea#syslog
+-- in librecmc theme there are problems with a width of 100 so we check for theme and set to lower value
+%>
+<textarea style="width: <%if media == "/luci-static/librecmc.org" then%>98.7%<%else%>100%<%end%> ; min-height: 500px; border: 3px solid #cccccc; padding: 5px; font-family: monospace; resize: none;" wrap="off" readonly="readonly"
+<%=
+attr("name", cbid .. ".txt") .. attr("id", cbid .. ".txt") .. ifattr(self.rows, "rows")
+%> >
+<%-=pcdata(self:cfgvalue(section))-%>
+</textarea>
+<br /><br />
+
+<%
+-- one button on top, one at the buttom
+%>
+<input class="cbi-button cbi-input-button" style="align: center; width: 100%" type="button" onclick="onclick_logview(this.name, true)"
+<%= attr("name", section) .. attr("id", cbid .. ".btn2") .. attr("value", self.inputtitle) %> />
+
+<%+cbi/valuefooter%>
+<!-- ++ END ++ Dynamic DNS ++ detail_logview.htm ++ -->

+ 22 - 0
luci/applications/luci-ddns/luasrc/view/ddns/detail_lvalue.htm

@@ -0,0 +1,22 @@
+
+<!-- ++ BEGIN ++ Dynamic DNS ++ detail_lvalue.htm ++ -->
+<!-- no value header to supress next line -->
+&#160;
+<% if self.widget == "select" then %>
+	<select class="cbi-input-select" onchange="cbi_d_update(this.id)"<%= attr("id", cbid) .. attr("name", cbid) .. ifattr(self.size, "size") %>>
+	<% for i, key in pairs(self.keylist) do -%>
+		<option id="cbi-<%=self.config.."-"..section.."-"..self.option.."-"..key%>"<%= attr("value", key) .. ifattr(tostring(self:cfgvalue(section) or self.default) == key, "selected", "selected") %>><%=striptags(self.vallist[i])%></option>
+	<%- end %>
+	</select>
+<% elseif self.widget == "radio" then
+	local c = 0
+	for i, key in pairs(self.keylist) do
+	c = c + 1
+%>
+	<input class="cbi-input-radio" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)" type="radio"<%= attr("id", cbid..c) .. attr("name", cbid) .. attr("value", key) .. ifattr((self:cfgvalue(section) or self.default) == key, "checked", "checked") %> />
+	<label<%= attr("for", cbid..c) %>><%=self.vallist[i]%></label>
+<% if c == self.size then c = 0 %><% if self.orientation == "horizontal" then %>&#160;<% else %><br /><% end %>
+<% end end %>
+<% end %>
+<%+cbi/valuefooter%>
+<!-- ++ END ++ Dynamic DNS ++ detail_lvalue.htm ++ -->

+ 9 - 0
luci/applications/luci-ddns/luasrc/view/ddns/detail_value.htm

@@ -0,0 +1,9 @@
+
+<!-- ++ BEGIN ++ Dynamic DNS ++ detail_value.htm ++ -->
+<%+cbi/valueheader%>
+	<input type="text" class="cbi-input-text" style="width: 10em;" onchange="cbi_d_update(this.id)"<%=
+		attr("name", cbid) .. attr("id", cbid) .. attr("value", self:cfgvalue(section) or self.default) ..
+		ifattr(self.size, "size") .. ifattr(self.placeholder, "placeholder")
+	%> />
+<!-- no value footer to supress next line -->
+<!-- ++ END ++ Dynamic DNS ++ detail_value.htm ++ -->

+ 10 - 0
luci/applications/luci-ddns/luasrc/view/ddns/overview_doubleline.htm

@@ -0,0 +1,10 @@
+
+<!-- ++ BEGIN ++ Dynamic DNS ++ overview_doubleline.htm ++ -->
+<%+cbi/valueheader%>
+
+<span id="<%=cbid%>.one"><%=self:set_one(section)%></span>
+<br />
+<span id="<%=cbid%>.two"><%=self:set_two(section)%></span>
+
+<%+cbi/valuefooter%>
+<!-- ++ END ++ Dynamic DNS ++ overview_doubleline.htm ++ -->

+ 15 - 0
luci/applications/luci-ddns/luasrc/view/ddns/overview_enabled.htm

@@ -0,0 +1,15 @@
+
+<!-- ++ BEGIN ++ Dynamic DNS ++ overview_enabled.htm ++ -->
+<%+cbi/valueheader%>
+
+<input type="hidden" value="1"<%=
+	attr("name", "cbi.cbe." .. self.config .. "." .. section .. "." .. self.option)
+%> />
+                                                                 <!-- modified to call own function -->
+<input class="cbi-input-checkbox" onclick="cbi_d_update(this.id)" onchange="onchange_enabled(this.id)" type="checkbox"<%=
+	attr("id", cbid) .. attr("name", cbid) .. attr("value", self.enabled or 1) ..
+	ifattr((self:cfgvalue(section) or self.default) == self.enabled, "checked", "checked")
+%> />
+
+<%+cbi/valuefooter%>
+<!-- ++ END ++ Dynamic DNS ++ overview_enabled.htm ++ -->

+ 17 - 0
luci/applications/luci-ddns/luasrc/view/ddns/overview_startstop.htm

@@ -0,0 +1,17 @@
+
+<!-- ++ BEGIN ++ Dynamic DNS ++ overview_startstop.htm ++ -->
+<%+cbi/valueheader%>
+
+<% if self:cfgvalue(section) ~= false then
+-- We need to garantie that function cfgvalue run first to set missing parameters
+%>
+	<!-- style="font-size: 100%;" needed for librecmc theme to fix font size -->
+	<!-- type="button" onclick="..." enable standard onclick functionalty   -->
+	<input class="cbi-button cbi-input-<%=self.inputstyle or "button" %>" style="font-size: 100%;" type="button" onclick="onclick_startstop(this.id)"
+	<%=
+	attr("name", section) .. attr("id", cbid) .. attr("value", self.inputtitle) .. ifattr(self.disabled, "disabled")
+	%> />
+<% end %>
+
+<%+cbi/valuefooter%>
+<!-- ++ END ++ Dynamic DNS ++ overview_startstop.htm ++ -->

+ 208 - 0
luci/applications/luci-ddns/luasrc/view/ddns/overview_status.htm

@@ -0,0 +1,208 @@
+
+<!-- ++ BEGIN ++ Dynamic DNS ++ overview_status.htm ++ -->
+<script type="text/javascript">//<![CDATA[
+
+	// variables to store version information
+	var luci_version
+	var luci_build
+	var ddns_version
+	var ddns_required
+
+	// helper to extract section from objects id
+	// cbi.ddns.SECTION._xyz
+	function _id2section(id) {
+		var x = id.split(".");
+		return x[2];
+	}
+
+	// helper to move status data to the relevant
+	// screen objects
+	// called by XHR.poll and onclick_startstop
+	function _data2elements(data) {
+		// DDNS Service
+		// fill Version informations
+		luci_version  = data[0].luci_ver
+		luci_build    = data[0].luci_build
+		ddns_version  = data[0].script_ver
+		ddns_required = data[0].script_min
+
+		// Service sections
+		for( i = 1; i < data.length; i++ )
+		{
+			var section = data[i].section	// Section to handle
+			var cbx = document.getElementById("cbid.ddns." + section + ".enabled");		// Enabled
+			var btn = document.getElementById("cbid.ddns." + section + "._startstop");	// Start/Stop button
+			var rip = document.getElementById("cbid.ddns." + section + "._domainIP.two");	// Registered IP
+			var lup = document.getElementById("cbid.ddns." + section + "._update.one");	// Last Update
+			var nup = document.getElementById("cbid.ddns." + section + "._update.two");	// Next Update
+			if ( !(cbx && btn && rip && lup && nup) ) { return; }	// security check
+
+			// process id
+			if (data[i].pid > 0) {
+				// stop always possible if process running
+				btn.value = "PID: " + data[i].pid;
+				btn.className = "cbi-button cbi-input-reset";
+			} else {
+				// default Start / enabled
+				btn.value = "<%:Start%>";
+				btn.className = "cbi-button cbi-input-apply";
+			}
+			btn.disabled = false;	// button enabled
+
+			// last update
+			switch (data[i].datelast) {
+				case "_empty_":
+					lup.innerHTML = '<em><%:Unknown error%></em>' ;
+					break;
+				case "_never_":
+					lup.innerHTML = '<em><%:Never%></em>' ;
+					break;
+				default:
+					lup.innerHTML = data[i].datelast;
+					break;
+			}
+
+			// next update
+			switch (data[i].datenext) {
+				case "_empty_":
+					nup.innerHTML = '<em><%:Unknown error%></em>' ;
+					break;
+				case "_verify_":
+					nup.innerHTML = '<em><%:Verify%></em>';
+					break;
+				case "_runonce_":
+				case "_stopped_":
+				case "_disabled_":
+					if (cbx.checked && data[i].datenext == "_runonce_") {
+						nup.innerHTML = '<em><%:Run once%></em>';
+					} else if (cbx.checked) {
+						nup.innerHTML = '<em><%:Stopped%></em>';
+					} else {
+						nup.innerHTML = '<em><%:Disabled%></em>';
+						btn.value = '----------';
+						btn.className = "cbi-button cbi-input-button";	// no image
+						btn.disabled = true;	// disabled
+					}
+					break;
+				default:
+					nup.innerHTML = data[i].datenext;
+					break;
+			}
+
+			// domain
+			// (data[i].domain ignored here
+
+			// registered IP
+			// rip.innerHTML = "Registered IP";
+			if (data[i].domain == "_nodomain_")
+				rip.innerHTML = '';
+			else if (data[i].reg_ip == "_nodata_")
+				rip.innerHTML = '<em><%:No data%></em>';
+			else
+				rip.innerHTML = data[i].reg_ip;
+
+			// monitored interfacce
+			// data[i].iface ignored here
+		}
+	}
+
+	// event handler for enabled checkbox
+	function onchange_enabled(id) {
+		// run original function in cbi.js
+		// whatever is done there
+		cbi_d_update(id);
+
+		var section = _id2section(id);
+		var cbx = document.getElementById("cbid.ddns." + section + ".enabled");
+		var btn = document.getElementById("cbid.ddns." + section + "._startstop");
+		if ( !(cbx && btn) ) { return; }	// security check
+
+		var pid_txt = btn.value;
+		var pid_found = ( pid_txt.search("PID") >= 0 ) ? true : false;
+
+		if (pid_found) {
+			// btn.value = "PID: 0000";
+			btn.className = "cbi-button cbi-button-reset";
+			btn.disabled = false;
+		} else if (cbx.checked) {
+			btn.value = "<%:Start%>";
+			btn.className = "cbi-button cbi-button-apply";
+			btn.disabled = false;
+		} else {
+			btn.value = '----------';
+			btn.className = "cbi-button cbi-input-button";	// no image
+			btn.disabled = true;		// disabled
+		}
+	}
+
+	// event handler for map.title link
+	function onclick_maptitle() {
+		var str = "<%:Version Information%>";
+		str += "\n\nluci-app-ddns:";
+		str += "\n\t<%:Version%>:\t" + luci_version;
+		str += "\n\t<%:Build%>:\t" + luci_build;
+		str += "\n\nddns-scripts <%:required%>:";
+		str += "\n\t<%:Version%>:\t" + ddns_required + " <%:or greater%>";
+		str += "\n\nddns-scripts <%:installed%>:";
+		str += "\n\t<%:Version%>:\t" + ddns_version;
+		str += "\n\n"
+		alert(str);
+	}
+
+	// event handler for start/stop button
+	function onclick_startstop(id) {
+		// extract section
+		var section = _id2section(id);
+		// get elements
+		var cbx = document.getElementById("cbid.ddns." + section + ".enabled");		// Enabled
+		var obj = document.getElementById("cbi-ddns-overview-status-legend");		// objext defined below to make in-/visible
+		if ( !(obj && cbx) ) { return; }	// security check
+
+		// make me visible
+		obj.parentNode.style.display = "block";
+
+		// do start/stop
+		var btnXHR = new XHR();
+		btnXHR.get('<%=luci.dispatcher.build_url("admin", "services", "ddns", "startstop")%>/' + section + '/' + cbx.checked, null,
+			function(x, data) {
+				if (x.responseText == "_uncommited_") {
+					// we need a trick to display Ampersand "&" in stead of "&#38;" or "&amp;"
+					// after translation
+					txt="<%:Please [Save & Apply] your changes first%>";
+					alert( txt.replace(new RegExp("<%:&%>", "g"), "&") );
+				} else {
+					// should have data because status changed
+					// so update screen
+					if (data)
+						_data2elements(data);
+				}
+				// make me invisible
+				obj.parentNode.style.display = "none";
+			}
+		);
+	}
+
+	// force to immediate show status on page load (not waiting for XHR.poll)
+	XHR.get('<%=luci.dispatcher.build_url("admin", "services", "ddns", "status")%>', null,
+		function(x, data) {
+			_data2elements(data);
+		}
+	);
+
+	// define only ONE XHR.poll in a page because if one is running it blocks the other one
+	// optimum is to define on Map or Section Level from here you can reach all elements
+	// we need update every 15 seconds only
+	XHR.poll(15, '<%=luci.dispatcher.build_url("admin", "services", "ddns", "status")%>', null,
+		function(x, data) {
+			_data2elements(data);
+		}
+	);
+
+//]]></script>
+
+<fieldset class="cbi-section" style="display:none">
+	<legend id="cbi-ddns-overview-status-legend"><%:Applying changes%></legend>
+	<img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" />
+	<span id="cbi-ddns-overview-status-text"><%:Waiting for changes to be applied...%></span>
+</fieldset>
+<!-- ++ END ++ Dynamic DNS ++ overview_status.htm ++ -->

+ 158 - 0
luci/applications/luci-ddns/luasrc/view/ddns/system_status.htm

@@ -0,0 +1,158 @@
+
+<!-- ++ BEGIN ++ Dynamic DNS ++ system_status.htm ++ -->
+<script type="text/javascript">//<![CDATA[
+	// helper to move status data to the relevant
+	// screen objects
+	// called by XHR.poll and XHR.get
+	function _data2elements(x, data) {
+		var tbl = document.getElementById('ddns_status_table');
+		// security check
+		if ( !(tbl) ) { return; }
+
+		// clear all rows
+		while (tbl.rows.length > 1)
+			tbl.deleteRow(1);
+
+		// variable for Modulo-Division use to set cbi-rowstyle-? (0 or 1)
+		var x = -1;
+		var i = 1;
+
+		// no data => no ddns-scripts Version 2 installed
+		if ( !data ) {
+			var txt = '<br /><strong><font color="red"><%:Old version of ddns-scripts installed%></font>' ;
+			var url = '<a href="' ;
+			url += '<%=luci.dispatcher.build_url("admin", "system", "packages")%>' ;
+			url += '"><%:install update here%></a></strong>' ;
+			var tr = tbl.insertRow(-1);
+			tr.className = 'cbi-section-table-row cbi-rowstyle-' + (((i + x) % 2) + 1);
+			var td = tr.insertCell(-1);
+			td.colSpan = 2 ;
+			td.innerHTML = txt + " - " + url
+			tr.insertCell(-1).colSpan = 3 ;
+			return;
+		}
+
+		// DDNS Service disabled
+		if (data[0].enabled == 0) {
+			var txt = '<strong><font color="red"><%:DDNS Autostart disabled%></font>' ;
+			var url = '<a href="' + data[0].url_up + '"><%:enable here%></a></strong>' ;
+			var tr = tbl.insertRow(-1);
+			tr.className = 'cbi-section-table-row cbi-rowstyle-' + (((i + x) % 2) + 1);
+			var td = tr.insertCell(-1);
+			td.colSpan = 2 ;
+			td.innerHTML = txt + " - " + url
+			tr.insertCell(-1).colSpan = 3 ;
+			x++ ;
+		}
+
+		for( i = 1; i < data.length; i++ )
+		{
+			var tr = tbl.insertRow(-1);
+			tr.className = 'cbi-section-table-row cbi-rowstyle-' + (((i + x) % 2) + 1) ;
+
+			// configuration
+			tr.insertCell(-1).innerHTML = '<strong>' + data[i].section + '</strong>' ;
+
+			// pid
+			// data[i].pid ignored here
+
+			// last update
+			// data[i].datelast ignored here
+
+			// next update
+			switch (data[i].datenext) {
+				case "_empty_":
+					tr.insertCell(-1).innerHTML = '<em><%:Unknown error%></em>' ;
+					break;
+				case "_stopped_":
+					tr.insertCell(-1).innerHTML = '<em><%:Stopped%></em>' ;
+					break;
+				case "_disabled_":
+					tr.insertCell(-1).innerHTML = '<em><%:Disabled%></em>' ;
+					break;
+				case "_noupdate_":
+					tr.insertCell(-1).innerHTML = '<em><%:Update error%></em>' ;
+					break;
+				case "_runonce_":
+					tr.insertCell(-1).innerHTML = '<em><%:Run once%></em>' ;
+					break;
+				case "_verify_":
+					tr.insertCell(-1).innerHTML = '<em><%:Verify%></em>';
+					break;
+				default:
+					tr.insertCell(-1).innerHTML = data[i].datenext ;
+					break;
+			}
+
+			// domain
+			if (data[i].domain == "_nodomain_")
+				tr.insertCell(-1).innerHTML = '<em><%:config error%></em>';
+			else
+				tr.insertCell(-1).innerHTML = data[i].domain;
+
+			// registered IP
+			switch (data[i].reg_ip) {
+				case "_nodomain_":
+					tr.insertCell(-1).innerHTML = '<em><%:Config error%></em>';
+					break;
+				case "_nodata_":
+					tr.insertCell(-1).innerHTML = '<em><%:No data%></em>';
+					break;
+				case "_noipv6_":
+					tr.insertCell(-1).innerHTML = '<em><%:IPv6 not supported%></em>';
+					break;
+				default:
+					tr.insertCell(-1).innerHTML = data[i].reg_ip;
+					break;
+			}
+
+			// monitored interfacce
+			if (data[i].iface == "_nonet_")
+				tr.insertCell(-1).innerHTML = '<em><%:Config error%></em>';
+			else
+				tr.insertCell(-1).innerHTML = data[i].iface;
+		}
+
+		if (tbl.rows.length == 1 || (data[0].enabled == 0 && tbl.rows.length == 2) ) {
+			var br = '<br />';
+			if (tbl.rows.length > 1)
+				br = '';
+			var tr = tbl.insertRow(-1);
+			tr.className = "cbi-section-table-row";
+			var td = tr.insertCell(-1);
+			td.colSpan = 5;
+			td.innerHTML = '<em>' + br + '<%:There is no service configured.%></em>' ;
+		}
+	}
+
+	// force to immediate show status (not waiting for XHR.poll)
+	XHR.get('<%=luci.dispatcher.build_url("admin", "services", "ddns", "status")%>', null,
+		function(x, data) {
+			_data2elements(x, data);
+		}
+	);
+
+	XHR.poll(10, '<%=luci.dispatcher.build_url("admin", "services", "ddns", "status")%>', null,
+		function(x, data) {
+			_data2elements(x, data);
+		}
+	);
+//]]></script>
+
+<fieldset class="cbi-section" id="ddns_status_section">
+	<legend><a href="<%=luci.dispatcher.build_url([[admin]], [[services]], [[ddns]])%>"><%:Dynamic DNS%></a></legend>
+
+	<table class="cbi-section-table" id="ddns_status_table">
+		<tr class="cbi-section-table-titles">
+			<th class="cbi-section-table-cell"><%:Configuration%></th>
+			<th class="cbi-section-table-cell"><%:Next Update%></th>
+			<th class="cbi-section-table-cell"><%:Hostname/Domain%></th>
+			<th class="cbi-section-table-cell"><%:Registered IP%></th>
+			<th class="cbi-section-table-cell"><%:Network%></th>
+		</tr>
+		<tr class="cbi-section-table-row">
+			<td colspan="5"><em><br /><%:Collecting data...%></em></td>
+		</tr>
+	</table>
+</fieldset>
+<!-- ++ END ++ Dynamic DNS ++ system_status.htm ++ -->

+ 19 - 0
luci/applications/luci-ddns/root/etc/uci-defaults/luci-ddns

@@ -0,0 +1,19 @@
+#!/bin/sh
+
+# no longer needed for "Save and Apply" to restart ddns
+uci -q batch <<-EOF >/dev/null
+	delete ucitrack.@ddns[-1]
+	commit ucitrack
+EOF
+
+# make helper script executable
+chmod 755 /usr/lib/ddns/dynamic_dns_lucihelper.sh
+
+# update application section for luci-app-ddns
+uci -q get ddns.global > /dev/null || uci -q set ddns.global='ddns'
+uci -q get ddns.global.date_format > /dev/null || uci -q set ddns.global.date_format='%F %R'
+uci -q get ddns.global.log_lines > /dev/null || uci -q set ddns.global.log_lines='250'
+uci -q commit ddns
+
+rm -f /tmp/luci-indexcache
+exit 0

+ 4 - 0
luci/applications/luci-diag-core/Makefile

@@ -0,0 +1,4 @@
+PO = diag_core
+
+include ../../build/config.mk
+include ../../build/module.mk

+ 26 - 0
luci/applications/luci-diag-core/luasrc/controller/luci_diag.lua

@@ -0,0 +1,26 @@
+--[[
+
+Luci Voice Core
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+]]--
+
+module("luci.controller.luci_diag", package.seeall)
+
+function index()
+	local e
+
+	e = entry({"admin", "network", "diag_config"}, template("diag/network_config_index") , _("Configure Diagnostics"), 120)
+	e.index = true
+	e.dependent = true
+
+	e = entry({"mini", "diag"}, template("diag/index"), _("Diagnostics"), 120)
+	e.index = true
+	e.dependent = true
+end

+ 18 - 0
luci/applications/luci-diag-core/luasrc/view/diag/index.htm

@@ -0,0 +1,18 @@
+<%#
+LuCI - Lua Configuration Interface
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+-%>
+<%+header%>
+<h2><a id="content" name="content"><%:Diagnostics%></a></h2>
+<p><%:The entries in the menu allow you to perform diagnostic tests on your system to aid in troubleshooting.%></p>
+<p><%:The diagnostics available under this menu depend on what modules you have installed on your device.%></p>
+<%+footer%>

+ 18 - 0
luci/applications/luci-diag-core/luasrc/view/diag/network_config_index.htm

@@ -0,0 +1,18 @@
+<%#
+LuCI - Lua Configuration Interface
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+-%>
+<%+header%>
+<h2><a id="content" name="content"><%:Diagnostics%></a></h2>
+<p><%:With this menu you can configure network diagnostics, such as network device scans and ping tests.%></p>
+<p><%:The diagnostics available under this menu depend on what modules you have installed on your device.%></p>
+<%+footer%>

+ 4 - 0
luci/applications/luci-diag-devinfo/Makefile

@@ -0,0 +1,4 @@
+PO=diag_devinfo
+
+include ../../build/config.mk
+include ../../build/module.mk

+ 195 - 0
luci/applications/luci-diag-devinfo/luasrc/controller/luci_diag/devinfo_common.lua

@@ -0,0 +1,195 @@
+--[[
+
+Luci diag - Diagnostics controller module
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+]]--
+
+module("luci.controller.luci_diag.devinfo_common", package.seeall)
+
+require("luci.i18n")
+require("luci.util")
+require("luci.sys")
+require("luci.cbi")
+require("luci.model.uci")
+
+local translate = luci.i18n.translate
+local DummyValue = luci.cbi.DummyValue
+local SimpleSection = luci.cbi.SimpleSection
+
+function index()
+	return -- no-op
+end
+
+function run_processes(outnets, cmdfunc)
+   i = next(outnets, nil)
+   while (i) do
+      outnets[i]["output"] = luci.sys.exec(cmdfunc(outnets, i))
+      i = next(outnets, i)
+   end
+end
+
+function parse_output(devmap, outnets, haslink, type, mini, debug)
+   local curnet = next(outnets, nil)
+
+   while (curnet) do
+      local output = outnets[curnet]["output"]
+      local subnet = outnets[curnet]["subnet"]
+      local ports = outnets[curnet]["ports"]
+      local interface = outnets[curnet]["interface"]
+      local netdevs = {}
+      devlines = luci.util.split(output)
+      if not devlines then
+	 devlines = {}
+	 table.insert(devlines, output)
+      end
+	    
+      local j = nil
+      j = next(devlines, j)
+      
+      local found_a_device = false
+      
+      while (j) do
+	 if devlines[j] and ( devlines[j] ~= "" ) then
+	    found_a_device = true
+	    local devtable
+	    local row = {}
+	    devtable = luci.util.split(devlines[j], ' | ')
+	    row["ip"] = devtable[1]
+	    if (not mini) then
+	       row["mac"] = devtable[2]
+	    end
+	    if ( devtable[4] == 'unknown' ) then 
+	       row["vendor"] = devtable[3]
+	    else
+	       row["vendor"] = devtable[4]
+	    end
+	    row["type"] = devtable[5]
+	    if (not mini) then
+	       row["model"] = devtable[6]
+	    end
+	    if (haslink) then
+	       row["config_page"] = devtable[7]
+	    end
+	    
+	    if (debug) then
+	       row["raw"] = devlines[j]
+	    end
+	    table.insert(netdevs, row)
+	 end
+	 j = next(devlines, j)
+      end
+      if not found_a_device then
+	 local row = {}
+	 row["ip"] = curnet
+	 if (not mini) then
+	    row["mac"] = ""
+	 end
+	 if (type == "smap") then
+	    row["vendor"] = luci.i18n.translate("No SIP devices")
+	 else
+	    row["vendor"] = luci.i18n.translate("No devices detected")
+	 end
+	 row["type"] = luci.i18n.translate("check other networks")
+	 if (not mini) then
+	    row["model"] = ""
+	 end
+	 if (haslink) then
+	    row["config_page"] = ""
+	 end
+	 if (debug) then
+	    row["raw"] = output
+	 end
+	 table.insert(netdevs, row)
+      end	 
+      local s
+      if (type == "smap") then
+	 if (mini) then
+	    s = devmap:section(luci.cbi.Table, netdevs, luci.i18n.translate("SIP devices discovered for") .. " " .. curnet)
+	 else
+	    local interfacestring = ""
+	    if ( interface ~= "" ) then
+	       interfacestring = ", " .. interface
+	    end
+	    s = devmap:section(luci.cbi.Table, netdevs, luci.i18n.translate("SIP devices discovered for") .. " " .. curnet .. " (" .. subnet .. ":" .. ports .. interfacestring .. ")")
+	 end
+	 s.template = "diag/smapsection"
+      else
+	 if (mini) then
+	    s = devmap:section(luci.cbi.Table, netdevs, luci.i18n.translate("Devices discovered for") .. " " .. curnet)
+	 else
+	    local interfacestring = ""
+	    if ( interface ~= "" ) then
+	       interfacestring = ", " .. interface
+	    end
+	    s = devmap:section(luci.cbi.Table, netdevs, luci.i18n.translate("Devices discovered for") .. " " .. curnet .. " (" .. subnet .. interfacestring .. ")")
+	 end
+      end
+      s:option(DummyValue, "ip", translate("IP Address"))
+      if (not mini) then
+	 s:option(DummyValue, "mac", translate("MAC Address"))
+      end
+      s:option(DummyValue, "vendor", translate("Vendor"))
+      s:option(DummyValue, "type", translate("Device Type"))
+      if (not mini) then
+	 s:option(DummyValue, "model", translate("Model"))
+      end
+      if (haslink) then
+	 s:option(DummyValue, "config_page", translate("Link to Device"))
+      end
+      if (debug) then
+	 s:option(DummyValue, "raw", translate("Raw"))
+      end
+      curnet = next(outnets, curnet)
+   end
+end
+
+function get_network_device(interface)
+   local state = luci.model.uci.cursor_state()
+   state:load("network")
+   local dev
+   
+   return state:get("network", interface, "ifname")
+end
+
+
+function cbi_add_networks(field)
+	uci.cursor():foreach("network", "interface",
+		function (section)
+			if section[".name"] ~= "loopback" then
+				field:value(section[".name"])
+			end
+		end
+	)
+	field.titleref = luci.dispatcher.build_url("admin", "network", "network")
+end
+
+function config_devinfo_scan(map, scannet)
+   local o
+   o = scannet:option(luci.cbi.Flag, "enable", translate("Enable"))
+   o.optional = false
+   o.rmempty = false
+
+   o = scannet:option(luci.cbi.Value, "interface", translate("Interface"))
+   o.optional = false
+   luci.controller.luci_diag.devinfo_common.cbi_add_networks(o)
+   
+   local scansubnet
+   scansubnet = scannet:option(luci.cbi.Value, "subnet", translate("Subnet"))
+   scansubnet.optional = false
+   
+   o = scannet:option(luci.cbi.Value, "timeout", translate("Timeout"), translate("Time to wait for responses in seconds (default 10)"))
+   o.optional = true
+   
+   o = scannet:option(luci.cbi.Value, "repeat_count", translate("Repeat Count"), translate("Number of times to send requests (default 1)"))
+   o.optional = true
+   
+   o = scannet:option(luci.cbi.Value, "sleepreq", translate("Sleep Between Requests"), translate("Milliseconds to sleep between requests (default 100)"))
+   o.optional = true
+end

+ 56 - 0
luci/applications/luci-diag-devinfo/luasrc/controller/luci_diag/luci_diag_devinfo.lua

@@ -0,0 +1,56 @@
+--[[
+
+Luci diag - Diagnostics controller module
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+]]--
+
+module("luci.controller.luci_diag.luci_diag_devinfo", package.seeall)
+
+function index()
+   local e
+
+   e = entry({"admin", "voice", "diag", "phones"}, arcombine(cbi("luci_diag/smap_devinfo"), cbi("luci_diag/smap_devinfo_config")), _("Phones"), 10)
+   e.leaf = true
+   e.subindex = true
+   e.dependent = true
+
+   e = entry({"admin", "voice", "diag", "phones", "config"}, cbi("luci_diag/smap_devinfo_config"), _("Configure"), 10)
+
+   e = entry({"admin", "status", "smap_devinfo"}, cbi("luci_diag/smap_devinfo"), _("SIP Devices on Network"), 120)
+   e.leaf = true
+   e.dependent = true
+
+   e = entry({"admin", "network", "diag_config", "netdiscover_devinfo_config"}, cbi("luci_diag/netdiscover_devinfo_config"), _("Network Device Scan"), 100)
+   e.leaf = true
+   e.dependent = true
+
+   e = entry({"admin", "network", "diag_config", "smap_devinfo_config"}, cbi("luci_diag/smap_devinfo_config"), _("SIP Device Scan"))
+   e.leaf = true
+   e.dependent = true
+
+   e = entry({"admin", "status", "netdiscover_devinfo"}, cbi("luci_diag/netdiscover_devinfo"), _("Devices on Network"), 90)
+   e.dependent = true
+
+   e = entry({"admin", "network", "mactodevinfo"}, cbi("luci_diag/mactodevinfo"), _("MAC Device Info Overrides"), 190)
+   e.dependent = true
+
+   e = entry({"mini", "diag", "phone_scan"}, cbi("luci_diag/smap_devinfo_mini"), _("Phone Scan"), 100)
+   e.dependent = true
+
+   e = entry({"mini", "voice", "phones", "phone_scan_config"}, cbi("luci_diag/smap_devinfo_config_mini"), _("Config Phone Scan"), 90)
+   e.dependent = true
+
+   e = entry({"mini", "diag", "netdiscover_devinfo"}, cbi("luci_diag/netdiscover_devinfo_mini"), _("Network Device Scan"), 10)
+   e.dependent = true
+
+   e = entry({"mini", "network", "netdiscover_devinfo_config"}, cbi("luci_diag/netdiscover_devinfo_config_mini"), _("Device Scan Config"))
+   e.dependent = true
+
+end

+ 101 - 0
luci/applications/luci-diag-devinfo/luasrc/controller/luci_diag/netdiscover_common.lua

@@ -0,0 +1,101 @@
+--[[
+
+Luci diag - Diagnostics controller module
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+]]--
+
+module("luci.controller.luci_diag.netdiscover_common", package.seeall)
+
+require("luci.i18n")
+require("luci.util")
+require("luci.sys")
+require("luci.cbi")
+require("luci.model.uci")
+
+local translate = luci.i18n.translate
+local DummyValue = luci.cbi.DummyValue
+local SimpleSection = luci.cbi.SimpleSection
+
+function index()
+	return -- no-op
+end
+	 
+function get_params()
+
+   local netdiscover_uci = luci.model.uci.cursor()
+   netdiscover_uci:load("luci_devinfo")
+   local nettable = netdiscover_uci:get_all("luci_devinfo")
+
+   local i 
+   local subnet
+   local netdout
+
+   local outnets = {}
+
+   i = next(nettable, nil)
+
+   while (i) do
+      if (netdiscover_uci:get("luci_devinfo", i) == "netdiscover_scannet") then 
+	 local scannet = netdiscover_uci:get_all("luci_devinfo", i)
+	 if scannet["subnet"] and (scannet["subnet"] ~= "") and scannet["enable"] and ( scannet["enable"] == "1") then
+	    local output = ""
+	    local outrow = {}
+	    outrow["interface"] = scannet["interface"]
+	    outrow["timeout"] = 10
+	    local timeout = tonumber(scannet["timeout"]) 
+	    if timeout and ( timeout > 0 ) then
+	       outrow["timeout"] = scannet["timeout"]
+	    end
+
+	    outrow["repeat_count"] = 1
+	    local repcount = tonumber(scannet["repeat_count"]) 
+	    if repcount and ( repcount > 0 ) then
+	       outrow["repeat_count"] = scannet["repeat_count"]
+	    end
+
+	    outrow["sleepreq"] = 100
+	    local repcount = tonumber(scannet["sleepreq"]) 
+	    if repcount and ( repcount > 0 ) then
+	       outrow["sleepreq"] = scannet["sleepreq"]
+	    end
+
+	    outrow["subnet"] = scannet["subnet"]    
+	    outrow["output"] = output
+	    outnets[i] = outrow
+	 end
+      end
+      i = next(nettable, i)
+   end
+   return outnets
+end
+
+function command_function(outnets, i)
+   local interface = luci.controller.luci_diag.devinfo_common.get_network_device(outnets[i]["interface"])
+   
+   return "/usr/bin/netdiscover-to-devinfo " .. outnets[i]["subnet"] .. " " .. interface .. " " .. outnets[i]["timeout"] .. " -r " .. outnets[i]["repeat_count"] .. " -s " .. outnets[i]["sleepreq"] .. " </dev/null"
+end
+
+function action_links(netdiscovermap, mini) 
+   s = netdiscovermap:section(SimpleSection, "", translate("Actions")) 
+   b = s:option(DummyValue, "_config", translate("Configure Scans"))
+   b.value = ""
+   if (mini) then
+      b.titleref = luci.dispatcher.build_url("mini", "network", "netdiscover_devinfo_config")
+   else
+      b.titleref = luci.dispatcher.build_url("admin", "network", "diag_config", "netdiscover_devinfo_config")
+   end
+   b = s:option(DummyValue, "_scans", translate("Repeat Scans (this can take a few minutes)"))
+   b.value = ""
+   if (mini) then
+      b.titleref = luci.dispatcher.build_url("mini", "diag", "netdiscover_devinfo")
+   else
+      b.titleref = luci.dispatcher.build_url("admin", "status", "netdiscover_devinfo")
+   end
+end

+ 112 - 0
luci/applications/luci-diag-devinfo/luasrc/controller/luci_diag/smap_common.lua

@@ -0,0 +1,112 @@
+--[[
+
+Luci diag - Diagnostics controller module
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+]]--
+
+module("luci.controller.luci_diag.smap_common", package.seeall)
+
+require("luci.i18n")
+require("luci.util")
+require("luci.sys")
+require("luci.cbi")
+require("luci.model.uci")
+
+local translate = luci.i18n.translate
+local DummyValue = luci.cbi.DummyValue
+local SimpleSection = luci.cbi.SimpleSection
+
+function index()
+	return -- no-op
+end
+
+function get_params()
+
+   local smapnets_uci = luci.model.uci.cursor()
+   smapnets_uci:load("luci_devinfo")
+   local nettable = smapnets_uci:get_all("luci_devinfo")
+
+   local i 
+   local subnet
+   local smapout
+
+   local outnets = {}
+
+   i = next(nettable, nil)
+
+   while (i) do
+      if (smapnets_uci:get("luci_devinfo", i) == "smap_scannet") then 
+	 local scannet = smapnets_uci:get_all("luci_devinfo", i)
+	 if scannet["subnet"] and (scannet["subnet"] ~= "") and scannet["enable"] and ( scannet["enable"] == "1") then
+	    local output = ""
+	    local outrow = {}
+	    outrow["subnet"] = scannet["subnet"]
+	    ports = "5060"
+	    if scannet["ports"] and ( scannet["ports"] ~= "" ) then
+	       ports = scannet["ports"]		 
+	    end
+	    outrow["timeout"] = 10
+	    local timeout = tonumber(scannet["timeout"]) 
+	    if timeout and ( timeout > 0 ) then
+	       outrow["timeout"] = scannet["timeout"]
+	    end
+
+	    outrow["repeat_count"] = 1
+	    local repcount = tonumber(scannet["repeat_count"]) 
+	    if repcount and ( repcount > 0 ) then
+	       outrow["repeat_count"] = scannet["repeat_count"]
+	    end
+
+	    outrow["sleepreq"] = 100
+	    local repcount = tonumber(scannet["sleepreq"]) 
+	    if repcount and ( repcount > 0 ) then
+	       outrow["sleepreq"] = scannet["sleepreq"]
+	    end
+
+	    if scannet["interface"] and ( scannet["interface"] ~= "" ) then
+	       outrow["interface"] = scannet["interface"]
+	    else
+	       outrow["interface"] = ""
+	    end
+
+	    outrow["ports"] = ports
+	    outrow["output"] = output
+	    outnets[i] = outrow
+	 end
+      end
+      i = next(nettable, i)
+   end
+   return outnets
+end
+
+function command_function(outnets, i) 
+
+   local interface = luci.controller.luci_diag.devinfo_common.get_network_device(outnets[i]["interface"])
+
+   return "/usr/bin/netsmap-to-devinfo -r " .. outnets[i]["subnet"] .. " -t " .. outnets[i]["timeout"] .. " -i " .. interface .. " -x -p " ..  outnets[i]["ports"]  .. " -c " .. outnets[i]["repeat_count"] .. " -s " .. outnets[i]["sleepreq"] .. " </dev/null"
+end
+
+function action_links(smapmap, mini) 
+   s = smapmap:section(SimpleSection, "", translate("Actions")) 
+   b = s:option(DummyValue, "_config", translate("Configure Scans"))
+   b.value = ""
+   if (mini) then
+      b.titleref = luci.dispatcher.build_url("mini", "voice", "phones", "phone_scan_config")
+   else
+      b.titleref = luci.dispatcher.build_url("admin", "network", "diag_config", "smap_devinfo_config")
+   end
+   b = s:option(DummyValue, "_scans", translate("Repeat Scans (this can take a few minutes)"))
+   b.value = ""
+   if (mini) then
+      b.titleref = luci.dispatcher.build_url("mini", "diag", "phone_scan")
+   else
+      b.titleref = luci.dispatcher.build_url("admin", "status", "smap_devinfo")
+   end
+end

+ 36 - 0
luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/mactodevinfo.lua

@@ -0,0 +1,36 @@
+--[[
+LuCI - Lua Configuration Interface
+
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+m = Map("mactodevinfo", luci.i18n.translate("MAC Device Info Overrides"), translate("Override the information returned by the MAC to Device Info Script (mac-to-devinfo) for a specified range of MAC Addresses"))
+
+s = m:section(TypedSection, "mactodevinfo", translate("MAC Device Override"), translate("MAC range and information used to override system and IEEE databases"))
+s.addremove = true
+s.anonymous = true
+
+v = s:option(Value, "name", translate("Name"))
+v.optional = true
+v = s:option(Value, "maclow", translate("Beginning of MAC address range"))
+v.optional = false
+v = s:option(Value, "machigh", translate("End of MAC address range"))
+v.optional = false
+v = s:option(Value, "vendor", translate("Vendor"))
+v.optional = false
+v = s:option(Value, "devtype", translate("Device Type"))
+v.optional = false
+v = s:option(Value, "model", translate("Model"))
+v.optional = false
+v = s:option(Value, "ouiowneroverride", translate("OUI Owner"))
+v.optional = true
+
+return m

+ 33 - 0
luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/netdiscover_devinfo.lua

@@ -0,0 +1,33 @@
+--[[
+netdiscover_devinfo - SIP Device Information
+
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+require("luci.i18n")
+require("luci.util")
+require("luci.sys")
+require("luci.model.uci")
+require("luci.controller.luci_diag.netdiscover_common")
+require("luci.controller.luci_diag.devinfo_common")
+
+local debug = false
+
+m = SimpleForm("luci_devinfo", translate("Network Device Scan"), translate("Scans for devices on specified networks."))
+m.reset = false
+m.submit = false
+
+local outnets = luci.controller.luci_diag.netdiscover_common.get_params()
+luci.controller.luci_diag.devinfo_common.run_processes(outnets, luci.controller.luci_diag.netdiscover_common.command_function)
+luci.controller.luci_diag.devinfo_common.parse_output(m, outnets, false, "netdiscover", false, debug)
+luci.controller.luci_diag.netdiscover_common.action_links(m, false)
+
+return m

+ 30 - 0
luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/netdiscover_devinfo_config.lua

@@ -0,0 +1,30 @@
+--[[
+LuCI - Lua Configuration Interface
+
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+require("luci.controller.luci_diag.devinfo_common")
+
+m = Map("luci_devinfo", translate("Network Device Scanning Configuration"), translate("Configure scanning for devices on specified networks. Decreasing \'Timeout\', \'Repeat Count\', and/or \'Sleep Between Requests\' may speed up scans, but also may fail to find some devices."))
+
+s = m:section(SimpleSection, "", translate("Use Configuration"))
+b = s:option(DummyValue, "_scans", translate("Perform Scans (this can take a few minutes)"))
+b.value = ""
+b.titleref = luci.dispatcher.build_url("admin", "status", "netdiscover_devinfo")
+
+scannet = m:section(TypedSection, "netdiscover_scannet", translate("Scanning Configuration"), translate("Networks to scan for devices"))
+scannet.addremove = true
+scannet.anonymous = false
+
+luci.controller.luci_diag.devinfo_common.config_devinfo_scan(m, scannet)
+
+return m

+ 30 - 0
luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/netdiscover_devinfo_config_mini.lua

@@ -0,0 +1,30 @@
+--[[
+LuCI - Lua Configuration Interface
+
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+require("luci.controller.luci_diag.devinfo_common")
+
+m = Map("luci_devinfo", translate("Network Device Scanning Configuration"), translate("Configure scanning for devices on specified networks. Decreasing \'Timeout\', \'Repeat Count\', and/or \'Sleep Between Requests\' may speed up scans, but also may fail to find some devices."))
+
+s = m:section(SimpleSection, "", translate("Use Configuration"))
+b = s:option(DummyValue, "_scans", translate("Perform Scans (this can take a few minutes)"))
+b.value = ""
+b.titleref = luci.dispatcher.build_url("mini", "diag", "netdiscover_devinfo")
+
+scannet = m:section(TypedSection, "netdiscover_scannet", translate("Scanning Configuration"), translate("Networks to scan for devices"))
+scannet.addremove = true
+scannet.anonymous = false
+
+luci.controller.luci_diag.devinfo_common.config_devinfo_scan(m, scannet)
+
+return m

+ 33 - 0
luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/netdiscover_devinfo_mini.lua

@@ -0,0 +1,33 @@
+--[[
+netdiscover_devinfo - SIP Device Information
+
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+require("luci.i18n")
+require("luci.util")
+require("luci.sys")
+require("luci.model.uci")
+require("luci.controller.luci_diag.netdiscover_common")
+require("luci.controller.luci_diag.devinfo_common")
+
+local debug = false
+
+m = SimpleForm("luci_devinfo", translate("Network Device Scan"), translate("Scan for devices on specified networks."))
+m.reset = false
+m.submit = false
+
+local outnets = luci.controller.luci_diag.netdiscover_common.get_params()
+luci.controller.luci_diag.devinfo_common.run_processes(outnets, luci.controller.luci_diag.netdiscover_common.command_function)
+luci.controller.luci_diag.devinfo_common.parse_output(m, outnets, false, "netdiscover", true, debug)
+luci.controller.luci_diag.netdiscover_common.action_links(m, true)
+
+return m

+ 33 - 0
luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/smap_devinfo.lua

@@ -0,0 +1,33 @@
+--[[
+smap_devinfo - SIP Device Information
+
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+require("luci.i18n")
+require("luci.util")
+require("luci.sys")
+require("luci.model.uci")
+require("luci.controller.luci_diag.smap_common")
+require("luci.controller.luci_diag.devinfo_common")
+
+local debug = false
+
+m = SimpleForm("luci-smap-to-devinfo", translate("SIP Device Information"), translate("Scan for supported SIP devices on specified networks."))
+m.reset = false
+m.submit = false
+
+local outnets = luci.controller.luci_diag.smap_common.get_params()
+luci.controller.luci_diag.devinfo_common.run_processes(outnets, luci.controller.luci_diag.smap_common.command_function)
+luci.controller.luci_diag.devinfo_common.parse_output(m, outnets, true, "smap", false, debug)
+luci.controller.luci_diag.smap_common.action_links(m, false)
+
+return m

+ 35 - 0
luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/smap_devinfo_config.lua

@@ -0,0 +1,35 @@
+--[[
+LuCI - Lua Configuration Interface
+
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+require("luci.controller.luci_diag.devinfo_common")
+
+m = Map("luci_devinfo", translate("SIP Device Scanning Configuration"), translate("Configure scanning for supported SIP devices on specified networks. Decreasing \'Timeout\', \'Repeat Count\', and/or \'Sleep Between Requests\' may speed up scans, but also may fail to find some devices."))
+
+s = m:section(SimpleSection, "", translate("Use Configuration"))
+b = s:option(DummyValue, "_scans", translate("Perform Scans (this can take a few minutes)"))
+b.value = ""
+b.titleref = luci.dispatcher.build_url("admin", "status", "smap_devinfo")
+
+scannet = m:section(TypedSection, "smap_scannet", translate("Scanning Configuration"), translate("Networks to scan for supported devices"))
+scannet.addremove = true
+scannet.anonymous = false
+
+local ports
+ports = scannet:option(Value, "ports", translate("Ports"))
+ports.optional = true
+ports.rmempty = true
+
+luci.controller.luci_diag.devinfo_common.config_devinfo_scan(m, scannet)
+
+return m

+ 36 - 0
luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/smap_devinfo_config_mini.lua

@@ -0,0 +1,36 @@
+--[[
+LuCI - Lua Configuration Interface
+
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+require("luci.controller.luci_diag.devinfo_common")
+
+m = Map("luci_devinfo", translate("Phone Scanning Configuration"), translate("Configure scanning for supported SIP devices on specified networks. Decreasing \'Timeout\', \'Repeat Count\', and/or \'Sleep Between Requests\' may speed up scans, but also may fail to find some devices."))
+
+s = m:section(SimpleSection, "", translate("Use Configuration"))
+b = s:option(DummyValue, "_scans", translate("Perform Scans (this can take a few minutes)"))
+b.value = ""
+b.titleref = luci.dispatcher.build_url("mini", "diag", "phone_scan")
+
+scannet = m:section(TypedSection, "smap_scannet", translate("Scanning Configuration"), translate("Networks to scan for supported devices"))
+scannet.addremove = true
+scannet.anonymous = false
+
+local ports
+ports = scannet:option(Value, "ports", translate("Ports"))
+ports.optional = true
+ports.rmempty = true
+
+luci.controller.luci_diag.devinfo_common.config_devinfo_scan(m, scannet)
+
+
+return m

+ 33 - 0
luci/applications/luci-diag-devinfo/luasrc/model/cbi/luci_diag/smap_devinfo_mini.lua

@@ -0,0 +1,33 @@
+--[[
+smap_devinfo - SIP Device Information
+
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+]]--
+
+require("luci.i18n")
+require("luci.util")
+require("luci.sys")
+require("luci.model.uci")
+require("luci.controller.luci_diag.smap_common")
+require("luci.controller.luci_diag.devinfo_common")
+
+local debug = false
+
+m = SimpleForm("luci-smap-to-devinfo", translate("Phone Information"), translate("Scan for supported SIP devices on specified networks."))
+m.reset = false
+m.submit = false
+
+local outnets = luci.controller.luci_diag.smap_common.get_params()
+luci.controller.luci_diag.devinfo_common.run_processes(outnets, luci.controller.luci_diag.smap_common.command_function)
+luci.controller.luci_diag.devinfo_common.parse_output(m, outnets, true, "smap", true, debug)
+luci.controller.luci_diag.smap_common.action_links(m, true)
+
+return m

+ 137 - 0
luci/applications/luci-diag-devinfo/luasrc/view/diag/smapsection.htm

@@ -0,0 +1,137 @@
+<%#
+LuCI - Lua Configuration Interface
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+-%>
+<%-
+local rowcnt = 1
+function rowstyle()
+	rowcnt = rowcnt + 1
+	return (rowcnt % 2) + 1
+end
+-%>
+
+<!-- smapsection -->
+<fieldset class="cbi-section" id="cbi-<%=self.config%>-<%=self.sectiontype%>">
+	<% if self.title and #self.title > 0 then -%>
+		<legend><%=self.title%></legend>
+	<%- end %>
+	<div class="cbi-section-descr"><%=self.description%></div>
+	<div class="cbi-section-node">
+		<%- local count = 0 -%>
+		<table class="cbi-section-table">
+			<tr class="cbi-section-table-titles">
+			<%- if not self.anonymous then -%>
+				<%- if self.sectionhead then -%>
+					<th class="cbi-section-table-cell"><%=self.sectionhead%></th>
+				<%- else -%>
+					<th>&#160;</th>
+				<%- end -%>
+			<%- end -%>
+			<%- for i, k in pairs(self.children) do if not k.optional then -%>
+				<th class="cbi-section-table-cell">
+				<%- if k.titleref then -%><a title="<%=self.titledesc or translate('Go to relevant configuration page')%>" class="cbi-title-ref" href="<%=k.titleref%>"><%- end -%>
+					<%-=k.title-%>
+				<%- if k.titleref then -%></a><%- end -%>
+				</th>
+			<%- count = count + 1; end; end; if self.extedit or self.addremove then -%>
+				<th class="cbi-section-table-cell">&#160;</th>
+			<%- count = count + 1; end -%>
+			</tr>
+			<tr class="cbi-section-table-descr">
+			<%- if not self.anonymous then -%>
+				<%- if self.sectiondesc then -%>
+					<th class="cbi-section-table-cell"><%=self.sectiondesc%></th>
+				<%- else -%>
+					<th></th>
+				<%- end -%>
+			<%- end -%>
+			<%- for i, k in pairs(self.children) do if not k.optional then -%>
+				<th class="cbi-section-table-cell"><%=k.description%></th>
+			<%- end; end; if self.extedit or self.addremove then -%>
+				<th class="cbi-section-table-cell"></th>
+			<%- end -%>
+			</tr>
+			<%- local isempty = true
+			    for i, k in ipairs(self:cfgsections()) do
+					section = k
+					isempty = false
+					scope = { valueheader = "cbi/cell_valueheader", valuefooter = "cbi/cell_valuefooter" }
+			-%>
+			<tr class="cbi-section-table-row<% if self.extedit or self.rowcolors then %> cbi-rowstyle-<%=rowstyle()%><% end %>" id="cbi-<%=self.config%>-<%=section%>">
+				<% if not self.anonymous then -%>
+					<th><h3><%=k%></h3></th>
+				<%- end %>
+
+				<%-	for k, node in ipairs(self.children) do -%>
+					<%-    if not node.optional then -%>
+					       <%- nodevalue = node:cfgvalue(section) -%>
+					       <%- if nodevalue and ( nodevalue ~= "" ) and string.find(nodevalue, 'http://', 1, plain) then
+					       	       node.href = nodevalue
+						       node.template = "diag/smapvalue"
+						    end 
+                                               -%>
+					       <%- node:render(section, scope or {}) -%>
+					<%- end -%>
+                                <%- end -%>
+
+				<%- if self.extedit or self.addremove then -%>
+					<td class="cbi-section-table-cell">
+						<%- if self.extedit then -%>
+							<a href="
+							<%- if type(self.extedit) == "string" then -%>
+								<%=self.extedit:format(section)%>
+							<%- elseif type(self.extedit) == "function" then -%>
+								<%=self:extedit(section)%>
+							<%- end -%>
+							" title="<%:Edit%>"><img style="border: none" src="<%=resource%>/cbi/edit.gif" alt="<%:Edit%>" /></a>
+						<%- end; if self.addremove then %>
+							<input type="image" value="<%:Delete%>" name="cbi.rts.<%=self.config%>.<%=k%>" alt="<%:Delete%>" title="<%:Delete%>" src="<%=resource%>/cbi/remove.gif" />
+						<%- end -%>
+					</td>
+				<%- end -%>
+			</tr>
+			<%- end -%>
+
+			<%- if isempty then -%>
+			<tr class="cbi-section-table-row">
+				<td colspan="<%=count%>"><em><br /><%:This section contains no values yet%></em></td>
+			</tr>
+			<%- end -%>
+		</table>
+
+		<% if self.error then %>
+			<div class="cbi-section-error">
+				<ul><% for _, c in pairs(self.error) do for _, e in ipairs(c) do -%>
+					<li><%=luci.util.pcdata(e):gsub("\n","<br />")%></li>
+				<%- end end %></ul>
+			</div>
+		<% end %>
+
+		<%- if self.addremove then -%>
+			<% if self.template_addremove then include(self.template_addremove) else -%>
+			<div class="cbi-section-create cbi-smapsection-create">
+				<% if self.anonymous then %>
+					<input class="cbi-button cbi-button-add" type="submit" value="<%:Add%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>" title="<%:Add%>" />
+				<% else %>
+					<% if self.invalid_cts then -%><div class="cbi-section-error"><% end %>
+					<input type="text" class="cbi-section-create-name" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>" />
+					<input class="cbi-button cbi-button-add" type="submit" value="<%:Add%>" title="<%:Add%>" />
+					<% if self.invalid_cts then -%>
+						<br /><%:Invalid%></div>
+					<%- end %>
+				<% end %>
+			</div>
+			<%- end %>
+		<%- end -%>
+	</div>
+</fieldset>
+<!-- /smapsection -->

+ 21 - 0
luci/applications/luci-diag-devinfo/luasrc/view/diag/smapvalue.htm

@@ -0,0 +1,21 @@
+<%#
+LuCI - Lua Configuration Interface
+(c) 2009 Daniel Dickinson
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+	http://www.apache.org/licenses/LICENSE-2.0
+
+$Id$
+
+-%>
+
+<%+cbi/valueheader%>
+<% if self.href then %><a href="<%=self.href%>" target="_blank"><% end -%>
+	<%=luci.util.pcdata(self:cfgvalue(section))%>
+<%- if self.href then %></a><%end%>
+&#160;
+<input type="hidden" id="<%=cbid%>" value="<%=luci.util.pcdata(self:cfgvalue(section))%>" />
+<%+cbi/valuefooter%>

Some files were not shown because too many files changed in this diff