Browse Source

gluon-web: add package

The gluon-web package is basically a stripped-down and refactored version
of the LuCI base.
Matthias Schiffer 7 years ago
parent
commit
e4b74be506
40 changed files with 4058 additions and 0 deletions
  1. 128 0
      contrib/i18n-scan.pl
  2. 53 0
      package/gluon-web/Makefile
  3. 14 0
      package/gluon-web/files/lib/gluon/web/view/csrftoken.html
  4. 9 0
      package/gluon-web/files/lib/gluon/web/view/error404.html
  5. 9 0
      package/gluon-web/files/lib/gluon/web/view/error500.html
  6. 3 0
      package/gluon-web/files/lib/gluon/web/view/layout.html
  7. 20 0
      package/gluon-web/files/lib/gluon/web/view/model/dynlist.html
  8. 28 0
      package/gluon-web/files/lib/gluon/web/view/model/form.html
  9. 5 0
      package/gluon-web/files/lib/gluon/web/view/model/fvalue.html
  10. 41 0
      package/gluon-web/files/lib/gluon/web/view/model/lvalue.html
  11. 28 0
      package/gluon-web/files/lib/gluon/web/view/model/section.html
  12. 3 0
      package/gluon-web/files/lib/gluon/web/view/model/tvalue.html
  13. 12 0
      package/gluon-web/files/lib/gluon/web/view/model/value.html
  14. 18 0
      package/gluon-web/files/lib/gluon/web/view/model/valuewrapper.html
  15. 6 0
      package/gluon-web/files/lib/gluon/web/view/model/wrapper.html
  16. 8 0
      package/gluon-web/files/lib/gluon/web/www/index.html
  17. 0 0
      package/gluon-web/files/lib/gluon/web/www/static/resources/gluon-web.js
  18. 56 0
      package/gluon-web/i18n/de.po
  19. 54 0
      package/gluon-web/i18n/fr.po
  20. 45 0
      package/gluon-web/i18n/gluon-web.pot
  21. 531 0
      package/gluon-web/javascript/gluon-web.js
  22. 3 0
      package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon
  23. 38 0
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/cgi.lua
  24. 258 0
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua
  25. 123 0
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua
  26. 268 0
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua
  27. 466 0
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua
  28. 167 0
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/model/datatypes.lua
  29. 92 0
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua
  30. 100 0
      package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua
  31. 16 0
      package/gluon-web/src/Makefile
  32. 288 0
      package/gluon-web/src/template_lmo.c
  33. 81 0
      package/gluon-web/src/template_lmo.h
  34. 121 0
      package/gluon-web/src/template_lualib.c
  35. 30 0
      package/gluon-web/src/template_lualib.h
  36. 419 0
      package/gluon-web/src/template_parser.c
  37. 80 0
      package/gluon-web/src/template_parser.h
  38. 384 0
      package/gluon-web/src/template_utils.c
  39. 51 0
      package/gluon-web/src/template_utils.h
  40. 2 0
      package/gluon.mk

+ 128 - 0
contrib/i18n-scan.pl

