From e4b74be506517f77b3492c61c51862c57be1f26f Mon Sep 17 00:00:00 2001 From: Matthias Schiffer Date: Wed, 8 Feb 2017 22:14:30 +0100 Subject: [PATCH] gluon-web: add package The gluon-web package is basically a stripped-down and refactored version of the LuCI base. --- contrib/i18n-scan.pl | 128 +++++ package/gluon-web/Makefile | 53 ++ .../files/lib/gluon/web/view/csrftoken.html | 14 + .../files/lib/gluon/web/view/error404.html | 9 + .../files/lib/gluon/web/view/error500.html | 9 + .../files/lib/gluon/web/view/layout.html | 3 + .../lib/gluon/web/view/model/dynlist.html | 20 + .../files/lib/gluon/web/view/model/form.html | 28 + .../lib/gluon/web/view/model/fvalue.html | 5 + .../lib/gluon/web/view/model/lvalue.html | 41 ++ .../lib/gluon/web/view/model/section.html | 28 + .../lib/gluon/web/view/model/tvalue.html | 3 + .../files/lib/gluon/web/view/model/value.html | 12 + .../gluon/web/view/model/valuewrapper.html | 18 + .../lib/gluon/web/view/model/wrapper.html | 6 + .../files/lib/gluon/web/www/index.html | 8 + .../web/www/static/resources/gluon-web.js | 1 + package/gluon-web/i18n/de.po | 56 ++ package/gluon-web/i18n/fr.po | 54 ++ package/gluon-web/i18n/gluon-web.pot | 45 ++ package/gluon-web/javascript/gluon-web.js | 531 ++++++++++++++++++ .../luasrc/lib/gluon/web/www/cgi-bin/gluon | 3 + .../luasrc/usr/lib/lua/gluon/web/cgi.lua | 38 ++ .../usr/lib/lua/gluon/web/dispatcher.lua | 258 +++++++++ .../luasrc/usr/lib/lua/gluon/web/http.lua | 123 ++++ .../usr/lib/lua/gluon/web/http/protocol.lua | 268 +++++++++ .../luasrc/usr/lib/lua/gluon/web/model.lua | 466 +++++++++++++++ .../usr/lib/lua/gluon/web/model/datatypes.lua | 167 ++++++ .../luasrc/usr/lib/lua/gluon/web/template.lua | 92 +++ .../luasrc/usr/lib/lua/gluon/web/util.lua | 100 ++++ package/gluon-web/src/Makefile | 16 + package/gluon-web/src/template_lmo.c | 288 ++++++++++ package/gluon-web/src/template_lmo.h | 81 +++ package/gluon-web/src/template_lualib.c | 121 ++++ package/gluon-web/src/template_lualib.h | 30 + package/gluon-web/src/template_parser.c | 419 ++++++++++++++ package/gluon-web/src/template_parser.h | 80 +++ package/gluon-web/src/template_utils.c | 384 +++++++++++++ package/gluon-web/src/template_utils.h | 51 ++ package/gluon.mk | 2 + 40 files changed, 4059 insertions(+) create mode 100755 contrib/i18n-scan.pl create mode 100644 package/gluon-web/Makefile create mode 100644 package/gluon-web/files/lib/gluon/web/view/csrftoken.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/error404.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/error500.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/layout.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/model/dynlist.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/model/form.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/model/fvalue.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/model/lvalue.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/model/section.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/model/tvalue.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/model/value.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/model/valuewrapper.html create mode 100644 package/gluon-web/files/lib/gluon/web/view/model/wrapper.html create mode 100644 package/gluon-web/files/lib/gluon/web/www/index.html create mode 100644 package/gluon-web/files/lib/gluon/web/www/static/resources/gluon-web.js create mode 100644 package/gluon-web/i18n/de.po create mode 100644 package/gluon-web/i18n/fr.po create mode 100644 package/gluon-web/i18n/gluon-web.pot create mode 100644 package/gluon-web/javascript/gluon-web.js create mode 100755 package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon create mode 100644 package/gluon-web/luasrc/usr/lib/lua/gluon/web/cgi.lua create mode 100644 package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua create mode 100644 package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua create mode 100644 package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua create mode 100644 package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua create mode 100644 package/gluon-web/luasrc/usr/lib/lua/gluon/web/model/datatypes.lua create mode 100644 package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua create mode 100644 package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua create mode 100644 package/gluon-web/src/Makefile create mode 100644 package/gluon-web/src/template_lmo.c create mode 100644 package/gluon-web/src/template_lmo.h create mode 100644 package/gluon-web/src/template_lualib.c create mode 100644 package/gluon-web/src/template_lualib.h create mode 100644 package/gluon-web/src/template_parser.c create mode 100644 package/gluon-web/src/template_parser.h create mode 100644 package/gluon-web/src/template_utils.c create mode 100644 package/gluon-web/src/template_utils.h diff --git a/contrib/i18n-scan.pl b/contrib/i18n-scan.pl new file mode 100755 index 00000000..8e7d2597 --- /dev/null +++ b/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 \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 = ; + 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; +} diff --git a/package/gluon-web/Makefile b/package/gluon-web/Makefile new file mode 100644 index 00000000..57700fc1 --- /dev/null +++ b/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)) diff --git a/package/gluon-web/files/lib/gluon/web/view/csrftoken.html b/package/gluon-web/files/lib/gluon/web/view/csrftoken.html new file mode 100644 index 00000000..673611de --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/csrftoken.html @@ -0,0 +1,14 @@ +<%# + Copyright 2015 Jo-Philipp Wich + Licensed to the public under the Apache License 2.0. +-%> + +

<%:Form token mismatch%>

+
+ +

<%:The submitted security token is invalid or already expired!%>

+ +

<%: + In order to prevent unauthorized access to the system, your request has + been blocked. +%>

diff --git a/package/gluon-web/files/lib/gluon/web/view/error404.html b/package/gluon-web/files/lib/gluon/web/view/error404.html new file mode 100644 index 00000000..d1dce589 --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/error404.html @@ -0,0 +1,9 @@ +<%# + Copyright 2008 Steven Barth + Copyright 2008 Jo-Philipp Wich + Licensed to the public under the Apache License 2.0. +-%> + +

404 <%:Not Found%>

+

<%:Sorry, the object you requested was not found.%>

+<%=pcdata(message)%> diff --git a/package/gluon-web/files/lib/gluon/web/view/error500.html b/package/gluon-web/files/lib/gluon/web/view/error500.html new file mode 100644 index 00000000..6c186286 --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/error500.html @@ -0,0 +1,9 @@ +<%# + Copyright 2008 Steven Barth + Copyright 2008 Jo-Philipp Wich + Licensed to the public under the Apache License 2.0. +-%> + +

500 <%:Internal Server Error%>

+

<%:Sorry, the server encountered an unexpected error.%>

+
<%=pcdata(message)%>
diff --git a/package/gluon-web/files/lib/gluon/web/view/layout.html b/package/gluon-web/files/lib/gluon/web/view/layout.html new file mode 100644 index 00000000..34f287c1 --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/layout.html @@ -0,0 +1,3 @@ +<% + include("themes/" .. theme .. "/layout") +%> diff --git a/package/gluon-web/files/lib/gluon/web/view/model/dynlist.html b/package/gluon-web/files/lib/gluon/web/view/model/dynlist.html new file mode 100644 index 00000000..76d7bbac --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/model/dynlist.html @@ -0,0 +1,20 @@ +> +<% + for i, val in ipairs(self:cfgvalue()) do +%> + />
+<% end %> + diff --git a/package/gluon-web/files/lib/gluon/web/view/model/form.html b/package/gluon-web/files/lib/gluon/web/view/model/form.html new file mode 100644 index 00000000..6fcb0d48 --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/model/form.html @@ -0,0 +1,28 @@ +
+ + + +
+ <% if self.title and #self.title > 0 then %>

<%=self.title%>