@@ -0,0 +1,128 @@
+#!/usr/bin/perl
+
+use strict;
+use warnings;
+use Text::Balanced qw(extract_bracketed extract_delimited extract_tagged);
+
+@ARGV >= 1 || die "Usage: $0 <source direcory>\n";
+
+
+my %stringtable;
+
+sub dec_lua_str
+{
+	my $s = shift;
+	$s =~ s/[\s\n]+/ /g;
+	$s =~ s/\\n/\n/g;
+	$s =~ s/\\t/\t/g;
+	$s =~ s/\\(.)/$1/g;
+	$s =~ s/^ //;
+	$s =~ s/ $//;
+	return $s;
+}
+
+sub dec_tpl_str
+{
+	my $s = shift;
+	$s =~ s/-$//;
+	$s =~ s/[\s\n]+/ /g;
+	$s =~ s/^ //;
+	$s =~ s/ $//;
+	$s =~ s/\\/\\\\/g;
+	return $s;
+}
+
+
+if( open F, "find @ARGV -type f '(' -name '*.html' -o -name '*.lua' ')' |" )
+{
+	while( defined( my $file = readline F ) )
+	{
+		chomp $file;
+
+		if( open S, "< $file" )
+		{
+			local $/ = undef;
+			my $raw = <S>;
+			close S;
+
+
+			my $text = $raw;
+
+			while( $text =~ s/ ^ .*? (?:translate|translatef|i18n|_) [\n\s]* \( /(/sgx )
+			{
+				( my $code, $text ) = extract_bracketed($text, q{('")});
+
+				$code =~ s/\\\n/ /g;
+				$code =~ s/^\([\n\s]*//;
+				$code =~ s/[\n\s]*\)$//;
+
+				my $res = "";
+				my $sub = "";
+
+				if( $code =~ /^['"]/ )
+				{
+					while( defined $sub )
+					{
+						( $sub, $code ) = extract_delimited($code, q{'"}, q{\s*(?:\.\.\s*)?});
+
+						if( defined $sub && length($sub) > 2 )
+						{
+							$res .= substr $sub, 1, length($sub) - 2;
+						}
+						else
+						{
+							undef $sub;
+						}
+					}
+				}
+				elsif( $code =~ /^(\[=*\[)/ )
+				{
+					my $stag = quotemeta $1;
+					my $etag = $stag;
+					   $etag =~ s/\[/]/g;
+
+					( $res ) = extract_tagged($code, $stag, $etag);
+
+					$res =~ s/^$stag//;
+					$res =~ s/$etag$//;
+				}
+
+				$res = dec_lua_str($res);
+				$stringtable{$res}++ if $res;
+			}
+
+
+			$text = $raw;
+
+			while( $text =~ s/ ^ .*? <% -? [:_] /<%/sgx )
+			{
+				( my $code, $text ) = extract_tagged($text, '<%', '%>');
+
+				if( defined $code )
+				{
+					$code = dec_tpl_str(substr $code, 2, length($code) - 4);
+					$stringtable{$code}++;
+				}
+			}
+		}
+	}
+
+	close F;
+}
+
+
+if( open C, "| msgcat -" )
+{
+	printf C "msgid \"\"\nmsgstr \"Content-Type: text/plain; charset=UTF-8\"\n\n";
+
+	foreach my $key ( sort keys %stringtable )
+	{
+		if( length $key )
+		{
+			$key =~ s/"/\\"/g;
+			printf C "msgid \"%s\"\nmsgstr \"\"\n\n", $key;
+		}
+	}
+
+	close C;
+}

+ 53 - 0
package/gluon-web/Makefile

@@ -0,0 +1,53 @@
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=gluon-web
+PKG_VERSION:=1
+
+PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
+
+include ../gluon.mk
+
+PKG_CONFIG_DEPENDS += $(GLUON_I18N_CONFIG)
+
+PKG_INSTALL:=1
+
+
+define Package/gluon-web
+  SECTION:=gluon
+  CATEGORY:=Gluon
+  TITLE:=Minimal Lua web framework derived from LuCI
+  DEPENDS:=+luci-lib-jsonc +luci-lib-nixio
+endef
+
+define lang-config
+
+config GLUON_WEB_LANG_$(1)
+	bool "$(GLUON_LANG_$(1)) language support for gluon-web"
+	depends on PACKAGE_gluon-web
+
+endef
+
+define Package/gluon-web/config
+$(foreach lang,$(GLUON_SUPPORTED_LANGS),$(call lang-config,$(lang)))
+endef
+
+define Build/Prepare
+	mkdir -p $(PKG_BUILD_DIR)
+	$(CP) ./src/* $(PKG_BUILD_DIR)/
+endef
+
+define Build/Compile
+	$(call Build/Compile/Default)
+	$(call GluonBuildI18N,gluon-web,i18n)
+	$(call GluonSrcDiet,./luasrc,$(PKG_BUILD_DIR)/luadest/)
+endef
+
+define Package/gluon-web/install
+	$(CP) ./files/* $(1)/
+	$(CP) $(PKG_INSTALL_DIR)/* $(1)/
+	$(CP) $(PKG_BUILD_DIR)/luadest/* $(1)/
+	$(call GluonInstallI18N,gluon-web,$(1))
+
+endef
+
+$(eval $(call BuildPackage,gluon-web))

+ 14 - 0
package/gluon-web/files/lib/gluon/web/view/csrftoken.html

@@ -0,0 +1,14 @@
+<%#
+ Copyright 2015 Jo-Philipp Wich <jow@openwrt.org>
+ Licensed to the public under the Apache License 2.0.
+-%>
+
+<h2 name="content"><%:Form token mismatch%></h2>
+<br />
+
+<p class="alert-message"><%:The submitted security token is invalid or already expired!%></p>
+
+<p><%:
+	In order to prevent unauthorized access to the system, your request has
+	been blocked.
+%></p>

+ 9 - 0
package/gluon-web/files/lib/gluon/web/view/error404.html

@@ -0,0 +1,9 @@
+<%#
+ Copyright 2008 Steven Barth <steven@midlink.org>
+ Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
+ Licensed to the public under the Apache License 2.0.
+-%>
+
+<h2 name="content">404 <%:Not Found%></h2>
+<p><%:Sorry, the object you requested was not found.%></p>
+<tt><%=pcdata(message)%></tt>

+ 9 - 0
package/gluon-web/files/lib/gluon/web/view/error500.html

@@ -0,0 +1,9 @@
+<%#
+ Copyright 2008 Steven Barth <steven@midlink.org>
+ Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
+ Licensed to the public under the Apache License 2.0.
+-%>
+
+<h2 name="content">500 <%:Internal Server Error%></h2>
+<p><%:Sorry, the server encountered an unexpected error.%></p>
+<pre class="error500"><%=pcdata(message)%></pre>

+ 3 - 0
package/gluon-web/files/lib/gluon/web/view/layout.html

@@ -0,0 +1,3 @@
+<%
+	include("themes/" .. theme .. "/layout")
+%>

+ 20 - 0
package/gluon-web/files/lib/gluon/web/view/model/dynlist.html

@@ -0,0 +1,20 @@
+<div<%=
+	attr("data-prefix", id) ..
+	attr("data-dynlist", {
+		type = self.datatype,
+		optional = self.datatype and self.optional,
+	}) ..
+	attr("data-size", self.size) ..
+	attr("data-placeholder", self.placeholder)
+%>>
+<%
+	for i, val in ipairs(self:cfgvalue()) do
+%>
+	<input class="gluon-input-text" value="<%=pcdata(val)%>" data-update="change" type="text"<%=
+		attr("id", id .. "." .. i) ..
+		attr("name", id) ..
+		attr("size", self.size) ..
+		attr("placeholder", self.placeholder)
+	%> /><br />
+<% end %>
+</div>

+ 28 - 0
package/gluon-web/files/lib/gluon/web/view/model/form.html

@@ -0,0 +1,28 @@
+<form method="post" enctype="multipart/form-data" action="<%=url(request)%>">
+	<input type="hidden" name="token" value="<%=token%>" />
+	<input type="hidden" name="<%=id%>" value="1" />
+
+	<div class="gluon-map" id="gluon-<%=self.config%>">
+		<% if self.title and #self.title > 0 then %><h2 name="content"><%=self.title%></h2><% end %>
+		<% if self.description and #self.description > 0 then %><div class="gluon-map-descr"><%=self.description%></div><% end %>
+		<% self:render_children(renderer) %>
+	</div>
+<%- if self.message then %>
+	<div><%=self.message%></div>
+<%- end %>
+<%- if self.errmessage then %>
+	<div class="error"><%=self.errmessage%></div>
+<%- end %>
+	<div class="gluon-page-actions">
+	<%- if self.submit ~= false then %>
+		<input class="gluon-button gluon-button-submit" type="submit" value="
+			<%- if not self.submit then -%><%-:Save-%><%-else-%><%=pcdata(self.submit)%><%end-%>
+		" />
+	<% end %>
+	<%- if self.reset ~= false then %>
+		<input class="gluon-button gluon-button-reset" type="reset" value="
+			<%- if not self.reset then -%><%-:Reset-%><%-else-%><%=pcdata(self.reset)%><%end-%>
+		" />
+	<% end %>
+	</div>
+</form>

+ 5 - 0
package/gluon-web/files/lib/gluon/web/view/model/fvalue.html

@@ -0,0 +1,5 @@
+<input class="gluon-input-checkbox" data-update="click change" type="checkbox" value="1"<%=
+	attr("id", id) .. attr("name", id) ..
+	attr("checked", self:cfgvalue() and "checked")
+%> />
+<label<%= attr("for", id)%>></label>

+ 41 - 0
package/gluon-web/files/lib/gluon/web/view/model/lvalue.html

@@ -0,0 +1,41 @@
+<%
+	local i, key
+	local br = self.orientation == "horizontal" and '&#160;' or '<br />'
+%>
+
+<% if self.widget == "select" then %>
+	<select class="gluon-input-select" data-update="change"<%=
+		attr("id", id) ..
+		attr("name", id) ..
+		attr("size", self.size)
+	%>>
+		<% for i, key in pairs(self.keylist) do -%>
+			<option<%=
+				attr("id", id.."."..key) ..
+				attr("value", key) ..
+				attr("data-index", i) ..
+				attr("data-depends", self:deplist(self.valdeps[i])) ..
+				attr("selected", (self:cfgvalue() == key) and "selected")
+			%>><%=pcdata(self.vallist[i])%></option>
+		<%- end %>
+	</select>
+<% elseif self.widget == "radio" then %>
+	<div>
+		<% for i, key in pairs(self.keylist) do %>
+			<label<%=
+				attr("data-index", i) ..
+				attr("data-depends", self:deplist(self.valdeps[i]))
+			%>>
+				<input class="gluon-input-radio" data-update="click change" type="radio"<%=
+					attr("id", id.."."..key) ..
+					attr("name", id) ..
+					attr("value", key) ..
+					attr("checked", (self:cfgvalue() == key) and "checked")
+				%> />
+				<label<%= attr("for", id.."-"..key)%>></label>
+				<%=pcdata(self.vallist[i])%>
+			</label>
+			<% if i == self.size then write(br) end %>
+		<% end %>
+	</div>
+<% end %>

+ 28 - 0
package/gluon-web/files/lib/gluon/web/view/model/section.html

@@ -0,0 +1,28 @@
+<fieldset class="gluon-section">
+	<% if self.title and #self.title > 0 then -%>
+		<legend><%=self.title%></legend>
+	<%- end %>
+	<% if self.description and #self.description > 0 then -%>
+		<div class="gluon-section-descr"><%=self.description%></div>
+	<%- end %>
+	<div class="gluon-section-node">
+		<div id="section-<%=id%>">
+			<% self:render_children(renderer, scope) %>
+		</div>
+		<% if self.error and self.error[1] then -%>
+			<div class="gluon-section-error">
+				<ul><% for _, e in ipairs(self.error[1]) do -%>
+					<li>
+						<%- if e == "invalid" then -%>
+							<%:One or more fields contain invalid values!%>
+						<%- elseif e == "missing" then -%>
+							<%:One or more required fields have no value!%>
+						<%- else -%>
+							<%=pcdata(e)%>
+						<%- end -%>
+					</li>
+				<%- end %></ul>
+			</div>
+		<%- end %>
+	</div>
+</fieldset>

+ 3 - 0
package/gluon-web/files/lib/gluon/web/view/model/tvalue.html

@@ -0,0 +1,3 @@
+<textarea class="gluon-input-textarea" <% if not self.size then %> style="width: 100%"<% else %> cols="<%=self.size%>"<% end %> data-update="change"<%= attr("name", id) .. attr("id", id) .. attr("rows", self.rows) .. attr("wrap", self.wrap) %>>
+<%-=pcdata(self:cfgvalue())-%>
+</textarea>

+ 12 - 0
package/gluon-web/files/lib/gluon/web/view/model/value.html

@@ -0,0 +1,12 @@
+<input data-update="change"<%=
+	attr("id", id) ..
+	attr("name", id) ..
+	attr("type", self.password and "password" or "text") ..
+	attr("class", self.password and "gluon-input-password" or "gluon-input-text") ..
+	attr("value", self:cfgvalue()) ..
+	attr("size", self.size) ..
+	attr("placeholder", self.placeholder) ..
+	attr("maxlength", self.maxlength) ..
+	attr("data-type", self.datatype) ..
+	attr("data-optional", self.datatype and self.optional)
+%> />

+ 18 - 0
package/gluon-web/files/lib/gluon/web/view/model/valuewrapper.html

@@ -0,0 +1,18 @@
+<div class="gluon-value<% if self.error then %> gluon-value-error<% end %>" id="value-<%=id%>" data-index="<%=self.index%>"<%= attr("data-depends", self:deplist()) %>>
+	<%- if self.title and #self.title > 0 then -%>
+		<label class="gluon-value-title"<%= attr("for", id) %>>
+			<%-=self.title-%>
+		</label>
+		<div class="gluon-value-field">
+	<%- end -%>
+	<% if self.subtemplate then include(self.subtemplate) end %>
+	<% if self.description and #self.description > 0 then -%>
+		<br />
+		<div class="gluon-value-description">
+			<%=self.description%>
+		</div>
+	<%- end %>
+	<%- if self.title and #self.title > 0 then -%>
+		</div>
+	<%- end -%>
+</div>

+ 6 - 0
package/gluon-web/files/lib/gluon/web/view/model/wrapper.html

@@ -0,0 +1,6 @@
+<%
+	for _, map in ipairs(maps) do
+		map:render(renderer)
+	end
+%>
+<script type="text/javascript" src="<%=resource%>/gluon-web.js"></script>

+ 8 - 0
package/gluon-web/files/lib/gluon/web/www/index.html

@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml">
+	<head>
+		<meta http-equiv="refresh" content="0; URL=/cgi-bin/gluon" />
+	</head>
+	<body>
+	</body>
+</html>

File diff suppressed because it is too large
+ 0 - 0
package/gluon-web/files/lib/gluon/web/www/static/resources/gluon-web.js


+ 56 - 0
package/gluon-web/i18n/de.po

@@ -0,0 +1,56 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"PO-Revision-Date: 2013-03-29 12:13+0200\n"
+"Last-Translator: Matthias Schiffer <mschiffer@universe-factory.net>\n"
+"Language-Team: German\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n != 1);\n"
+
+msgid "Form token mismatch"
+msgstr "Formular-Token ungültig"
+
+msgid ""
+"In order to prevent unauthorized access to the system, your request has been "
+"blocked."
+msgstr ""
+"Die Anfrage wurde blockiert, um unauthorisierten Zugriff aufs System zu verhindern."
+
+msgid "Internal Server Error"
+msgstr "Interner Serverfehler"
+
+msgid "JavaScript required!"
+msgstr "JavaScript benötigt!"
+
+msgid "Not Found"
+msgstr "Nicht Gefunden"
+
+msgid "One or more fields contain invalid values!"
+msgstr "Ein oder mehrere Felder enthalten ungültige Werte!"
+
+msgid "One or more required fields have no value!"
+msgstr "Ein oder mehr benötigte Felder sind nicht ausgefüllt!"
+
+msgid "Reset"
+msgstr "Zurücksetzen"
+
+msgid "Save"
+msgstr "Speichern"
+
+msgid "Sorry, the object you requested was not found."
+msgstr "Entschuldigung, das anfgeforderte Objekt wurde nicht gefunden."
+
+msgid "Sorry, the server encountered an unexpected error."
+msgstr ""
+"Entschuldigung, auf dem Server ist ein unerwarteter Fehler aufgetreten."
+
+msgid "The submitted security token is invalid or already expired!"
+msgstr "Das übermittelte Sicherheits-Token ist ungültig oder bereits abgelaufen!"
+
+msgid ""
+"You must enable JavaScript in your browser or the web interface will not "
+"work properly."
+msgstr ""

+ 54 - 0
package/gluon-web/i18n/fr.po

@@ -0,0 +1,54 @@
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"PO-Revision-Date: 2013-12-22 17:11+0200\n"
+"Last-Translator: goofy <pierre.gaufillet@gmail.com>\n"
+"Language-Team: French\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
+
+msgid "Form token mismatch"
+msgstr ""
+
+msgid ""
+"In order to prevent unauthorized access to the system, your request has been "
+"blocked."
+msgstr ""
+
+msgid "Internal Server Error"
+msgstr "Erreur Serveur Interne"
+
+msgid "JavaScript required!"
+msgstr ""
+
+msgid "Not Found"
+msgstr "Pas trouvé"
+
+msgid "One or more fields contain invalid values!"
+msgstr "Un ou plusieurs champs contiennent des valeurs incorrectes !"
+
+msgid "One or more required fields have no value!"
+msgstr "Un ou plusieurs champs n'ont pas de valeur !"
+
+msgid "Reset"
+msgstr "Remise à zéro"
+
+msgid "Save"
+msgstr "Soumettre"
+
+msgid "Sorry, the object you requested was not found."
+msgstr "Désolé, l'objet que vous avez demandé n'as pas été trouvé."
+
+msgid "Sorry, the server encountered an unexpected error."
+msgstr "Désolé, le serveur à rencontré une erreur inattendue."
+
+msgid "The submitted security token is invalid or already expired!"
+msgstr ""
+
+msgid ""
+"You must enable JavaScript in your browser or the web interface will not "
+"work properly."
+msgstr ""

+ 45 - 0
package/gluon-web/i18n/gluon-web.pot

@@ -0,0 +1,45 @@
+msgid ""
+msgstr "Content-Type: text/plain; charset=UTF-8"
+
+msgid "Form token mismatch"
+msgstr ""
+
+msgid ""
+"In order to prevent unauthorized access to the system, your request has been "
+"blocked."
+msgstr ""
+
+msgid "Internal Server Error"
+msgstr ""
+
+msgid "JavaScript required!"
+msgstr ""
+
+msgid "Not Found"
+msgstr ""
+
+msgid "One or more fields contain invalid values!"
+msgstr ""
+
+msgid "One or more required fields have no value!"
+msgstr ""
+
+msgid "Reset"
+msgstr ""
+
+msgid "Save"
+msgstr ""
+
+msgid "Sorry, the object you requested was not found."
+msgstr ""
+
+msgid "Sorry, the server encountered an unexpected error."
+msgstr ""
+
+msgid "The submitted security token is invalid or already expired!"
+msgstr ""
+
+msgid ""
+"You must enable JavaScript in your browser or the web interface will not "
+"work properly."
+msgstr ""

+ 531 - 0
package/gluon-web/javascript/gluon-web.js

@@ -0,0 +1,531 @@
+/*
+	Copyright 2008 Steven Barth <steven@midlink.org>
+	Copyright 2008-2012 Jo-Philipp Wich <jow@openwrt.org>
+	Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.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
+*/
+
+/*
+	Build using:
+
+	uglifyjs javascript/gluon-web.js -o files/lib/gluon/web/www/static/resources/gluon-web.js -c -m --support-ie8
+*/
+
+
+
+(function() {
+	var dep_entries = {};
+
+	function Int(x) {
+		return (/^-?\d+$/.test(x) ? +x : NaN);
+	}
+
+	function Dec(x) {
+		return (/^-?\d*\.?\d+?$/.test(x) ? +x : NaN);
+	}
+
+	var validators = {
+
+		'integer': function() {
+			return !isNaN(Int(this));
+		},
+
+		'uinteger': function() {
+			return (Int(this) >= 0);
+		},
+
+		'float': function() {
+			return !isNaN(Dec(this));
+		},
+
+		'ufloat': function() {
+			return (Dec(this) >= 0);
+		},
+
+		'ipaddr': function() {
+			return validators.ip4addr.apply(this) ||
+				validators.ip6addr.apply(this);
+		},
+
+		'ip4addr': function() {
+			if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)) {
+				return (RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
+				       (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
+				       (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
+				       (RegExp.$4 >= 0) && (RegExp.$4 <= 255);
+			}
+
+			return false;
+		},
+
+		'ip6addr': function() {
+			if (this.indexOf('::') < 0)
+				return (this.match(/^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i) != null);
+
+			if (
+				(this.indexOf(':::') >= 0) || this.match(/::.+::/) ||
+				this.match(/^:[^:]/) || this.match(/[^:]:$/)
+			)
+				return false;
+
+			if (this.match(/^(?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}$/i))
+				return true;
+			if (this.match(/^(?:[a-f0-9]{1,4}:){7}:$/i))
+				return true;
+			if (this.match(/^:(?::[a-f0-9]{1,4}){7}$/i))
+				return true;
+
+			return false;
+		},
+
+		'wpakey': function() {
+			var v = this;
+
+			if (v.length == 64)
+				return (v.match(/^[a-f0-9]{64}$/i) != null);
+			else
+				return (v.length >= 8) && (v.length <= 63);
+		},
+
+		'range': function(min, max)	{
+			var val = Dec(this);
+			return (val >= +min && val <= +max);
+		},
+
+		'min': function(min) {
+			return (Dec(this) >= +min);
+		},
+
+		'max': function(max) {
+			return (Dec(this) <= +max);
+		},
+
+		'irange': function(min, max) {
+			var val = Int(this);
+			return (val >= +min && val <= +max);
+		},
+
+		'imin': function(min) {
+			return (Int(this) >= +min);
+		},
+
+		'imax': function(max)	{
+			return (Int(this) <= +max);
+		},
+
+		'minlength': function(min) {
+			return ((''+this).length >= +min);
+		},
+
+		'maxlength': function(max) {
+			return ((''+this).length <= +max);
+		},
+	};
+
+	function compile(type) {
+		var v;
+		if (type.match(/^([^\(]+)\(([^,]+),([^\)]+)\)$/) && (v = validators[RegExp.$1]) !== undefined) {
+			return function() {
+				return v(RegExp.$2, RegExp.$3);
+			}
+		} else if (type.match(/^([^\(]+)\(([^,\)]+)\)$/) && (v = validators[RegExp.$1]) !== undefined) {
+			return function() {
+				return v(RegExp.$2);
+			}
+		} else {
+			return validators[type];
+		}
+	}
+
+	function checkvalue(target, ref) {
+		var t = document.getElementById(target);
+		var value;
+
+		if (t) {
+			if (t.type == "checkbox") {
+				value = t.checked;
+			} else if (t.value) {
+				value = t.value;
+			} else {
+				value = "";
+
+			}
+		}
+
+		return (value == ref)
+	}
+
+	function check(deps) {
+		for (var i=0; i < deps.length; i++) {
+			var stat = true;
+
+			for (var j in deps[i]) {
+				stat = (stat && checkvalue(j, deps[i][j]));
+			}
+
+			if (stat)
+				return true;
+		}
+
+		return false;
+	}
+
+	function update() {
+		var state = false;
+		for (var id in dep_entries) {
+			var entry = dep_entries[id];
+			var node  = document.getElementById(id);
+			var parent = document.getElementById(entry.parent);
+
+			if (node && node.parentNode && !check(entry.deps)) {
+				node.parentNode.removeChild(node);
+				state = true;
+			} else if (parent && (!node || !node.parentNode) && check(entry.deps)) {
+				var next = undefined;
+
+				for (next = parent.firstChild; next; next = next.nextSibling) {
+					if (next.getAttribute && parseInt(next.getAttribute('data-index'), 10) > entry.index) {
+						break;
+					}
+				}
+
+				if (!next) {
+					parent.appendChild(entry.node);
+				} else {
+					parent.insertBefore(entry.node, next);
+				}
+
+				state = true;
+			}
+
+			// hide optionals widget if no choices remaining
+			if (parent && parent.parentNode && parent.getAttribute('data-optionals'))
+				parent.parentNode.style.display = (parent.options.length <= 1) ? 'none' : '';
+		}
+
+		if (state) {
+			update();
+		}
+	}
+
+	function bind(obj, type, callback, mode) {
+		if (!obj.addEventListener) {
+			obj.attachEvent('on' + type,
+				function() {
+					var e = window.event;
+
+					if (!e.target && e.srcElement)
+						e.target = e.srcElement;
+
+					return !!callback(e);
+				}
+			);
+		} else {
+			obj.addEventListener(type, callback, !!mode);
+		}
+		return obj;
+	}
+
+	function init_dynlist(parent, datatype, optional) {
+		var prefix = parent.getAttribute('data-prefix');
+		var holder = parent.getAttribute('data-placeholder');
+
+
+		function dynlist_redraw(focus, add, del) {
+			var values = [];
+
+			while (parent.firstChild) {
+				var n = parent.firstChild;
+				var i = +n.index;
+
+				if (i != del) {
+					if (n.nodeName.toLowerCase() == 'input')
+						values.push(n.value || '');
+					else if (n.nodeName.toLowerCase() == 'select')
+						values[values.length-1] = n.options[n.selectedIndex].value;
+				}
+
+				parent.removeChild(n);
+			}
+
+			if (add >= 0) {
+				focus = add + 1;
+				values.splice(add, 0, '');
+			} else if (!optional && values.length == 0) {
+				values.push('');
+			}
+
+			for (var i = 1; i <= values.length; i++) {
+				var t = document.createElement('input');
+					t.id = prefix + '.' + i;
+					t.name = prefix;
+					t.value = values[i-1];
+					t.type = 'text';
+					t.index = i;
+					t.className = 'gluon-input-text';
+
+				if (holder)
+					t.placeholder = holder;
+
+				parent.appendChild(t);
+
+				if (datatype)
+					validate_field(t, false, datatype);
+
+				bind(t, 'keydown',  dynlist_keydown);
+				bind(t, 'keypress', dynlist_keypress);
+
+				if (i == focus) {
+					t.focus();
+				} else if (-i == focus) {
+					t.focus();
+
+					/* force cursor to end */
+					var v = t.value;
+					t.value = ' '
+					t.value = v;
+				}
+
+				if (optional || values.length > 1) {
+					var b = document.createElement('span');
+						b.className = 'gluon-remove';
+
+					parent.appendChild(b);
+
+					bind(b, 'click', dynlist_btnclick(false));
+
+					parent.appendChild(document.createElement('br'));
+				}
+			}
+
+			var b = document.createElement('span');
+				b.className = 'gluon-add';
+
+			parent.appendChild(b);
+
+			bind(b, 'click', dynlist_btnclick(true));
+		}
+
+		function dynlist_keypress(ev) {
+			ev = ev ? ev : window.event;
+
+			var se = ev.target ? ev.target : ev.srcElement;
+
+			if (se.nodeType == 3)
+				se = se.parentNode;
+
+			switch (ev.keyCode) {
+				/* backspace, delete */
+				case 8:
+				case 46:
+					if (se.value.length == 0) {
+						if (ev.preventDefault)
+							ev.preventDefault();
+
+						return false;
+					}
+
+					return true;
+
+				/* enter, arrow up, arrow down */
+				case 13:
+				case 38:
+				case 40:
+					if (ev.preventDefault)
+						ev.preventDefault();
+
+					return false;
+			}
+
+			return true;
+		}
+
+		function dynlist_keydown(ev) {
+			ev = ev ? ev : window.event;
+
+			var se = ev.target ? ev.target : ev.srcElement;
+
+			var index = 0;
+			var prev, next;
+
+			if (se) {
+				if (se.nodeType == 3)
+					se = se.parentNode;
+
+				index = se.index;
+
+				prev = se.previousSibling;
+				while (prev && prev.name != prefix)
+					prev = prev.previousSibling;
+
+				next = se.nextSibling;
+				while (next && next.name != prefix)
+					next = next.nextSibling;
+			}
+
+			switch (ev.keyCode) {
+				/* backspace, delete */
+				case 8:
+				case 46:
+					var del = (se.nodeName.toLowerCase() == 'select')
+						? true : (se.value.length == 0);
+
+					if (del) {
+						if (ev.preventDefault)
+							ev.preventDefault();
+
+						var focus = se.index;
+						if (ev.keyCode == 8)
+							focus = -focus+1;
+
+						dynlist_redraw(focus, -1, index);
+
+						return false;
+					}
+
+					break;
+
+				/* enter */
+				case 13:
+					dynlist_redraw(-1, index, -1);
+					break;
+
+				/* arrow up */
+				case 38:
+					if (prev)
+						prev.focus();
+
+					break;
+
+				/* arrow down */
+				case 40:
+					if (next)
+						next.focus();
+
+					break;
+			}
+
+			return true;
+		}
+
+		function dynlist_btnclick(add) {
+			return function(ev) {
+				ev = ev ? ev : window.event;
+
+				var se = ev.target ? ev.target : ev.srcElement;
+				var input = se.previousSibling;
+				while (input && input.name != prefix) {
+					input = input.previousSibling;
+				}
+
+				if (add) {
+					dynlist_keydown({
+						target:  input,
+						keyCode: 13
+					});
+				} else {
+					input.value = '';
+
+					dynlist_keydown({
+						target:  input,
+						keyCode: 8
+					});
+				}
+
+				return false;
+			}
+		}
+
+		dynlist_redraw(NaN, -1, -1);
+	}
+
+	function validate_field(field, optional, type) {
+		var check = compile(type);
+		if (!check)
+			return;
+
+		var validator = function() {
+			if (!field.form)
+				return;
+
+			field.className = field.className.replace(/ gluon-input-invalid/g, '');
+
+			var value = (field.options && field.options.selectedIndex > -1)
+				? field.options[field.options.selectedIndex].value : field.value;
+
+			if (!(((value.length == 0) && optional) || check.apply(value)))
+				field.className += ' gluon-input-invalid';
+		};
+
+		bind(field, "blur",  validator);
+		bind(field, "keyup", validator);
+
+		if (field.nodeName == 'SELECT') {
+			bind(field, "change", validator);
+			bind(field, "click",  validator);
+		}
+
+		validator();
+	}
+
+	function add(obj, dep, index) {
+		var entry = dep_entries[obj.id];
+		if (!entry) {
+			entry = {
+				"node": obj,
+				"parent": obj.parentNode.id,
+				"deps": [],
+				"index": index
+			};
+			dep_entries[obj.id] = entry;
+		}
+		entry.deps.push(dep)
+	}
+
+	(function() {
+		var nodes;
+
+		nodes = document.querySelectorAll('[data-depends]');
+
+		for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
+			var index = parseInt(node.getAttribute('data-index'), 10);
+			var depends = JSON.parse(node.getAttribute('data-depends'));
+			if (!isNaN(index) && depends.length > 0) {
+				for (var alt = 0; alt < depends.length; alt++) {
+					add(node, depends[alt], index);
+				}
+			}
+		}
+
+		nodes = document.querySelectorAll('[data-update]');
+
+		for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
+			var events = node.getAttribute('data-update').split(' ');
+			for (var j = 0, event; (event = events[j]) !== undefined; j++) {
+				bind(node, event, update);
+			}
+		}
+
+		nodes = document.querySelectorAll('[data-type]');
+
+		for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
+			validate_field(node, node.getAttribute('data-optional') === 'true',
+			                   node.getAttribute('data-type'));
+		}
+
+		nodes = document.querySelectorAll('[data-dynlist]');
+
+		for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
+			var list = JSON.parse(node.getAttribute('data-dynlist'));
+
+			init_dynlist(node, list.type, list.optional);
+		}
+
+		update();
+	})();
+})();

+ 3 - 0
package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon

@@ -0,0 +1,3 @@
+#!/usr/bin/lua
+require "gluon.web.cgi"
+gluon.web.cgi.run()

+ 38 - 0
package/gluon-web/luasrc/usr/lib/lua/gluon/web/cgi.lua

@@ -0,0 +1,38 @@
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
+-- Licensed to the public under the Apache License 2.0.
+
+module("gluon.web.cgi", package.seeall)
+local nixio = require("nixio")
+require("gluon.web.http")
+require("gluon.web.dispatcher")
+
+-- Limited source to avoid endless blocking
+local function limitsource(handle, limit)
+	limit = limit or 0
+	local BLOCKSIZE = 2048
+
+	return function()
+		if limit < 1 then
+			handle:close()
+			return nil
+		else
+			local read = (limit > BLOCKSIZE) and BLOCKSIZE or limit
+			limit = limit - read
+
+			local chunk = handle:read(read)
+			if not chunk then handle:close() end
+			return chunk
+		end
+	end
+end
+
+function run()
+	local http = gluon.web.http.Http(
+		nixio.getenv(),
+		limitsource(io.stdin, tonumber(nixio.getenv("CONTENT_LENGTH"))),
+		io.stdout
+	)
+
+	gluon.web.dispatcher.httpdispatch(http)
+end

+ 258 - 0
package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua

@@ -0,0 +1,258 @@
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
+-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
+-- Licensed to the public under the Apache License 2.0.
+
+local fs = require "nixio.fs"
+local tpl = require "gluon.web.template"
+local util = require "gluon.web.util"
+local proto = require "gluon.web.http.protocol"
+
+module("gluon.web.dispatcher", package.seeall)
+
+
+function build_url(http, path)
+	return (http:getenv("SCRIPT_NAME") or "") .. "/" .. table.concat(path, "/")
+end
+
+function redirect(http, ...)
+	http:redirect(build_url(http, {...}))
+end
+
+function node_visible(node)
+	return (
+		node.title and
+		node.target and
+		(not node.hidden)
+	)
+end
+
+function node_children(node)
+	if not node then return {} end
+
+	local ret = {}
+	for k, v in pairs(node.nodes) do
+		if node_visible(v) then
+			table.insert(ret, k)
+		end
+	end
+
+	table.sort(ret,
+		function(a, b)
+			return (node.nodes[a].order or 100)
+			     < (node.nodes[b].order or 100)
+		end
+	)
+	return ret
+end
+
+
+function httpdispatch(http)
+	local request = {}
+	local pathinfo = proto.urldecode(http:getenv("PATH_INFO") or "", true)
+	for node in pathinfo:gmatch("[^/]+") do
+		table.insert(request, node)
+	end
+
+	ok, err = pcall(dispatch, http, request)
+	if not ok then
+		http:status(500, "Internal Server Error")
+		http:prepare_content("text/plain")
+		http:write(err)
+	end
+end
+
+
+local function set_language(renderer, accept)
+	local langs = {}
+	local weights = {}
+	local star = 0
+
+	local function add(lang, q)
+		if not weights[lang] then
+			table.insert(langs, lang)
+			weights[lang] = q
+		end
+	end
+
+	for match in accept:gmatch("[^,]+") do
+		local lang = match:match('^%s*([^%s;-_]+)')
+		local q = tonumber(match:match(';q=(%S+)%s*$') or 1)
+
+		if lang == '*' then
+			star = q
+		elseif lang and q > 0 then
+			add(lang, q)
+		end
+	end
+
+	add('en', star)
+
+	table.sort(langs, function(a, b)
+		return (weights[a] or 0) > (weights[b] or 0)
+	end)
+
+	for _, lang in ipairs(langs) do
+		if renderer.setlanguage(lang) then
+			return
+		end
+	end
+end
+
+
+function dispatch(http, request)
+	local tree = {nodes={}}
+	local nodes = {[''] = tree}
+
+	local function _node(path, create)
+		local name = table.concat(path, ".")
+		local c = nodes[name]
+
+		if not c and create then
+			local last = table.remove(path)
+			local parent = _node(path, true)
+
+			c = {nodes={}}
+			parent.nodes[last] = c
+			nodes[name] = c
+		end
+		return c
+	end
+
+	-- Init template engine
+	local function attr(key, val)
+		if not val then
+			return ''
+		end
+
+		if type(val) == "table" then
+			val = util.serialize_json(val)
+		end
+
+		return string.format(' %s="%s"', key, util.pcdata(tostring(val)))
+	end
+
+	local renderer = tpl.renderer(setmetatable({
+		http        = http,
+		request     = request,
+		node        = function(path) return _node({path}) end,
+		write       = function(...) return http:write(...) end,
+		pcdata      = util.pcdata,
+		urlencode   = proto.urlencode,
+		media       = '/static/gluon',
+		theme       = 'gluon',
+		resource    = '/static/resources',
+		attr        = attr,
+		url         = function(path) return build_url(http, path) end,
+	}, { __index = _G }))
+
+	local subdisp = setmetatable({
+		node = function(...)
+			return _node({...})
+		end,
+
+		entry = function(path, target, title, order)
+			local c = _node(path, true)
+
+			c.target = target
+			c.title  = title
+			c.order  = order
+
+			return c
+		end,
+
+		alias = function(...)
+			local req = {...}
+			return function()
+				http:redirect(build_url(http, req))
+			end
+		end,
+
+		call = function(func, ...)
+			local args = {...}
+			return function()
+				func(http, renderer, unpack(args))
+			end
+		end,
+
+		template = function(view)
+			return function()
+				renderer.render("layout", {content = view})
+			end
+		end,
+
+		model = function(name)
+			return function()
+				local hidenav = false
+
+				local model = require "gluon.web.model"
+				local maps = model.load(name, renderer)
+
+				for _, map in ipairs(maps) do
+					map:parse(http)
+				end
+				for _, map in ipairs(maps) do
+					map:handle()
+					hidenav = hidenav or map.hidenav
+				end
+
+				renderer.render("layout", {
+					content = "model/wrapper",
+					maps = maps,
+					hidenav = hidenav,
+				})
+			end
+		end,
+
+		_ = function(text)
+			return text
+		end,
+	}, { __index = _G })
+
+	local function createtree()
+		local base = util.libpath() .. "/controller/"
+
+		local function load_ctl(path)
+			local ctl = assert(loadfile(path))
+
+			local env = setmetatable({}, { __index = subdisp })
+			setfenv(ctl, env)
+
+			ctl()
+		end
+
+		for path in (fs.glob(base .. "*.lua") or function() end) do
+			load_ctl(path)
+		end
+		for path in (fs.glob(base .. "*/*.lua") or function() end) do
+			load_ctl(path)
+		end
+	end
+
+	set_language(renderer, http:getenv("HTTP_ACCEPT_LANGUAGE") or "")
+
+	createtree()
+
+
+	local node = _node(request)
+
+	if not node or not node.target then
+		http:status(404, "Not Found")
+		renderer.render("layout", { content = "error404", message =
+			"No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
+		        "If this URL belongs to an extension, make sure it is properly installed.\n"
+		})
+		return
+	end
+
+	http:parse_input(node.filehandler)
+
+	local ok, err = pcall(node.target)
+	if not ok then
+		http:status(500, "Internal Server Error")
+		renderer.render("layout", { content = "error500", message =
+			"Failed to execute dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
+			"The called action terminated with an exception:\n" .. tostring(err or "(unknown)")
+		})
+	end
+end

+ 123 - 0
package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua

@@ -0,0 +1,123 @@
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
+-- Licensed to the public under the Apache License 2.0.
+
+local string = string
+local table = table
+local nixio = require "nixio"
+local protocol = require "gluon.web.http.protocol"
+local util  = require "gluon.web.util"
+
+local ipairs, pairs, tostring = ipairs, pairs, tostring
+
+module "gluon.web.http"
+
+
+Http = util.class()
+function Http:__init__(env, input, output)
+	self.input = input
+	self.output = output
+
+	self.request = {
+		env = env,
+		headers = {},
+		params = protocol.urldecode_params(env.QUERY_STRING or ""),
+	}
+	self.headers = {}
+end
+
+local function push_headers(self)
+	if self.eoh then return end
+
+	for _, header in pairs(self.headers) do
+		self.output:write(string.format("%s: %s\r\n", header[1], header[2]))
+	end
+	self.output:write("\r\n")
+
+	self.eoh = true
+end
+
+function Http:parse_input(filehandler)
+	protocol.parse_message_body(
+		self.input,
+		self.request,
+		filehandler
+	)
+end
+
+function Http:formvalue(name)
+	return self:formvaluetable(name)[1]
+end
+
+function Http:formvaluetable(name)
+	return self.request.params[name] or {}
+end
+
+function Http:getcookie(name)
+	local c = string.gsub(";" .. (self:getenv("HTTP_COOKIE") or "") .. ";", "%s*;%s*", ";")
+	local p = ";" .. name .. "=(.-);"
+	local i, j, value = c:find(p)
+	return value and urldecode(value)
+end
+
+function Http:getenv(name)
+	return self.request.env[name]
+end
+
+function Http:close()
+	if not self.output then return end
+
+	push_headers(self)
+
+	self.output:flush()
+	self.output:close()
+	self.output = nil
+end
+
+function Http:header(key, value)
+	self.headers[key:lower()] = {key, value}
+end
+
+function Http:prepare_content(mime)
+	if self.headers["content-type"] then return end
+
+	if mime == "application/xhtml+xml" then
+		local accept = self:getenv("HTTP_ACCEPT")
+		if not accept or not accept:find("application/xhtml+xml", nil, true) then
+			mime = "text/html; charset=UTF-8"
+		end
+		self:header("Vary", "Accept")
+	end
+	self:header("Content-Type", mime)
+end
+
+function Http:status(code, request)
+	if not self.output or self.code then return end
+
+	code = code or 200
+	request = request or "OK"
+	self.code = code
+	self.output:write(string.format("Status: %i %s\r\n", code, request))
+end
+
+function Http:write(content)
+	if not self.output then return end
+
+	self:status()
+
+	self:prepare_content("text/html; charset=utf-8")
+
+	if not self.headers["cache-control"] then
+		self:header("Cache-Control", "no-cache")
+		self:header("Expires", "0")
+	end
+
+	push_headers(self)
+	self.output:write(content)
+end
+
+function Http:redirect(url)
+	self:status(302, "Found")
+	self:header("Location", url)
+	self:close()
+end

+ 268 - 0
package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua

@@ -0,0 +1,268 @@
+-- Copyright 2008 Freifunk Leipzig / Jo-Philipp Wich <jow@openwrt.org>
+-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
+-- Licensed to the public under the Apache License 2.0.
+
+-- This class contains several functions useful for http message- and content
+-- decoding and to retrive form data from raw http messages.
+module("gluon.web.http.protocol", package.seeall)
+
+
+HTTP_MAX_CONTENT      = 1024*8		-- 8 kB maximum content size
+
+
+local function pump(src, snk)
+	while true do
+		local chunk, src_err = src()
+		local ret, snk_err = snk(chunk, src_err)
+
+		if not (chunk and ret) then
+			local err = src_err or snk_err
+			if err then
+				return nil, err
+			else
+				return true
+			end
+		end
+	end
+end
+
+function urlencode(s)
+	return (string.gsub(s, '[^a-zA-Z0-9%-_%.~]',
+		function(c)
+			local ret = ''
+
+			for i = 1, string.len(c) do
+				ret = ret .. string.format('%%%02X', string.byte(c, i, i))
+			end
+
+			return ret
+		end
+	))
+end
+
+-- the "+" sign to " " - and return the decoded string.
+function urldecode(str, no_plus)
+
+	local function chrdec(hex)
+		return string.char(tonumber(hex, 16))
+	end
+
+	if type(str) == "string" then
+		if not no_plus then
+			str = str:gsub("+", " ")
+		end
+
+		str = str:gsub("%%(%x%x)", chrdec)
+	end
+
+	return str
+end
+
+local function initval(tbl, key)
+	if not tbl[key] then
+		tbl[key] = {}
+	end
+
+	table.insert(tbl[key], "")
+end
+
+local function appendval(tbl, key, chunk)
+	local t = tbl[key]
+	t[#t] = t[#t] .. chunk
+end
+
+-- from given url or string. Returns a table with urldecoded values.
+-- Simple parameters are stored as string values associated with the parameter
+-- name within the table. Parameters with multiple values are stored as array
+-- containing the corresponding values.
+function urldecode_params(url)
+	local params = {}
+
+	if url:find("?") then
+		url = url:gsub("^.+%?([^?]+)", "%1")
+	end
+
+	for pair in url:gmatch("[^&;]+") do
+
+		-- find key and value
+		local key = urldecode(pair:match("^([^=]+)"))
+		local val = urldecode(pair:match("^[^=]+=(.+)$"))
+
+		-- store
+		if key and key:len() > 0 then
+			initval(params, key)
+			if val then
+				appendval(params, key, val)
+			end
+		end
+	end
+
+	return params
+end
+
+-- Content-Type. Stores all extracted data associated with its parameter name
+-- in the params table withing the given message object. Multiple parameter
+-- values are stored as tables, ordinary ones as strings.
+-- If an optional file callback function is given then it is feeded with the
+-- file contents chunk by chunk and only the extracted file name is stored
+-- within the params table. The callback function will be called subsequently
+-- with three arguments:
+--  o Table containing decoded (name, file) and raw (headers) mime header data
+--  o String value containing a chunk of the file data
+--  o Boolean which indicates wheather the current chunk is the last one (eof)
+function mimedecode_message_body(src, msg, filecb)
+
+	if msg and msg.env.CONTENT_TYPE then
+		msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
+	end
+
+	if not msg.mime_boundary then
+		return nil, "Invalid Content-Type found"
+	end
+
+
+	local tlen   = 0
+	local inhdr  = false
+	local field  = nil
+	local store  = nil
+	local lchunk = nil
+
+	local function parse_headers(chunk, field)
+		local stat
+		repeat
+			chunk, stat = chunk:gsub(
+				"^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
+				function(k,v)
+					field.headers[k] = v
+					return ""
+				end
+			)
+		until stat == 0
+
+		chunk, stat = chunk:gsub("^\r\n","")
+
+		-- End of headers
+		if stat > 0 then
+			if field.headers["Content-Disposition"] then
+				if field.headers["Content-Disposition"]:match("^form%-data; ") then
+					field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
+					field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$')
+				end
+			end
+
+			if not field.headers["Content-Type"] then
+				field.headers["Content-Type"] = "text/plain"
+			end
+
+
+			if field.name then
+				initval(msg.params, field.name)
+				if field.file then
+					appendval(msg.params, field.name, field.file)
+					store = filecb
+				else
+					store = function(hdr, buf, eof)
+						appendval(msg.params, field.name, buf)
+					end
+				end
+			else
+				store = nil
+			end
+
+			return chunk, true
+		end
+
+		return chunk, false
+	end
+
+	local function snk(chunk)
+
+		tlen = tlen + (chunk and #chunk or 0)
+
+		if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then
+			return nil, "Message body size exceeds Content-Length"
+		end
+
+		if chunk and not lchunk then
+			lchunk = "\r\n" .. chunk
+
+		elseif lchunk then
+			local data = lchunk .. (chunk or "")
+			local spos, epos, found
+
+			repeat
+				spos, epos = data:find("\r\n--" .. msg.mime_boundary .. "\r\n", 1, true)
+
+				if not spos then
+					spos, epos = data:find("\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true)
+				end
+
+
+				if spos then
+					local predata = data:sub(1, spos - 1)
+
+					if inhdr then
+						predata, eof = parse_headers(predata, field)
+
+						if not eof then
+							return nil, "Invalid MIME section header"
+						elseif not field.name then
+							return nil, "Invalid Content-Disposition header"
+						end
+					end
+
+					if store then
+						store(field, predata, true)
+					end
+
+
+					field = { headers = { } }
+					found = true
+
+					data, eof = parse_headers(data:sub(epos + 1, #data), field)
+					inhdr = not eof
+				end
+			until not spos
+
+			if found then
+				-- We found at least some boundary. Save
+				-- the unparsed remaining data for the
+				-- next chunk.
+				lchunk, data = data, nil
+			else
+				-- There was a complete chunk without a boundary. Parse it as headers or
+				-- append it as data, depending on our current state.
+				if inhdr then
+					lchunk, eof = parse_headers(data, field)
+					inhdr = not eof
+				else
+					-- We're inside data, so append the data. Note that we only append
+					-- lchunk, not all of data, since there is a chance that chunk
+					-- contains half a boundary. Assuming that each chunk is at least the
+					-- boundary in size, this should prevent problems
+					if store then
+						store(field, lchunk, false)
+					end
+					lchunk, chunk = chunk, nil
+				end
+			end
+		end
+
+		return true
+	end
+
+	return pump(src, snk)
+end
+
+-- This function will examine the Content-Type within the given message object
+-- to select the appropriate content decoder.
+-- Currently only the multipart/form-data mime type is supported.
+function parse_message_body(src, msg, filecb)
+	if not (msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE) then
+		return
+	end
+
+	if msg.env.CONTENT_TYPE:match("^multipart/form%-data") then
+		return mimedecode_message_body(src, msg, filecb)
+	end
+end

+ 466 - 0
package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua

@@ -0,0 +1,466 @@
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
+-- Licensed to the public under the Apache License 2.0.
+
+module("gluon.web.model", package.seeall)
+
+local util = require("gluon.web.util")
+
+local fs         = require("nixio.fs")
+local datatypes  = require("gluon.web.model.datatypes")
+local dispatcher = require("gluon.web.dispatcher")
+local class      = util.class
+local instanceof = util.instanceof
+
+FORM_NODATA  =  0
+FORM_VALID   =  1
+FORM_INVALID = -1
+
+-- Loads a model from given file, creating an environment and returns it
+function load(name, renderer)
+	local modeldir = util.libpath() .. "/model/"
+
+	if not fs.access(modeldir..name..".lua") then
+		error("Model '" .. name .. "' not found!")
+	end
+
+	local func = assert(loadfile(modeldir..name..".lua"))
+
+	local env = {
+		translate=renderer.translate,
+		translatef=renderer.translatef,
+	}
+
+	setfenv(func, setmetatable(env, {__index =
+		function(tbl, key)
+			return _M[key] or _G[key]
+		end
+	}))
+
+	local models = { func() }
+
+	for k, model in ipairs(models) do
+		if not instanceof(model, Node) then
+			error("model definition returned an invalid model object")
+		end
+		model.index = k
+	end
+
+	return models
+end
+
+
+local function parse_datatype(code)
+	local match, arg, arg2
+
+	match, arg, arg2 = code:match('^([^%(]+)%(([^,]+),([^%)]+)%)$')
+	if match then
+		return datatypes[match], {arg, arg2}
+	end
+
+	match, arg = code:match('^([^%(]+)%(([^%)]+)%)$')
+	if match then
+		return datatypes[match], {arg}
+	end
+
+	return datatypes[code], {}
+end
+
+local function verify_datatype(dt, value)
+	if dt then
+		local c, args = parse_datatype(dt)
+		assert(c, "Invalid datatype")
+		return c(value, unpack(args))
+	end
+	return true
+end
+
+
+Node = class()
+
+function Node:__init__(title, description, name)
+	self.children = {}
+	self.title = title or ""
+	self.description = description or ""
+	self.name = name
+	self.index = nil
+	self.parent = nil
+end
+
+function Node:append(obj)
+	table.insert(self.children, obj)
+	obj.index = #self.children
+	obj.parent = self
+end
+
+function Node:id_suffix()
+	return self.name or (self.index and tostring(self.index)) or '_'
+end
+
+function Node:id()
+	local prefix = self.parent and self.parent:id() or "id"
+
+	return prefix.."."..self:id_suffix()
+end
+
+function Node:parse(http)
+	for _, child in ipairs(self.children) do
+		child:parse(http)
+	end
+end
+
+function Node:render(renderer, scope)
+	if self.template then
+		local env = setmetatable({
+			self  = self,
+			id  = self:id(),
+			scope = scope,
+		}, {__index = scope})
+		renderer.render(self.template, env)
+	end
+end
+
+function Node:render_children(renderer, scope)
+	for _, node in ipairs(self.children) do
+		node:render(renderer, scope)
+	end
+end
+
+function Node:resolve_depends()
+	local updated = false
+	for _, node in ipairs(self.children) do
+		update = updated or node:resolve_depends()
+	end
+	return updated
+end
+
+function Node:handle()
+	for _, node in ipairs(self.children) do
+		node:handle()
+	end
+end
+
+
+Template = class(Node)
+
+function Template:__init__(template)
+	Node.__init__(self)
+	self.template = template
+end
+
+
+Form = class(Node)
+
+function Form:__init__(...)
+	Node.__init__(self, ...)
+	self.template = "model/form"
+end
+
+function Form:submitstate(http)
+	return http:getenv("REQUEST_METHOD") == "POST" and http:formvalue(self:id()) ~= nil
+end
+
+function Form:parse(http)
+	if not self:submitstate(http) then
+		self.state = FORM_NODATA
+		return
+	end
+
+	Node.parse(self, http)
+
+	while self:resolve_depends() do end
+
+	for _, s in ipairs(self.children) do
+		for _, v in ipairs(s.children) do
+			if v.state == FORM_INVALID then
+				self.state = FORM_INVALID
+				return
+			end
+		end
+	end
+
+	self.state = FORM_VALID
+end
+
+function Form:handle()
+	if self.state == FORM_VALID then
+		Node.handle(self)
+		self:write()
+	end
+end
+
+function Form:write()
+end
+
+function Form:section(t, ...)
+	assert(instanceof(t, Section), "class must be a descendent of Section")
+
+	local obj  = t(...)
+	self:append(obj)
+	return obj
+end
+
+
+Section = class(Node)
+
+function Section:__init__(...)
+	Node.__init__(self, ...)
+	self.fields = {}
+	self.template = "model/section"
+end
+
+function Section:option(t, option, title, description, ...)
+	assert(instanceof(t, AbstractValue), "class must be a descendant of AbstractValue")
+
+	local obj  = t(title, description, option, ...)
+	self:append(obj)
+	self.fields[option] = obj
+	return obj
+end
+
+
+AbstractValue = class(Node)
+
+function AbstractValue:__init__(option, ...)
+	Node.__init__(self, option, ...)
+	self.deps = {}
+
+	self.default   = nil
+	self.size      = nil
+	self.optional  = false
+
+	self.template  = "model/valuewrapper"
+
+	self.state = FORM_NODATA
+end
+
+function AbstractValue:depends(field, value)
+	local deps
+	if instanceof(field, Node) then
+		deps = { [field] = value }
+	else
+		deps = field
+	end
+
+	table.insert(self.deps, deps)
+end
+
+function AbstractValue:deplist(section, deplist)
+	local deps = {}
+
+	for _, d in ipairs(deplist or self.deps) do
+		local a = {}
+		for k, v in pairs(d) do
+			a[k:id()] = v
+		end
+		table.insert(deps, a)
+	end
+
+	if next(deps) then
+		return deps
+	end
+end
+
+function AbstractValue:defaultvalue()
+	return self.default
+end
+
+function AbstractValue:formvalue(http)
+	return http:formvalue(self:id())
+end
+
+function AbstractValue:cfgvalue()
+	if self.state == FORM_NODATA then
+		return self:defaultvalue()
+	else
+		return self.data
+	end
+end
+
+function AbstractValue:add_error(type, msg)
+	self.error = msg or type
+
+	if type == "invalid" then
+		self.tag_invalid = true
+	elseif type == "missing" then
+		self.tag_missing = true
+	end
+
+	self.state = FORM_INVALID
+end
+
+function AbstractValue:reset()
+	self.error = nil
+	self.tag_invalid = nil
+	self.tag_missing = nil
+	self.data = nil
+	self.state = FORM_NODATA
+
+end
+
+function AbstractValue:parse(http)
+	self.data = self:formvalue(http)
+
+	local ok, err = self:validate()
+	if not ok then
+		if type(self.data) ~= "string" or #self.data > 0 then
+			self:add_error("invalid", err)
+		else
+			self:add_error("missing", err)
+		end
+		return
+	end
+
+	self.state = FORM_VALID
+end
+
+function AbstractValue:resolve_depends()
+	if self.state == FORM_NODATA or #self.deps == 0 then
+		return false
+	end
+
+	for _, d in ipairs(self.deps) do
+		local valid = true
+		for k, v in pairs(d) do
+			if k.state ~= FORM_VALID or k.data ~= v then
+				valid = false
+				break
+			end
+		end
+		if valid then return false end
+	end
+
+	self:reset()
+	return true
+end
+
+function AbstractValue:validate()
+	if self.data and verify_datatype(self.datatype, self.data) then
+		return true
+	end
+
+	if type(self.data) == "string" and #self.data == 0 then
+		self.data = nil
+	end
+
+	if self.data == nil then
+		return self.optional
+	end
+
+	return false
+
+end
+
+function AbstractValue:handle()
+	if self.state == FORM_VALID then
+		self:write(self.data)
+	end
+end
+
+function AbstractValue:write(value)
+end
+
+
+Value = class(AbstractValue)
+
+function Value:__init__(...)
+	AbstractValue.__init__(self, ...)
+	self.subtemplate  = "model/value"
+	self.keylist = {}
+	self.vallist = {}
+end
+
+
+Flag = class(AbstractValue)
+
+function Flag:__init__(...)
+	AbstractValue.__init__(self, ...)
+	self.subtemplate  = "model/fvalue"
+
+	self.default = false
+end
+
+function Flag:formvalue(http)
+	return http:formvalue(self:id()) ~= nil
+end
+
+function Flag:validate()
+	return true
+end
+
+
+ListValue = class(AbstractValue)
+
+function ListValue:__init__(...)
+	AbstractValue.__init__(self, ...)
+	self.subtemplate  = "model/lvalue"
+
+	self.size = 1
+	self.widget = "select"
+
+	self.keylist = {}
+	self.vallist = {}
+	self.valdeps = {}
+end
+
+function ListValue:value(key, val, ...)
+	if util.contains(self.keylist, key) then
+		return
+	end
+
+	val = val or key
+	table.insert(self.keylist, tostring(key))
+	table.insert(self.vallist, tostring(val))
+	table.insert(self.valdeps, {...})
+end
+
+function ListValue:validate()
+	return util.contains(self.keylist, self.data)
+end
+
+
+DynamicList = class(AbstractValue)
+
+function DynamicList:__init__(...)
+	AbstractValue.__init__(self, ...)
+	self.subtemplate  = "model/dynlist"
+end
+
+function DynamicList:defaultvalue()
+	local value = self.default
+
+	if type(value) == "table" then
+		return value
+	else
+		return { value }
+	end
+end
+
+function DynamicList:formvalue(http)
+	return http:formvaluetable(self:id())
+end
+
+function DynamicList:validate()
+	if self.data == nil then
+		self.data = {}
+	end
+
+	if #self.data == 0 then
+		return self.optional
+	end
+
+	for _, v in ipairs(self.data) do
+		if not verify_datatype(self.datatype, v) then
+			return false
+		end
+	end
+	return true
+end
+
+
+TextValue = class(AbstractValue)
+
+function TextValue:__init__(...)
+	AbstractValue.__init__(self, ...)
+	self.subtemplate  = "model/tvalue"
+end

+ 167 - 0
package/gluon-web/luasrc/usr/lib/lua/gluon/web/model/datatypes.lua

@@ -0,0 +1,167 @@
+-- Copyright 2010 Jo-Philipp Wich <jow@openwrt.org>
+-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
+-- Licensed to the public under the Apache License 2.0.
+
+local tonumber = tonumber
+
+
+module "gluon.web.model.datatypes"
+
+
+function bool(val)
+	if val == "1" or val == "yes" or val == "on" or val == "true" then
+		return true
+	elseif val == "0" or val == "no" or val == "off" or val == "false" then
+		return true
+	elseif val == "" or val == nil then
+		return true
+	end
+
+	return false
+end
+
+local function dec(val)
+	if val:match('^%-?%d*%.?%d+$') then
+		return tonumber(val)
+	end
+end
+
+local function int(val)
+	if val:match('^%-?%d+$') then
+		return tonumber(val)
+	end
+end
+
+function uinteger(val)
+	local n = int(val)
+	return (n ~= nil and n >= 0)
+end
+
+function integer(val)
+	return (int(val) ~= nil)
+end
+
+function ufloat(val)
+	local n = dec(val)
+	return (n ~= nil and n >= 0)
+end
+
+function float(val)
+	return (dec(val) ~= nil)
+end
+
+function ipaddr(val)
+	return ip4addr(val) or ip6addr(val)
+end
+
+function ip4addr(val)
+	local g = '(%d%d?%d?)'
+	local v1, v2, v3, v4 = val:match('^'..((g..'%.'):rep(3))..g..'$')
+	local n1, n2, n3, n4 = tonumber(v1), tonumber(v2), tonumber(v3), tonumber(v4)
+
+	if not (n1 and n2 and n3 and n4) then return false end
+
+	return (
+		(n1 >= 0) and (n1 <= 255) and
+		(n2 >= 0) and (n2 <= 255) and
+		(n3 >= 0) and (n3 <= 255) and
+		(n4 >= 0) and (n4 <= 255)
+	)
+end
+
+function ip6addr(val)
+	local g1 = '%x%x?%x?%x?'
+
+	if not val:match('::') then
+		return val:match('^'..((g1..':'):rep(7))..g1..'$') ~= nil
+	end
+
+	if
+		val:match(':::') or val:match('::.+::') or
+		val:match('^:[^:]') or val:match('[^:]:$')
+	then
+		return false
+	end
+
+	local g0 = '%x?%x?%x?%x?'
+	for i = 2, 7 do
+		if val:match('^'..((g0..':'):rep(i))..g0..'$') then
+			return true
+		end
+	end
+
+	if val:match('^'..((g1..':'):rep(7))..':$') then
+		return true
+	end
+	if val:match('^:'..((':'..g1):rep(7))..'$') then
+		return true
+	end
+
+	return false
+end
+
+function wpakey(val)
+	if #val == 64 then
+		return (val:match("^%x+$") ~= nil)
+	else
+		return (#val >= 8) and (#val <= 63)
+	end
+end
+
+function range(val, vmin, vmax)
+	return min(val, vmin) and max(val, vmax)
+end
+
+function min(val, min)
+	val = dec(val)
+	min = tonumber(min)
+
+	if val ~= nil and min ~= nil then
+		return (val >= min)
+	end
+
+	return false
+end
+
+function max(val, max)
+	val = dec(val)
+	max = tonumber(max)
+
+	if val ~= nil and max ~= nil then
+		return (val <= max)
+	end
+
+	return false
+end
+
+function irange(val, vmin, vmax)
+	return integer(val) and range(val, vmin, vmax)
+end
+
+function imin(val, vmin)
+	return integer(val) and min(val, vmin)
+end
+
+function imax(val, vmax)
+	return integer(val) and max(val, vmax)
+end
+
+function minlength(val, min)
+	min = tonumber(min)
+
+	if min ~= nil then
+		return (#val >= min)
+	end
+
+	return false
+end
+
+function maxlength(val, max)
+	max = tonumber(max)
+
+	if max ~= nil then
+		return (#val <= max)
+	end
+
+	return false
+end

+ 92 - 0
package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua

@@ -0,0 +1,92 @@
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
+-- Licensed to the public under the Apache License 2.0.
+
+local tparser = require "gluon.web.template.parser"
+local util = require "gluon.web.util"
+local fs = require "nixio.fs"
+
+local tostring, setmetatable, setfenv, pcall, assert = tostring, setmetatable, setfenv, pcall, assert
+
+
+module "gluon.web.template"
+
+local viewdir = util.libpath() .. "/view/"
+local i18ndir = util.libpath() .. "/i18n/"
+
+function renderer(env)
+	local ctx = {}
+
+
+	local function render_template(name, template, scope)
+		scope = scope or {}
+
+		local locals = {
+			renderer = ctx,
+			translate = ctx.translate,
+			translatef = ctx.translatef,
+			include = function(name)
+				ctx.render(name, scope)
+			end,
+		}
+
+		setfenv(template, setmetatable({}, {
+			__index = function(tbl, key)
+				return scope[key] or env[key] or locals[key]
+			end
+		}))
+
+		-- Now finally render the thing
+		local stat, err = pcall(template)
+		assert(stat, "Failed to execute template '" .. name .. "'.\n" ..
+			      "A runtime error occured: " .. tostring(err or "(nil)"))
+	end
+
+	--- Render a certain template.
+	-- @param name		Template name
+	-- @param scope		Scope to assign to template (optional)
+	function ctx.render(name, scope)
+		local sourcefile = viewdir .. name .. ".html"
+		local template, _, err = tparser.parse(sourcefile)
+
+		assert(template, "Failed to load template '" .. name .. "'.\n" ..
+			"Error while parsing template '" .. sourcefile .. "':\n" ..
+			(err or "Unknown syntax error"))
+
+		render_template(name, template, scope)
+	end
+
+	--- Render a template from a string.
+	-- @param template	Template string
+	-- @param scope		Scope to assign to template (optional)
+	function ctx.render_string(str, scope)
+		local template, _, err = tparser.parse_string(str)
+
+		assert(template, "Error while parsing template:\n" ..
+			(err or "Unknown syntax error"))
+
+		render_template('(local)', template, scope)
+	end
+
+	function ctx.setlanguage(lang)
+		lang = lang:gsub("_", "-")
+		if not lang then return false end
+
+		if lang ~= 'en' and not fs.access(i18ndir .. "gluon-web." .. lang .. ".lmo") then
+			return false
+		end
+
+		return tparser.load_catalog(lang, i18ndir)
+	end
+
+	function ctx.translate(key)
+		return tparser.translate(key) or key
+	end
+
+	function ctx.translatef(key, ...)
+		local t = ctx.translate(key)
+		return t and t:format(...)
+	end
+
+	return ctx
+end

+ 100 - 0
package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua

@@ -0,0 +1,100 @@
+-- Copyright 2008 Steven Barth <steven@midlink.org>
+-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
+-- Licensed to the public under the Apache License 2.0.
+
+local io = require "io"
+local table = require "table"
+local tparser = require "gluon.web.template.parser"
+local json = require "luci.jsonc"
+local nixio = require "nixio"
+local fs = require "nixio.fs"
+
+local getmetatable, setmetatable = getmetatable, setmetatable
+local tostring, pairs = tostring, pairs
+
+module "gluon.web.util"
+
+--
+-- Class helper routines
+--
+
+-- Instantiates a class
+local function _instantiate(class, ...)
+	local inst = setmetatable({}, {__index = class})
+
+	if inst.__init__ then
+		inst:__init__(...)
+	end
+
+	return inst
+end
+
+-- The class object can be instantiated by calling itself.
+-- Any class functions or shared parameters can be attached to this object.
+-- Attaching a table to the class object makes this table shared between
+-- all instances of this class. For object parameters use the __init__ function.
+-- Classes can inherit member functions and values from a base class.
+-- Class can be instantiated by calling them. All parameters will be passed
+-- to the __init__ function of this class - if such a function exists.
+-- The __init__ function must be used to set any object parameters that are not shared
+-- with other objects of this class. Any return values will be ignored.
+function class(base)
+	return setmetatable({}, {
+		__call  = _instantiate,
+		__index = base
+	})
+end
+
+function instanceof(object, class)
+	while object do
+		if object == class then
+			return true
+		end
+		local mt = getmetatable(object)
+		object = mt and mt.__index
+	end
+	return false
+end
+
+
+--
+-- String and data manipulation routines
+--
+
+function pcdata(value)
+	return value and tparser.pcdata(tostring(value))
+end
+
+
+function contains(table, value)
+	for k, v in pairs(table) do
+		if value == v then
+			return k
+		end
+	end
+	return false
+end
+
+
+--
+-- System utility functions
+--
+
+function exec(command)
+	local pp   = io.popen(command)
+	local data = pp:read("*a")
+	pp:close()
+
+	return data
+end
+
+function uniqueid(bytes)
+	local rand = fs.readfile("/dev/urandom", bytes)
+	return nixio.bin.hexlify(rand)
+end
+
+serialize_json = json.stringify
+
+function libpath()
+	return '/lib/gluon/web'
+end

+ 16 - 0
package/gluon-web/src/Makefile

@@ -0,0 +1,16 @@
+all: compile
+
+%.o: %.c
+	$(CC) $(CPPFLAGS) $(CFLAGS) -fPIC -c -o $@ $<
+
+clean:
+	rm -f parser.so *.o
+
+parser.so: template_parser.o template_utils.o template_lmo.o template_lualib.o
+	$(CC) $(LDFLAGS) -shared -o $@ $^
+
+compile: parser.so
+
+install: compile
+	mkdir -p $(DESTDIR)/usr/lib/lua/gluon/web/template
+	cp parser.so $(DESTDIR)/usr/lib/lua/gluon/web/template/parser.so

+ 288 - 0
package/gluon-web/src/template_lmo.c

@@ -0,0 +1,288 @@
+/*
+ * lmo - Lua Machine Objects - Base functions
+ *
+ *   Copyright (C) 2009-2010 Jo-Philipp Wich <jow@openwrt.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
+ *
+ *  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.
+ */
+
+#include "template_lmo.h"
+
+/*
+ * Hash function from http://www.azillionmonkeys.com/qed/hash.html
+ * Copyright (C) 2004-2008 by Paul Hsieh
+ */
+
+static uint32_t sfh_hash(const char *data, int len)
+{
+	uint32_t hash = len, tmp;
+	int rem;
+
+	if (len <= 0 || data == NULL) return 0;
+
+	rem = len & 3;
+	len >>= 2;
+
+	/* Main loop */
+	for (;len > 0; len--) {
+		hash  += sfh_get16(data);
+		tmp    = (sfh_get16(data+2) << 11) ^ hash;
+		hash   = (hash << 16) ^ tmp;
+		data  += 2*sizeof(uint16_t);
+		hash  += hash >> 11;
+	}
+
+	/* Handle end cases */
+	switch (rem) {
+		case 3: hash += sfh_get16(data);
+			hash ^= hash << 16;
+			hash ^= data[sizeof(uint16_t)] << 18;
+			hash += hash >> 11;
+			break;
+		case 2: hash += sfh_get16(data);
+			hash ^= hash << 11;
+			hash += hash >> 17;
+			break;
+		case 1: hash += *data;
+			hash ^= hash << 10;
+			hash += hash >> 1;
+	}
+
+	/* Force "avalanching" of final 127 bits */
+	hash ^= hash << 3;
+	hash += hash >> 5;
+	hash ^= hash << 4;
+	hash += hash >> 17;
+	hash ^= hash << 25;
+	hash += hash >> 6;
+
+	return hash;
+}
+
+static uint32_t lmo_canon_hash(const char *str, int len)
+{
+	char res[4096];
+	char *ptr, prev;
+	int off;
+
+	if (!str || len >= sizeof(res))
+		return 0;
+
+	for (prev = ' ', ptr = res, off = 0; off < len; prev = *str, off++, str++)
+	{
+		if (isspace(*str))
+		{
+			if (!isspace(prev))
+				*ptr++ = ' ';
+		}
+		else
+		{
+			*ptr++ = *str;
+		}
+	}
+
+	if ((ptr > res) && isspace(*(ptr-1)))
+		ptr--;
+
+	return sfh_hash(res, ptr - res);
+}
+
+static lmo_archive_t * lmo_open(const char *file)
+{
+	int in = -1;
+	uint32_t idx_offset = 0;
+	struct stat s;
+
+	lmo_archive_t *ar = NULL;
+
+	if (stat(file, &s) == -1)
+		goto err;
+
+	if ((in = open(file, O_RDONLY)) == -1)
+		goto err;
+
+	if ((ar = (lmo_archive_t *)malloc(sizeof(*ar))) != NULL)
+	{
+		memset(ar, 0, sizeof(*ar));
+
+		ar->fd     = in;
+		ar->size = s.st_size;
+
+		fcntl(ar->fd, F_SETFD, fcntl(ar->fd, F_GETFD) | FD_CLOEXEC);
+
+		if ((ar->mmap = mmap(NULL, ar->size, PROT_READ, MAP_SHARED, ar->fd, 0)) == MAP_FAILED)
+			goto err;
+
+		idx_offset = ntohl(*((const uint32_t *)
+		                     (ar->mmap + ar->size - sizeof(uint32_t))));
+
+		if (idx_offset >= ar->size)
+			goto err;
+
+		ar->index  = (lmo_entry_t *)(ar->mmap + idx_offset);
+		ar->length = (ar->size - idx_offset - sizeof(uint32_t)) / sizeof(lmo_entry_t);
+		ar->end    = ar->mmap + ar->size;
+
+		return ar;
+	}
+
+err:
+	if (in > -1)
+		close(in);
+
+	if (ar != NULL)
+	{
+		if ((ar->mmap != NULL) && (ar->mmap != MAP_FAILED))
+			munmap(ar->mmap, ar->size);
+
+		free(ar);
+	}
+
+	return NULL;
+}
+
+
+static lmo_catalog_t *_lmo_catalogs;
+static lmo_catalog_t *_lmo_active_catalog;
+
+int lmo_load_catalog(const char *lang, const char *dir)
+{
+	DIR *dh = NULL;
+	char pattern[16];
+	char path[PATH_MAX];
+	struct dirent *de = NULL;
+
+	lmo_archive_t *ar = NULL;
+	lmo_catalog_t *cat = NULL;
+
+	if (!lmo_change_catalog(lang))
+		return 0;
+
+	if (!dir || !(dh = opendir(dir)))
+		goto err;
+
+	if (!(cat = malloc(sizeof(*cat))))
+		goto err;
+
+	memset(cat, 0, sizeof(*cat));
+
+	snprintf(cat->lang, sizeof(cat->lang), "%s", lang);
+	snprintf(pattern, sizeof(pattern), "*.%s.lmo", lang);
+
+	while ((de = readdir(dh)) != NULL)
+	{
+		if (!fnmatch(pattern, de->d_name, 0))
+		{
+			snprintf(path, sizeof(path), "%s/%s", dir, de->d_name);
+			ar = lmo_open(path);
+
+			if (ar)
+			{
+				ar->next = cat->archives;
+				cat->archives = ar;
+			}
+		}
+	}
+
+	closedir(dh);
+
+	cat->next = _lmo_catalogs;
+	_lmo_catalogs = cat;
+
+	if (!_lmo_active_catalog)
+		_lmo_active_catalog = cat;
+
+	return 0;
+
+err:
+	if (dh) closedir(dh);
+	if (cat) free(cat);
+
+	return -1;
+}
+
+int lmo_change_catalog(const char *lang)
+{
+	lmo_catalog_t *cat;
+
+	for (cat = _lmo_catalogs; cat; cat = cat->next)
+	{
+		if (!strncmp(cat->lang, lang, sizeof(cat->lang)))
+		{
+			_lmo_active_catalog = cat;
+			return 0;
+		}
+	}
+
+	return -1;
+}
+
+static lmo_entry_t * lmo_find_entry(lmo_archive_t *ar, uint32_t hash)
+{
+	unsigned int m, l, r;
+	uint32_t k;
+
+	l = 0;
+	r = ar->length - 1;
+
+	while (1)
+	{
+		m = l + ((r - l) / 2);
+
+		if (r < l)
+			break;
+
+		k = ntohl(ar->index[m].key_id);
+
+		if (k == hash)
+			return &ar->index[m];
+
+		if (k > hash)
+		{
+			if (!m)
+				break;
+
+			r = m - 1;
+		}
+		else
+		{
+			l = m + 1;
+		}
+	}
+
+	return NULL;
+}
+
+int lmo_translate(const char *key, int keylen, char **out, int *outlen)
+{
+	uint32_t hash;
+	lmo_entry_t *e;
+	lmo_archive_t *ar;
+
+	if (!key || !_lmo_active_catalog)
+		return -2;
+
+	hash = lmo_canon_hash(key, keylen);
+
+	for (ar = _lmo_active_catalog->archives; ar; ar = ar->next)
+	{
+		if ((e = lmo_find_entry(ar, hash)) != NULL)
+		{
+			*out = ar->mmap + ntohl(e->offset);
+			*outlen = ntohl(e->length);
+			return 0;
+		}
+	}
+
+	return -1;
+}

+ 81 - 0
package/gluon-web/src/template_lmo.h

@@ -0,0 +1,81 @@
+/*
+ * lmo - Lua Machine Objects - General header
+ *
+ *   Copyright (C) 2009-2012 Jo-Philipp Wich <jow@openwrt.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
+ *
+ *  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.
+ */
+
+#ifndef _TEMPLATE_LMO_H_
+#define _TEMPLATE_LMO_H_
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <string.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <arpa/inet.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fnmatch.h>
+#include <dirent.h>
+#include <ctype.h>
+#include <limits.h>
+
+#if (defined(__GNUC__) && defined(__i386__))
+#define sfh_get16(d) (*((const uint16_t *) (d)))
+#else
+#define sfh_get16(d) ((((uint32_t)(((const uint8_t *)(d))[1])) << 8)\
+					   +(uint32_t)(((const uint8_t *)(d))[0]) )
+#endif
+
+
+struct lmo_entry {
+	uint32_t key_id;
+	uint32_t val_id;
+	uint32_t offset;
+	uint32_t length;
+} __attribute__((packed));
+
+typedef struct lmo_entry lmo_entry_t;
+
+
+struct lmo_archive {
+	int         fd;
+	int	        length;
+	uint32_t    size;
+	lmo_entry_t *index;
+	char        *mmap;
+	char		*end;
+	struct lmo_archive *next;
+};
+
+typedef struct lmo_archive lmo_archive_t;
+
+
+struct lmo_catalog {
+	char lang[6];
+	struct lmo_archive *archives;
+	struct lmo_catalog *next;
+};
+
+typedef struct lmo_catalog lmo_catalog_t;
+
+
+int lmo_load_catalog(const char *lang, const char *dir);
+int lmo_change_catalog(const char *lang);
+int lmo_translate(const char *key, int keylen, char **out, int *outlen);
+
+#endif

+ 121 - 0
package/gluon-web/src/template_lualib.c

@@ -0,0 +1,121 @@
+/*
+ * LuCI Template - Lua binding
+ *
+ *   Copyright (C) 2009 Jo-Philipp Wich <jow@openwrt.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
+ *
+ *  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.
+ */
+
+#include "template_lualib.h"
+
+static int template_L_do_parse(lua_State *L, struct template_parser *parser, const char *chunkname)
+{
+	int lua_status, rv;
+
+	if (!parser)
+	{
+		lua_pushnil(L);
+		lua_pushinteger(L, errno);
+		lua_pushstring(L, strerror(errno));
+		return 3;
+	}
+
+	lua_status = lua_load(L, template_reader, parser, chunkname);
+
+	if (lua_status == 0)
+		rv = 1;
+	else
+		rv = template_error(L, parser);
+
+	template_close(parser);
+
+	return rv;
+}
+
+static int template_L_parse(lua_State *L)
+{
+	const char *file = luaL_checkstring(L, 1);
+	struct template_parser *parser = template_open(file);
+
+	return template_L_do_parse(L, parser, file);
+}
+
+static int template_L_parse_string(lua_State *L)
+{
+	size_t len;
+	const char *str = luaL_checklstring(L, 1, &len);
+	struct template_parser *parser = template_string(str, len);
+
+	return template_L_do_parse(L, parser, "[string]");
+}
+
+static int template_L_pcdata(lua_State *L)
+{
+	size_t len = 0;
+	const char *str = luaL_checklstring(L, 1, &len);
+	char *res = pcdata(str, len);
+
+	if (res != NULL)
+	{
+		lua_pushstring(L, res);
+		free(res);
+
+		return 1;
+	}
+
+	return 0;
+}
+
+static int template_L_load_catalog(lua_State *L) {
+	const char *lang = luaL_optstring(L, 1, "en");
+	const char *dir  = luaL_optstring(L, 2, NULL);
+	lua_pushboolean(L, !lmo_load_catalog(lang, dir));
+	return 1;
+}
+
+static int template_L_translate(lua_State *L) {
+	size_t len;
+	char *tr;
+	int trlen;
+	const char *key = luaL_checklstring(L, 1, &len);
+
+	switch (lmo_translate(key, len, &tr, &trlen))
+	{
+		case 0:
+			lua_pushlstring(L, tr, trlen);
+			return 1;
+
+		case -1:
+			return 0;
+	}
+
+	lua_pushnil(L);
+	lua_pushstring(L, "no catalog loaded");
+	return 2;
+}
+
+
+/* module table */
+static const luaL_reg R[] = {
+	{ "parse",          template_L_parse },
+	{ "parse_string",   template_L_parse_string },
+	{ "pcdata",         template_L_pcdata },
+	{ "load_catalog",   template_L_load_catalog },
+	{ "translate",      template_L_translate },
+	{}
+};
+
+LUALIB_API int luaopen_gluon_web_template_parser(lua_State *L) {
+	luaL_register(L, TEMPLATE_LUALIB_META, R);
+	return 1;
+}

+ 30 - 0
package/gluon-web/src/template_lualib.h

@@ -0,0 +1,30 @@
+/*
+ * LuCI Template - Lua library header
+ *
+ *   Copyright (C) 2009 Jo-Philipp Wich <jow@openwrt.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
+ *
+ *  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.
+ */
+
+#ifndef _TEMPLATE_LUALIB_H_
+#define _TEMPLATE_LUALIB_H_
+
+#include "template_parser.h"
+#include "template_utils.h"
+#include "template_lmo.h"
+
+#define TEMPLATE_LUALIB_META  "gluon.web.template.parser"
+
+LUALIB_API int luaopen_gluon_web_template_parser(lua_State *L);
+
+#endif

+ 419 - 0
package/gluon-web/src/template_parser.c

@@ -0,0 +1,419 @@
+/*
+ * LuCI Template - Parser implementation
+ *
+ *   Copyright (C) 2009-2012 Jo-Philipp Wich <jow@openwrt.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
+ *
+ *  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.
+ */
+
+#include "template_parser.h"
+#include "template_utils.h"
+#include "template_lmo.h"
+
+
+/* leading and trailing code for different types */
+static const char *const gen_code[9][2] = {
+	{NULL,              NULL},
+	{"write(\"",        "\")"},
+	{NULL,              NULL},
+	{"write(tostring(", " or \"\"))"},
+	{"include(\"",      "\")"},
+	{"write(\"",        "\")"},
+	{"write(\"",        "\")"},
+	{NULL,              " "},
+	{}
+};
+
+/* Simple strstr() like function that takes len arguments for both haystack and needle. */
+static char *strfind(char *haystack, int hslen, const char *needle, int ndlen)
+{
+	int match = 0;
+	int i, j;
+
+	for( i = 0; i < hslen; i++ )
+	{
+		if( haystack[i] == needle[0] )
+		{
+			match = ((ndlen == 1) || ((i + ndlen) <= hslen));
+
+			for( j = 1; (j < ndlen) && ((i + j) < hslen); j++ )
+			{
+				if( haystack[i+j] != needle[j] )
+				{
+					match = 0;
+					break;
+				}
+			}
+
+			if( match )
+				return &haystack[i];
+		}
+	}
+
+	return NULL;
+}
+
+struct template_parser * template_open(const char *file)
+{
+	struct stat s;
+	struct template_parser *parser;
+
+	if (!(parser = malloc(sizeof(*parser))))
+		goto err;
+
+	memset(parser, 0, sizeof(*parser));
+	parser->fd = -1;
+	parser->file = file;
+
+	if (stat(file, &s))
+		goto err;
+
+	if ((parser->fd = open(file, O_RDONLY)) < 0)
+		goto err;
+
+	parser->size = s.st_size;
+	parser->data = mmap(NULL, parser->size, PROT_READ, MAP_PRIVATE,
+						parser->fd, 0);
+
+	if (parser->data != MAP_FAILED)
+	{
+		parser->off = parser->data;
+		parser->cur_chunk.type = T_TYPE_INIT;
+		parser->cur_chunk.s    = parser->data;
+		parser->cur_chunk.e    = parser->data;
+
+		return parser;
+	}
+
+err:
+	template_close(parser);
+	return NULL;
+}
+
+struct template_parser * template_string(const char *str, uint32_t len)
+{
+	struct template_parser *parser;
+
+	if (!str) {
+		errno = EINVAL;
+		return NULL;
+	}
+
+	if (!(parser = malloc(sizeof(*parser))))
+		goto err;
+
+	memset(parser, 0, sizeof(*parser));
+	parser->fd = -1;
+
+	parser->size = len;
+	parser->data = (char*)str;
+
+	parser->off = parser->data;
+	parser->cur_chunk.type = T_TYPE_INIT;
+	parser->cur_chunk.s    = parser->data;
+	parser->cur_chunk.e    = parser->data;
+
+	return parser;
+
+err:
+	template_close(parser);
+	return NULL;
+}
+
+void template_close(struct template_parser *parser)
+{
+	if (!parser)
+		return;
+
+	if (parser->gc != NULL)
+		free(parser->gc);
+
+	/* if file is not set, we were parsing a string */
+	if (parser->file) {
+		if ((parser->data != NULL) && (parser->data != MAP_FAILED))
+			munmap(parser->data, parser->size);
+
+		if (parser->fd >= 0)
+			close(parser->fd);
+	}
+
+	free(parser);
+}
+
+static void template_text(struct template_parser *parser, const char *e)
+{
+	const char *s = parser->off;
+
+	if (s < (parser->data + parser->size))
+	{
+		if (parser->strip_after)
+		{
+			while ((s <= e) && isspace(*s))
+				s++;
+		}
+
+		parser->cur_chunk.type = T_TYPE_TEXT;
+	}
+	else
+	{
+		parser->cur_chunk.type = T_TYPE_EOF;
+	}
+
+	parser->cur_chunk.line = parser->line;
+	parser->cur_chunk.s = s;
+	parser->cur_chunk.e = e;
+}
+
+static void template_code(struct template_parser *parser, const char *e)
+{
+	const char *s = parser->off;
+
+	parser->strip_before = 0;
+	parser->strip_after = 0;
+
+	if (*s == '-')
+	{
+		parser->strip_before = 1;
+		for (s++; (s <= e) && (*s == ' ' || *s == '\t'); s++);
+	}
+
+	if (*(e-1) == '-')
+	{
+		parser->strip_after = 1;
+		for (e--; (e >= s) && (*e == ' ' || *e == '\t'); e--);
+	}
+
+	switch (*s)
+	{
+		/* comment */
+		case '#':
+			s++;
+			parser->cur_chunk.type = T_TYPE_COMMENT;
+			break;
+
+		/* include */
+		case '+':
+			s++;
+			parser->cur_chunk.type = T_TYPE_INCLUDE;
+			break;
+
+		/* translate */
+		case ':':
+			s++;
+			parser->cur_chunk.type = T_TYPE_I18N;
+			break;
+
+		/* translate raw */
+		case '_':
+			s++;
+			parser->cur_chunk.type = T_TYPE_I18N_RAW;
+			break;
+
+		/* expr */
+		case '=':
+			s++;
+			parser->cur_chunk.type = T_TYPE_EXPR;
+			break;
+
+		/* code */
+		default:
+			parser->cur_chunk.type = T_TYPE_CODE;
+			break;
+	}
+
+	parser->cur_chunk.line = parser->line;
+	parser->cur_chunk.s = s;
+	parser->cur_chunk.e = e;
+}
+
+static const char *
+template_format_chunk(struct template_parser *parser, size_t *sz)
+{
+	const char *s, *p;
+	const char *head, *tail;
+	struct template_chunk *c = &parser->prv_chunk;
+	struct template_buffer *buf;
+
+	*sz = 0;
+	s = parser->gc = NULL;
+
+	if (parser->strip_before && c->type == T_TYPE_TEXT)
+	{
+		while ((c->e > c->s) && isspace(*(c->e - 1)))
+			c->e--;
+	}
+
+	/* empty chunk */
+	if (c->s == c->e)
+	{
+		if (c->type == T_TYPE_EOF)
+		{
+			*sz = 0;
+			s = NULL;
+		}
+		else
+		{
+			*sz = 1;
+			s = " ";
+		}
+	}
+
+	/* format chunk */
+	else if ((buf = buf_init(c->e - c->s)) != NULL)
+	{
+		if ((head = gen_code[c->type][0]) != NULL)
+			buf_append(buf, head, strlen(head));
+
+		switch (c->type)
+		{
+			case T_TYPE_TEXT:
+				luastr_escape(buf, c->s, c->e - c->s, 0);
+				break;
+
+			case T_TYPE_EXPR:
+				buf_append(buf, c->s, c->e - c->s);
+				for (p = c->s; p < c->e; p++)
+					parser->line += (*p == '\n');
+				break;
+
+			case T_TYPE_INCLUDE:
+				luastr_escape(buf, c->s, c->e - c->s, 0);
+				break;
+
+			case T_TYPE_I18N:
+				luastr_translate(buf, c->s, c->e - c->s, 1);
+				break;
+
+			case T_TYPE_I18N_RAW:
+				luastr_translate(buf, c->s, c->e - c->s, 0);
+				break;
+
+			case T_TYPE_CODE:
+				buf_append(buf, c->s, c->e - c->s);
+				for (p = c->s; p < c->e; p++)
+					parser->line += (*p == '\n');
+				break;
+		}
+
+		if ((tail = gen_code[c->type][1]) != NULL)
+			buf_append(buf, tail, strlen(tail));
+
+		*sz = buf_length(buf);
+		s = parser->gc = buf_destroy(buf);
+
+		if (!*sz)
+		{
+			*sz = 1;
+			s = " ";
+		}
+	}
+
+	return s;
+}
+
+const char *template_reader(lua_State *L, void *ud, size_t *sz)
+{
+	struct template_parser *parser = ud;
+	int rem = parser->size - (parser->off - parser->data);
+	char *tag;
+
+	parser->prv_chunk = parser->cur_chunk;
+
+	/* free previous string */
+	if (parser->gc)
+	{
+		free(parser->gc);
+		parser->gc = NULL;
+	}
+
+	/* before tag */
+	if (!parser->in_expr)
+	{
+		if ((tag = strfind(parser->off, rem, "<%", 2)) != NULL)
+		{
+			template_text(parser, tag);
+			parser->off = tag + 2;
+			parser->in_expr = 1;
+		}
+		else
+		{
+			template_text(parser, parser->data + parser->size);
+			parser->off = parser->data + parser->size;
+		}
+	}
+
+	/* inside tag */
+	else
+	{
+		if ((tag = strfind(parser->off, rem, "%>", 2)) != NULL)
+		{
+			template_code(parser, tag);
+			parser->off = tag + 2;
+			parser->in_expr = 0;
+		}
+		else
+		{
+			/* unexpected EOF */
+			template_code(parser, parser->data + parser->size);
+
+			*sz = 1;
+			return "\033";
+		}
+	}
+
+	return template_format_chunk(parser, sz);
+}
+
+int template_error(lua_State *L, struct template_parser *parser)
+{
+	const char *err = luaL_checkstring(L, -1);
+	const char *off = parser->prv_chunk.s;
+	const char *ptr;
+	char msg[1024];
+	int line = 0;
+	int chunkline = 0;
+
+	if ((ptr = strfind((char *)err, strlen(err), "]:", 2)) != NULL)
+	{
+		chunkline = atoi(ptr + 2) - parser->prv_chunk.line;
+
+		while (*ptr)
+		{
+			if (*ptr++ == ' ')
+			{
+				err = ptr;
+				break;
+			}
+		}
+	}
+
+	if (strfind((char *)err, strlen(err), "'char(27)'", 10) != NULL)
+	{
+		off = parser->data + parser->size;
+		err = "'%>' expected before end of file";
+		chunkline = 0;
+	}
+
+	for (ptr = parser->data; ptr < off; ptr++)
+		if (*ptr == '\n')
+			line++;
+
+	snprintf(msg, sizeof(msg), "Syntax error in %s:%d: %s",
+			 parser->file ? parser->file : "[string]", line + chunkline, err ? err : "(unknown error)");
+
+	lua_pushnil(L);
+	lua_pushinteger(L, line + chunkline);
+	lua_pushstring(L, msg);
+
+	return 3;
+}

+ 80 - 0
package/gluon-web/src/template_parser.h

@@ -0,0 +1,80 @@
+/*
+ * LuCI Template - Parser header
+ *
+ *   Copyright (C) 2009 Jo-Philipp Wich <jow@openwrt.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
+ *
+ *  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.
+ */
+
+#ifndef _TEMPLATE_PARSER_H_
+#define _TEMPLATE_PARSER_H_
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <stdint.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <sys/mman.h>
+#include <string.h>
+#include <ctype.h>
+#include <errno.h>
+
+#include <lua.h>
+#include <lualib.h>
+#include <lauxlib.h>
+
+
+/* code types */
+#define T_TYPE_INIT     0
+#define T_TYPE_TEXT     1
+#define T_TYPE_COMMENT  2
+#define T_TYPE_EXPR     3
+#define T_TYPE_INCLUDE  4
+#define T_TYPE_I18N     5
+#define T_TYPE_I18N_RAW 6
+#define T_TYPE_CODE     7
+#define T_TYPE_EOF      8
+
+
+struct template_chunk {
+	const char *s;
+	const char *e;
+	int type;
+	int line;
+};
+
+/* parser state */
+struct template_parser {
+	int fd;
+	uint32_t size;
+	char *data;
+	char *off;
+	char *gc;
+	int line;
+	int in_expr;
+	int strip_before;
+	int strip_after;
+	struct template_chunk prv_chunk;
+	struct template_chunk cur_chunk;
+	const char *file;
+};
+
+struct template_parser * template_open(const char *file);
+struct template_parser * template_string(const char *str, uint32_t len);
+void template_close(struct template_parser *parser);
+
+const char *template_reader(lua_State *L, void *ud, size_t *sz);
+int template_error(lua_State *L, struct template_parser *parser);
+
+#endif

+ 384 - 0
package/gluon-web/src/template_utils.c

@@ -0,0 +1,384 @@
+/*
+ * LuCI Template - Utility functions
+ *
+ *   Copyright (C) 2010 Jo-Philipp Wich <jow@openwrt.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
+ *
+ *  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.
+ */
+
+#include "template_utils.h"
+#include "template_lmo.h"
+
+/* initialize a buffer object */
+struct template_buffer * buf_init(int size)
+{
+	struct template_buffer *buf;
+
+	if (size <= 0)
+		size = 1024;
+
+	buf = (struct template_buffer *)malloc(sizeof(struct template_buffer));
+
+	if (buf != NULL)
+	{
+		buf->fill = 0;
+		buf->size = size;
+		buf->data = malloc(buf->size);
+
+		if (buf->data != NULL)
+		{
+			buf->dptr = buf->data;
+			buf->data[0] = 0;
+
+			return buf;
+		}
+
+		free(buf);
+	}
+
+	return NULL;
+}
+
+/* grow buffer */
+static int buf_grow(struct template_buffer *buf, int size)
+{
+	unsigned int off = (buf->dptr - buf->data);
+	char *data;
+
+	if (size <= 0)
+		size = 1024;
+
+	data = realloc(buf->data, buf->size + size);
+
+	if (data != NULL)
+	{
+		buf->data  = data;
+		buf->dptr  = data + off;
+		buf->size += size;
+
+		return buf->size;
+	}
+
+	return 0;
+}
+
+/* put one char into buffer object */
+static int buf_putchar(struct template_buffer *buf, char c)
+{
+	if( ((buf->fill + 1) >= buf->size) && !buf_grow(buf, 0) )
+		return 0;
+
+	*(buf->dptr++) = c;
+	*(buf->dptr) = 0;
+
+	buf->fill++;
+	return 1;
+}
+
+/* append data to buffer */
+int buf_append(struct template_buffer *buf, const char *s, int len)
+{
+	if ((buf->fill + len + 1) >= buf->size)
+	{
+		if (!buf_grow(buf, len + 1))
+			return 0;
+	}
+
+	memcpy(buf->dptr, s, len);
+	buf->fill += len;
+	buf->dptr += len;
+
+	*(buf->dptr) = 0;
+
+	return len;
+}
+
+/* destroy buffer object and return pointer to data */
+char * buf_destroy(struct template_buffer *buf)
+{
+	char *data = buf->data;
+
+	free(buf);
+	return data;
+}
+
+
+/* calculate the number of expected continuation chars */
+static inline int mb_num_chars(unsigned char c)
+{
+	if ((c & 0xE0) == 0xC0)
+		return 2;
+	else if ((c & 0xF0) == 0xE0)
+		return 3;
+	else if ((c & 0xF8) == 0xF0)
+		return 4;
+	else if ((c & 0xFC) == 0xF8)
+		return 5;
+	else if ((c & 0xFE) == 0xFC)
+		return 6;
+
+	return 1;
+}
+
+/* test whether the given byte is a valid continuation char */
+static inline int mb_is_cont(unsigned char c)
+{
+	return ((c >= 0x80) && (c <= 0xBF));
+}
+
+/* test whether the byte sequence at the given pointer with the given
+ * length is the shortest possible representation of the code point */
+static inline int mb_is_shortest(unsigned char *s, int n)
+{
+	switch (n)
+	{
+		case 2:
+			/* 1100000x (10xxxxxx) */
+			return !(((*s >> 1) == 0x60) &&
+					 ((*(s+1) >> 6) == 0x02));
+
+		case 3:
+			/* 11100000 100xxxxx (10xxxxxx) */
+			return !((*s == 0xE0) &&
+					 ((*(s+1) >> 5) == 0x04) &&
+					 ((*(s+2) >> 6) == 0x02));
+
+		case 4:
+			/* 11110000 1000xxxx (10xxxxxx 10xxxxxx) */
+			return !((*s == 0xF0) &&
+					 ((*(s+1) >> 4) == 0x08) &&
+					 ((*(s+2) >> 6) == 0x02) &&
+					 ((*(s+3) >> 6) == 0x02));
+
+		case 5:
+			/* 11111000 10000xxx (10xxxxxx 10xxxxxx 10xxxxxx) */
+			return !((*s == 0xF8) &&
+					 ((*(s+1) >> 3) == 0x10) &&
+					 ((*(s+2) >> 6) == 0x02) &&
+					 ((*(s+3) >> 6) == 0x02) &&
+					 ((*(s+4) >> 6) == 0x02));
+
+		case 6:
+			/* 11111100 100000xx (10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx) */
+			return !((*s == 0xF8) &&
+					 ((*(s+1) >> 2) == 0x20) &&
+					 ((*(s+2) >> 6) == 0x02) &&
+					 ((*(s+3) >> 6) == 0x02) &&
+					 ((*(s+4) >> 6) == 0x02) &&
+					 ((*(s+5) >> 6) == 0x02));
+	}
+
+	return 1;
+}
+
+/* test whether the byte sequence at the given pointer with the given
+ * length is an UTF-16 surrogate */
+static inline int mb_is_surrogate(unsigned char *s, int n)
+{
+	return ((n == 3) && (*s == 0xED) && (*(s+1) >= 0xA0) && (*(s+1) <= 0xBF));
+}
+
+/* test whether the byte sequence at the given pointer with the given
+ * length is an illegal UTF-8 code point */
+static inline int mb_is_illegal(unsigned char *s, int n)
+{
+	return ((n == 3) && (*s == 0xEF) && (*(s+1) == 0xBF) &&
+			(*(s+2) >= 0xBE) && (*(s+2) <= 0xBF));
+}
+
+
+/* scan given source string, validate UTF-8 sequence and store result
+ * in given buffer object */
+static int validate_utf8(unsigned char **s, int l, struct template_buffer *buf)
+{
+	unsigned char *ptr = *s;
+	unsigned int o = 0, v, n;
+
+	/* ascii byte without null */
+	if ((*(ptr+0) >= 0x01) && (*(ptr+0) <= 0x7F))
+	{
+		if (!buf_putchar(buf, *ptr++))
+			return 0;
+
+		o = 1;
+	}
+
+	/* multi byte sequence */
+	else if ((n = mb_num_chars(*ptr)) > 1)
+	{
+		/* count valid chars */
+		for (v = 1; (v <= n) && ((o+v) < l) && mb_is_cont(*(ptr+v)); v++);
+
+		switch (n)
+		{
+			case 6:
+			case 5:
+				/* five and six byte sequences are always invalid */
+				if (!buf_putchar(buf, '?'))
+					return 0;
+
+				break;
+
+			default:
+				/* if the number of valid continuation bytes matches the
+				 * expected number and if the sequence is legal, copy
+				 * the bytes to the destination buffer */
+				if ((v == n) && mb_is_shortest(ptr, n) &&
+					!mb_is_surrogate(ptr, n) && !mb_is_illegal(ptr, n))
+				{
+					/* copy sequence */
+					if (!buf_append(buf, (char *)ptr, n))
+						return 0;
+				}
+
+				/* the found sequence is illegal, skip it */
+				else
+				{
+					/* invalid sequence */
+					if (!buf_putchar(buf, '?'))
+						return 0;
+				}
+
+				break;
+		}
+
+		/* advance beyound the last found valid continuation char */
+		o = v;
+		ptr += v;
+	}
+
+	/* invalid byte (0x00) */
+	else
+	{
+		if (!buf_putchar(buf, '?')) /* or 0xEF, 0xBF, 0xBD */
+			return 0;
+
+		o = 1;
+		ptr++;
+	}
+
+	*s = ptr;
+	return o;
+}
+
+/* Sanitize given string and strip all invalid XML bytes
+ * Validate UTF-8 sequences
+ * Escape XML control chars */
+char * pcdata(const char *s, unsigned int l)
+{
+	struct template_buffer *buf = buf_init(l);
+	unsigned char *ptr = (unsigned char *)s;
+	unsigned int o, v;
+	char esq[8];
+	int esl;
+
+	if (!buf)
+		return NULL;
+
+	for (o = 0; o < l; o++)
+	{
+		/* Invalid XML bytes */
+		if (((*ptr >= 0x00) && (*ptr <= 0x08)) ||
+		    ((*ptr >= 0x0B) && (*ptr <= 0x0C)) ||
+		    ((*ptr >= 0x0E) && (*ptr <= 0x1F)) ||
+		    (*ptr == 0x7F))
+		{
+			ptr++;
+		}
+
+		/* Escapes */
+		else if ((*ptr == 0x26) ||
+		         (*ptr == 0x27) ||
+		         (*ptr == 0x22) ||
+		         (*ptr == 0x3C) ||
+		         (*ptr == 0x3E))
+		{
+			esl = snprintf(esq, sizeof(esq), "&#%i;", *ptr);
+
+			if (!buf_append(buf, esq, esl))
+				break;
+
+			ptr++;
+		}
+
+		/* ascii char */
+		else if (*ptr <= 0x7F)
+		{
+			buf_putchar(buf, (char)*ptr++);
+		}
+
+		/* multi byte sequence */
+		else
+		{
+			if (!(v = validate_utf8(&ptr, l - o, buf)))
+				break;
+
+			o += (v - 1);
+		}
+	}
+
+	return buf_destroy(buf);
+}
+
+void luastr_escape(struct template_buffer *out, const char *s, unsigned int l, int escape_xml)
+{
+	int esl;
+	char esq[8];
+	char *ptr;
+
+	for (ptr = (char *)s; ptr < (s + l); ptr++)
+	{
+		switch (*ptr)
+		{
+		case '\\':
+			buf_append(out, "\\\\", 2);
+			break;
+
+		case '"':
+			if (escape_xml)
+				buf_append(out, "&#34;", 5);
+			else
+				buf_append(out, "\\\"", 2);
+			break;
+
+		case '\n':
+			buf_append(out, "\\n", 2);
+			break;
+
+		case '\'':
+		case '&':
+		case '<':
+		case '>':
+			if (escape_xml)
+			{
+				esl = snprintf(esq, sizeof(esq), "&#%i;", *ptr);
+				buf_append(out, esq, esl);
+				break;
+			}
+
+		default:
+			buf_putchar(out, *ptr);
+		}
+	}
+}
+
+void luastr_translate(struct template_buffer *out, const char *s, unsigned int l, int escape_xml)
+{
+	char *tr;
+	int trlen;
+
+	if (!lmo_translate(s, l, &tr, &trlen))
+		luastr_escape(out, tr, trlen, escape_xml);
+	else
+		luastr_escape(out, s, l, escape_xml);
+}

+ 51 - 0
package/gluon-web/src/template_utils.h

@@ -0,0 +1,51 @@
+/*
+ * LuCI Template - Utility header
+ *
+ *   Copyright (C) 2010-2012 Jo-Philipp Wich <jow@openwrt.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
+ *
+ *  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.
+ */
+
+#ifndef _TEMPLATE_UTILS_H_
+#define _TEMPLATE_UTILS_H_
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+
+/* buffer object */
+struct template_buffer {
+	char *data;
+	char *dptr;
+	unsigned int size;
+	unsigned int fill;
+};
+
+struct template_buffer * buf_init(int size);
+int buf_append(struct template_buffer *buf, const char *s, int len);
+char * buf_destroy(struct template_buffer *buf);
+
+/* read buffer length */
+static inline int buf_length(struct template_buffer *buf)
+{
+	return buf->fill;
+}
+
+
+char * pcdata(const char *s, unsigned int l);
+
+void luastr_escape(struct template_buffer *out, const char *s, unsigned int l, int escape_xml);
+void luastr_translate(struct template_buffer *out, const char *s, unsigned int l, int escape_xml);
+
+#endif

+ 2 - 0
package/gluon.mk

@@ -17,6 +17,8 @@ endef
 
 # Languages supported by LuCi
 GLUON_SUPPORTED_LANGS := ca cs de el en es fr he hu it ja ms no pl pt-br pt ro ru sk sv tr uk vi zh-cn zh-tw
+GLUON_LANG_de := German
+GLUON_LANG_fr := French
 
 GLUON_I18N_PACKAGES := $(foreach lang,$(GLUON_SUPPORTED_LANGS),+LUCI_LANG_$(lang):luci-i18n-base-$(lang))
 GLUON_I18N_CONFIG := $(foreach lang,$(GLUON_SUPPORTED_LANGS),CONFIG_LUCI_LANG_$(lang))

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