<% end %> + <% if self.description and #self.description > 0 then %>
<%=self.description%>
<% end %> + <% self:render_children(renderer) %> +
+<%- if self.message then %> +
<%=self.message%>
+<%- end %> +<%- if self.errmessage then %> +
<%=self.errmessage%>
+<%- end %> +
+ <%- if self.submit ~= false then %> + + <% end %> + <%- if self.reset ~= false then %> + + <% end %> +
+
diff --git a/package/gluon-web/files/lib/gluon/web/view/model/fvalue.html b/package/gluon-web/files/lib/gluon/web/view/model/fvalue.html new file mode 100644 index 00000000..60741e32 --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/model/fvalue.html @@ -0,0 +1,5 @@ + /> +> diff --git a/package/gluon-web/files/lib/gluon/web/view/model/lvalue.html b/package/gluon-web/files/lib/gluon/web/view/model/lvalue.html new file mode 100644 index 00000000..298c2faa --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/model/lvalue.html @@ -0,0 +1,41 @@ +<% + local i, key + local br = self.orientation == "horizontal" and ' ' or '
' +%> + +<% if self.widget == "select" then %> + +<% elseif self.widget == "radio" then %> +
+ <% for i, key in pairs(self.keylist) do %> + > + /> + > + <%=pcdata(self.vallist[i])%> + + <% if i == self.size then write(br) end %> + <% end %> +
+<% end %> diff --git a/package/gluon-web/files/lib/gluon/web/view/model/section.html b/package/gluon-web/files/lib/gluon/web/view/model/section.html new file mode 100644 index 00000000..45ee0272 --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/model/section.html @@ -0,0 +1,28 @@ +
+ <% if self.title and #self.title > 0 then -%> + <%=self.title%> + <%- end %> + <% if self.description and #self.description > 0 then -%> +
<%=self.description%>
+ <%- end %> +
+
+ <% self:render_children(renderer, scope) %> +
+ <% if self.error and self.error[1] then -%> +
+
    <% for _, e in ipairs(self.error[1]) do -%> +
  • + <%- 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 -%> +
  • + <%- end %>
+
+ <%- end %> +
+
diff --git a/package/gluon-web/files/lib/gluon/web/view/model/tvalue.html b/package/gluon-web/files/lib/gluon/web/view/model/tvalue.html new file mode 100644 index 00000000..bac1d829 --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/model/tvalue.html @@ -0,0 +1,3 @@ + diff --git a/package/gluon-web/files/lib/gluon/web/view/model/value.html b/package/gluon-web/files/lib/gluon/web/view/model/value.html new file mode 100644 index 00000000..f14c122e --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/model/value.html @@ -0,0 +1,12 @@ + /> diff --git a/package/gluon-web/files/lib/gluon/web/view/model/valuewrapper.html b/package/gluon-web/files/lib/gluon/web/view/model/valuewrapper.html new file mode 100644 index 00000000..35926cae --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/view/model/valuewrapper.html @@ -0,0 +1,18 @@ +
> + <%- if self.title and #self.title > 0 then -%> + +
+ <%- end -%> + <% if self.subtemplate then include(self.subtemplate) end %> + <% if self.description and #self.description > 0 then -%> +
+
+ <%=self.description%> +
+ <%- end %> + <%- if self.title and #self.title > 0 then -%> +
+ <%- end -%> +
diff --git a/package/gluon-web/files/lib/gluon/web/view/model/wrapper.html b/package/gluon-web/files/lib/gluon/web/view/model/wrapper.html new file mode 100644 index 00000000..5b45aa7d --- /dev/null +++ b/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 +%> + diff --git a/package/gluon-web/files/lib/gluon/web/www/index.html b/package/gluon-web/files/lib/gluon/web/www/index.html new file mode 100644 index 00000000..326644e8 --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/www/index.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/package/gluon-web/files/lib/gluon/web/www/static/resources/gluon-web.js b/package/gluon-web/files/lib/gluon/web/www/static/resources/gluon-web.js new file mode 100644 index 00000000..7d1b2084 --- /dev/null +++ b/package/gluon-web/files/lib/gluon/web/www/static/resources/gluon-web.js @@ -0,0 +1 @@ +!function(){function e(e){return/^-?\d+$/.test(e)?+e:NaN}function t(e){return/^-?\d*\.?\d+?$/.test(e)?+e:NaN}function n(e){var t;return e.match(/^([^\(]+)\(([^,]+),([^\)]+)\)$/)&&void 0!==(t=s[RegExp.$1])?function(){return t(RegExp.$2,RegExp.$3)}:e.match(/^([^\(]+)\(([^,\)]+)\)$/)&&void 0!==(t=s[RegExp.$1])?function(){return t(RegExp.$2)}:s[e]}function r(e,t){var n,r=document.getElementById(e);return r&&(n="checkbox"==r.type?r.checked:r.value?r.value:""),n==t}function a(e){for(var t=0;tn.index);u=u.nextSibling);u?o.insertBefore(n.node,u):o.appendChild(n.node),e=!0}o&&o.parentNode&&o.getAttribute("data-optionals")&&(o.parentNode.style.display=o.options.length<=1?"none":"")}e&&i()}function o(e,t,n,r){return e.addEventListener?e.addEventListener(t,n,!!r):e.attachEvent("on"+t,function(){var e=window.event;return!e.target&&e.srcElement&&(e.target=e.srcElement),!!n(e)}),e}function u(e,t,n){function r(r,s,p){for(var f=[];e.firstChild;){var v=e.firstChild,h=+v.index;h!=p&&("input"==v.nodeName.toLowerCase()?f.push(v.value||""):"select"==v.nodeName.toLowerCase()&&(f[f.length-1]=v.options[v.selectedIndex].value)),e.removeChild(v)}s>=0?(r=s+1,f.splice(s,0,"")):n||0!=f.length||f.push("");for(var h=1;h<=f.length;h++){var g=document.createElement("input");if(g.id=l+"."+h,g.name=l,g.value=f[h-1],g.type="text",g.index=h,g.className="gluon-input-text",c&&(g.placeholder=c),e.appendChild(g),t&&d(g,!1,t),o(g,"keydown",i),o(g,"keypress",a),h==r)g.focus();else if(-h==r){g.focus();var m=g.value;g.value=" ",g.value=m}if(n||f.length>1){var x=document.createElement("span");x.className="gluon-remove",e.appendChild(x),o(x,"click",u(!1)),e.appendChild(document.createElement("br"))}}var x=document.createElement("span");x.className="gluon-add",e.appendChild(x),o(x,"click",u(!0))}function a(e){e=e?e:window.event;var t=e.target?e.target:e.srcElement;switch(3==t.nodeType&&(t=t.parentNode),e.keyCode){case 8:case 46:return 0!=t.value.length||(e.preventDefault&&e.preventDefault(),!1);case 13:case 38:case 40:return e.preventDefault&&e.preventDefault(),!1}return!0}function i(e){e=e?e:window.event;var t,n,a=e.target?e.target:e.srcElement,i=0;if(a){for(3==a.nodeType&&(a=a.parentNode),i=a.index,t=a.previousSibling;t&&t.name!=l;)t=t.previousSibling;for(n=a.nextSibling;n&&n.name!=l;)n=n.nextSibling}switch(e.keyCode){case 8:case 46:var o="select"==a.nodeName.toLowerCase()||0==a.value.length;if(o){e.preventDefault&&e.preventDefault();var u=a.index;return 8==e.keyCode&&(u=-u+1),r(u,-1,i),!1}break;case 13:r(-1,i,-1);break;case 38:t&&t.focus();break;case 40:n&&n.focus()}return!0}function u(e){return function(t){t=t?t:window.event;for(var n=t.target?t.target:t.srcElement,r=n.previousSibling;r&&r.name!=l;)r=r.previousSibling;return e?i({target:r,keyCode:13}):(r.value="",i({target:r,keyCode:8})),!1}}var l=e.getAttribute("data-prefix"),c=e.getAttribute("data-placeholder");r(NaN,-1,-1)}function d(e,t,r){var a=n(r);if(a){var i=function(){if(e.form){e.className=e.className.replace(/ gluon-input-invalid/g,"");var n=e.options&&e.options.selectedIndex>-1?e.options[e.options.selectedIndex].value:e.value;0==n.length&&t||a.apply(n)||(e.className+=" gluon-input-invalid")}};o(e,"blur",i),o(e,"keyup",i),"SELECT"==e.nodeName&&(o(e,"change",i),o(e,"click",i)),i()}}function l(e,t,n){var r=c[e.id];r||(r={node:e,parent:e.parentNode.id,deps:[],index:n},c[e.id]=r),r.deps.push(t)}var c={},s={integer:function(){return!isNaN(e(this))},uinteger:function(){return e(this)>=0},"float":function(){return!isNaN(t(this))},ufloat:function(){return t(this)>=0},ipaddr:function(){return s.ip4addr.apply(this)||s.ip6addr.apply(this)},ip4addr:function(){return!!this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/)&&(RegExp.$1>=0&&RegExp.$1<=255&&RegExp.$2>=0&&RegExp.$2<=255&&RegExp.$3>=0&&RegExp.$3<=255&&RegExp.$4>=0&&RegExp.$4<=255)},ip6addr:function(){return this.indexOf("::")<0?null!=this.match(/^(?:[a-f0-9]{1,4}:){7}[a-f0-9]{1,4}$/i):!(this.indexOf(":::")>=0||this.match(/::.+::/)||this.match(/^:[^:]/)||this.match(/[^:]:$/))&&(!!this.match(/^(?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}$/i)||(!!this.match(/^(?:[a-f0-9]{1,4}:){7}:$/i)||!!this.match(/^:(?::[a-f0-9]{1,4}){7}$/i)))},wpakey:function(){var e=this;return 64==e.length?null!=e.match(/^[a-f0-9]{64}$/i):e.length>=8&&e.length<=63},range:function(e,n){var r=t(this);return r>=+e&&r<=+n},min:function(e){return t(this)>=+e},max:function(e){return t(this)<=+e},irange:function(t,n){var r=e(this);return r>=+t&&r<=+n},imin:function(t){return e(this)>=+t},imax:function(t){return e(this)<=+t},minlength:function(e){return(""+this).length>=+e},maxlength:function(e){return(""+this).length<=+e}};!function(){var e;e=document.querySelectorAll("[data-depends]");for(var t,n=0;void 0!==(t=e[n]);n++){var r=parseInt(t.getAttribute("data-index"),10),a=JSON.parse(t.getAttribute("data-depends"));if(!isNaN(r)&&a.length>0)for(var c=0;c\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 "" diff --git a/package/gluon-web/i18n/fr.po b/package/gluon-web/i18n/fr.po new file mode 100644 index 00000000..eff0092b --- /dev/null +++ b/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 \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 "" diff --git a/package/gluon-web/i18n/gluon-web.pot b/package/gluon-web/i18n/gluon-web.pot new file mode 100644 index 00000000..b8d0e01d --- /dev/null +++ b/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 "" diff --git a/package/gluon-web/javascript/gluon-web.js b/package/gluon-web/javascript/gluon-web.js new file mode 100644 index 00000000..910af710 --- /dev/null +++ b/package/gluon-web/javascript/gluon-web.js @@ -0,0 +1,531 @@ +/* + Copyright 2008 Steven Barth + Copyright 2008-2012 Jo-Philipp Wich + Copyright 2017 Matthias Schiffer + + 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(); + })(); +})(); diff --git a/package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon b/package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon new file mode 100755 index 00000000..2388b0ce --- /dev/null +++ b/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() diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/cgi.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/cgi.lua new file mode 100644 index 00000000..c7a1169a --- /dev/null +++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/cgi.lua @@ -0,0 +1,38 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2017 Matthias Schiffer +-- 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 diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua new file mode 100644 index 00000000..90318ce6 --- /dev/null +++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua @@ -0,0 +1,258 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2008-2015 Jo-Philipp Wich +-- Copyright 2017 Matthias Schiffer +-- 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 diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua new file mode 100644 index 00000000..2cf83de0 --- /dev/null +++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua @@ -0,0 +1,123 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2017 Matthias Schiffer +-- 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 diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua new file mode 100644 index 00000000..b0a2ad1e --- /dev/null +++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua @@ -0,0 +1,268 @@ +-- Copyright 2008 Freifunk Leipzig / Jo-Philipp Wich +-- Copyright 2017 Matthias Schiffer +-- 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 diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua new file mode 100644 index 00000000..249dddee --- /dev/null +++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua @@ -0,0 +1,466 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2017 Matthias Schiffer +-- 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 diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/model/datatypes.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/model/datatypes.lua new file mode 100644 index 00000000..49f55c70 --- /dev/null +++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/model/datatypes.lua @@ -0,0 +1,167 @@ +-- Copyright 2010 Jo-Philipp Wich +-- Copyright 2017 Matthias Schiffer +-- 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 diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua new file mode 100644 index 00000000..55b49ca9 --- /dev/null +++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua @@ -0,0 +1,92 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2017 Matthias Schiffer +-- 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 diff --git a/package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua new file mode 100644 index 00000000..8259ff95 --- /dev/null +++ b/package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua @@ -0,0 +1,100 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2017 Matthias Schiffer +-- 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 diff --git a/package/gluon-web/src/Makefile b/package/gluon-web/src/Makefile new file mode 100644 index 00000000..09c8a364 --- /dev/null +++ b/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 diff --git a/package/gluon-web/src/template_lmo.c b/package/gluon-web/src/template_lmo.c new file mode 100644 index 00000000..8cff1a33 --- /dev/null +++ b/package/gluon-web/src/template_lmo.c @@ -0,0 +1,288 @@ +/* + * lmo - Lua Machine Objects - Base functions + * + * Copyright (C) 2009-2010 Jo-Philipp Wich + * + * 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; +} diff --git a/package/gluon-web/src/template_lmo.h b/package/gluon-web/src/template_lmo.h new file mode 100644 index 00000000..d2951e9e --- /dev/null +++ b/package/gluon-web/src/template_lmo.h @@ -0,0 +1,81 @@ +/* + * lmo - Lua Machine Objects - General header + * + * Copyright (C) 2009-2012 Jo-Philipp Wich + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 diff --git a/package/gluon-web/src/template_lualib.c b/package/gluon-web/src/template_lualib.c new file mode 100644 index 00000000..b06a4857 --- /dev/null +++ b/package/gluon-web/src/template_lualib.c @@ -0,0 +1,121 @@ +/* + * LuCI Template - Lua binding + * + * Copyright (C) 2009 Jo-Philipp Wich + * + * 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; +} diff --git a/package/gluon-web/src/template_lualib.h b/package/gluon-web/src/template_lualib.h new file mode 100644 index 00000000..ec0e17c8 --- /dev/null +++ b/package/gluon-web/src/template_lualib.h @@ -0,0 +1,30 @@ +/* + * LuCI Template - Lua library header + * + * Copyright (C) 2009 Jo-Philipp Wich + * + * 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 diff --git a/package/gluon-web/src/template_parser.c b/package/gluon-web/src/template_parser.c new file mode 100644 index 00000000..55519edd --- /dev/null +++ b/package/gluon-web/src/template_parser.c @@ -0,0 +1,419 @@ +/* + * LuCI Template - Parser implementation + * + * Copyright (C) 2009-2012 Jo-Philipp Wich + * + * 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; +} diff --git a/package/gluon-web/src/template_parser.h b/package/gluon-web/src/template_parser.h new file mode 100644 index 00000000..3349c52b --- /dev/null +++ b/package/gluon-web/src/template_parser.h @@ -0,0 +1,80 @@ +/* + * LuCI Template - Parser header + * + * Copyright (C) 2009 Jo-Philipp Wich + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +/* 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 diff --git a/package/gluon-web/src/template_utils.c b/package/gluon-web/src/template_utils.c new file mode 100644 index 00000000..505d09d2 --- /dev/null +++ b/package/gluon-web/src/template_utils.c @@ -0,0 +1,384 @@ +/* + * LuCI Template - Utility functions + * + * Copyright (C) 2010 Jo-Philipp Wich + * + * 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, """, 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); +} diff --git a/package/gluon-web/src/template_utils.h b/package/gluon-web/src/template_utils.h new file mode 100644 index 00000000..5e44f3ab --- /dev/null +++ b/package/gluon-web/src/template_utils.h @@ -0,0 +1,51 @@ +/* + * LuCI Template - Utility header + * + * Copyright (C) 2010-2012 Jo-Philipp Wich + * + * 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 +#include +#include + + +/* 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 diff --git a/package/gluon.mk b/package/gluon.mk index daa66026..b6fbfea6 100644 --- a/package/gluon.mk +++ b/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))