gluon-web: add package

The gluon-web package is basically a stripped-down and refactored version
of the LuCI base.
This commit is contained in:
Matthias Schiffer 2017-02-08 22:14:30 +01:00
parent a06541623e
commit e4b74be506
No known key found for this signature in database
GPG Key ID: 16EF3F64CB201D9C
40 changed files with 4059 additions and 0 deletions

128
contrib/i18n-scan.pl Executable file
View File

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

View File

@ -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))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -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 ""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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

View File

@ -0,0 +1,288 @@
/*
* lmo - Lua Machine Objects - Base functions
*
* Copyright (C) 2009-2010 Jo-Philipp Wich <jow@openwrt.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "template_lmo.h"
/*
* Hash function from http://www.azillionmonkeys.com/qed/hash.html
* Copyright (C) 2004-2008 by Paul Hsieh
*/
static uint32_t sfh_hash(const char *data, int len)
{
uint32_t hash = len, tmp;
int rem;
if (len <= 0 || data == NULL) return 0;
rem = len & 3;
len >>= 2;
/* Main loop */
for (;len > 0; len--) {
hash += sfh_get16(data);
tmp = (sfh_get16(data+2) << 11) ^ hash;
hash = (hash << 16) ^ tmp;
data += 2*sizeof(uint16_t);
hash += hash >> 11;
}
/* Handle end cases */
switch (rem) {
case 3: hash += sfh_get16(data);
hash ^= hash << 16;
hash ^= data[sizeof(uint16_t)] << 18;
hash += hash >> 11;
break;
case 2: hash += sfh_get16(data);
hash ^= hash << 11;
hash += hash >> 17;
break;
case 1: hash += *data;
hash ^= hash << 10;
hash += hash >> 1;
}
/* Force "avalanching" of final 127 bits */
hash ^= hash << 3;
hash += hash >> 5;
hash ^= hash << 4;
hash += hash >> 17;
hash ^= hash << 25;
hash += hash >> 6;
return hash;
}
static uint32_t lmo_canon_hash(const char *str, int len)
{
char res[4096];
char *ptr, prev;
int off;
if (!str || len >= sizeof(res))
return 0;
for (prev = ' ', ptr = res, off = 0; off < len; prev = *str, off++, str++)
{
if (isspace(*str))
{
if (!isspace(prev))
*ptr++ = ' ';
}
else
{
*ptr++ = *str;
}
}
if ((ptr > res) && isspace(*(ptr-1)))
ptr--;
return sfh_hash(res, ptr - res);
}
static lmo_archive_t * lmo_open(const char *file)
{
int in = -1;
uint32_t idx_offset = 0;
struct stat s;
lmo_archive_t *ar = NULL;
if (stat(file, &s) == -1)
goto err;
if ((in = open(file, O_RDONLY)) == -1)
goto err;
if ((ar = (lmo_archive_t *)malloc(sizeof(*ar))) != NULL)
{
memset(ar, 0, sizeof(*ar));
ar->fd = in;
ar->size = s.st_size;
fcntl(ar->fd, F_SETFD, fcntl(ar->fd, F_GETFD) | FD_CLOEXEC);
if ((ar->mmap = mmap(NULL, ar->size, PROT_READ, MAP_SHARED, ar->fd, 0)) == MAP_FAILED)
goto err;
idx_offset = ntohl(*((const uint32_t *)
(ar->mmap + ar->size - sizeof(uint32_t))));
if (idx_offset >= ar->size)
goto err;
ar->index = (lmo_entry_t *)(ar->mmap + idx_offset);
ar->length = (ar->size - idx_offset - sizeof(uint32_t)) / sizeof(lmo_entry_t);
ar->end = ar->mmap + ar->size;
return ar;
}
err:
if (in > -1)
close(in);
if (ar != NULL)
{
if ((ar->mmap != NULL) && (ar->mmap != MAP_FAILED))
munmap(ar->mmap, ar->size);
free(ar);
}
return NULL;
}
static lmo_catalog_t *_lmo_catalogs;
static lmo_catalog_t *_lmo_active_catalog;
int lmo_load_catalog(const char *lang, const char *dir)
{
DIR *dh = NULL;
char pattern[16];
char path[PATH_MAX];
struct dirent *de = NULL;
lmo_archive_t *ar = NULL;
lmo_catalog_t *cat = NULL;
if (!lmo_change_catalog(lang))
return 0;
if (!dir || !(dh = opendir(dir)))
goto err;
if (!(cat = malloc(sizeof(*cat))))
goto err;
memset(cat, 0, sizeof(*cat));
snprintf(cat->lang, sizeof(cat->lang), "%s", lang);
snprintf(pattern, sizeof(pattern), "*.%s.lmo", lang);
while ((de = readdir(dh)) != NULL)
{
if (!fnmatch(pattern, de->d_name, 0))
{
snprintf(path, sizeof(path), "%s/%s", dir, de->d_name);
ar = lmo_open(path);
if (ar)
{
ar->next = cat->archives;
cat->archives = ar;
}
}
}
closedir(dh);
cat->next = _lmo_catalogs;
_lmo_catalogs = cat;
if (!_lmo_active_catalog)
_lmo_active_catalog = cat;
return 0;
err:
if (dh) closedir(dh);
if (cat) free(cat);
return -1;
}
int lmo_change_catalog(const char *lang)
{
lmo_catalog_t *cat;
for (cat = _lmo_catalogs; cat; cat = cat->next)
{
if (!strncmp(cat->lang, lang, sizeof(cat->lang)))
{
_lmo_active_catalog = cat;
return 0;
}
}
return -1;
}
static lmo_entry_t * lmo_find_entry(lmo_archive_t *ar, uint32_t hash)
{
unsigned int m, l, r;
uint32_t k;
l = 0;
r = ar->length - 1;
while (1)
{
m = l + ((r - l) / 2);
if (r < l)
break;
k = ntohl(ar->index[m].key_id);
if (k == hash)
return &ar->index[m];
if (k > hash)
{
if (!m)
break;
r = m - 1;
}
else
{
l = m + 1;
}
}
return NULL;
}
int lmo_translate(const char *key, int keylen, char **out, int *outlen)
{
uint32_t hash;
lmo_entry_t *e;
lmo_archive_t *ar;
if (!key || !_lmo_active_catalog)
return -2;
hash = lmo_canon_hash(key, keylen);
for (ar = _lmo_active_catalog->archives; ar; ar = ar->next)
{
if ((e = lmo_find_entry(ar, hash)) != NULL)
{
*out = ar->mmap + ntohl(e->offset);
*outlen = ntohl(e->length);
return 0;
}
}
return -1;
}

View File

@ -0,0 +1,81 @@
/*
* lmo - Lua Machine Objects - General header
*
* Copyright (C) 2009-2012 Jo-Philipp Wich <jow@openwrt.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef _TEMPLATE_LMO_H_
#define _TEMPLATE_LMO_H_
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>
#include <fnmatch.h>
#include <dirent.h>
#include <ctype.h>
#include <limits.h>
#if (defined(__GNUC__) && defined(__i386__))
#define sfh_get16(d) (*((const uint16_t *) (d)))
#else
#define sfh_get16(d) ((((uint32_t)(((const uint8_t *)(d))[1])) << 8)\
+(uint32_t)(((const uint8_t *)(d))[0]) )
#endif
struct lmo_entry {
uint32_t key_id;
uint32_t val_id;
uint32_t offset;
uint32_t length;
} __attribute__((packed));
typedef struct lmo_entry lmo_entry_t;
struct lmo_archive {
int fd;
int length;
uint32_t size;
lmo_entry_t *index;
char *mmap;
char *end;
struct lmo_archive *next;
};
typedef struct lmo_archive lmo_archive_t;
struct lmo_catalog {
char lang[6];
struct lmo_archive *archives;
struct lmo_catalog *next;
};
typedef struct lmo_catalog lmo_catalog_t;
int lmo_load_catalog(const char *lang, const char *dir);
int lmo_change_catalog(const char *lang);
int lmo_translate(const char *key, int keylen, char **out, int *outlen);
#endif

View File

@ -0,0 +1,121 @@
/*
* LuCI Template - Lua binding
*
* Copyright (C) 2009 Jo-Philipp Wich <jow@openwrt.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "template_lualib.h"
static int template_L_do_parse(lua_State *L, struct template_parser *parser, const char *chunkname)
{
int lua_status, rv;
if (!parser)
{
lua_pushnil(L);
lua_pushinteger(L, errno);
lua_pushstring(L, strerror(errno));
return 3;
}
lua_status = lua_load(L, template_reader, parser, chunkname);
if (lua_status == 0)
rv = 1;
else
rv = template_error(L, parser);
template_close(parser);
return rv;
}
static int template_L_parse(lua_State *L)
{
const char *file = luaL_checkstring(L, 1);
struct template_parser *parser = template_open(file);
return template_L_do_parse(L, parser, file);
}
static int template_L_parse_string(lua_State *L)
{
size_t len;
const char *str = luaL_checklstring(L, 1, &len);
struct template_parser *parser = template_string(str, len);
return template_L_do_parse(L, parser, "[string]");
}
static int template_L_pcdata(lua_State *L)
{
size_t len = 0;
const char *str = luaL_checklstring(L, 1, &len);
char *res = pcdata(str, len);
if (res != NULL)
{
lua_pushstring(L, res);
free(res);
return 1;
}
return 0;
}
static int template_L_load_catalog(lua_State *L) {
const char *lang = luaL_optstring(L, 1, "en");
const char *dir = luaL_optstring(L, 2, NULL);
lua_pushboolean(L, !lmo_load_catalog(lang, dir));
return 1;
}
static int template_L_translate(lua_State *L) {
size_t len;
char *tr;
int trlen;
const char *key = luaL_checklstring(L, 1, &len);
switch (lmo_translate(key, len, &tr, &trlen))
{
case 0:
lua_pushlstring(L, tr, trlen);
return 1;
case -1:
return 0;
}
lua_pushnil(L);
lua_pushstring(L, "no catalog loaded");
return 2;
}
/* module table */
static const luaL_reg R[] = {
{ "parse", template_L_parse },
{ "parse_string", template_L_parse_string },
{ "pcdata", template_L_pcdata },
{ "load_catalog", template_L_load_catalog },
{ "translate", template_L_translate },
{}
};
LUALIB_API int luaopen_gluon_web_template_parser(lua_State *L) {
luaL_register(L, TEMPLATE_LUALIB_META, R);
return 1;
}

View File

@ -0,0 +1,30 @@
/*
* LuCI Template - Lua library header
*
* Copyright (C) 2009 Jo-Philipp Wich <jow@openwrt.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef _TEMPLATE_LUALIB_H_
#define _TEMPLATE_LUALIB_H_
#include "template_parser.h"
#include "template_utils.h"
#include "template_lmo.h"
#define TEMPLATE_LUALIB_META "gluon.web.template.parser"
LUALIB_API int luaopen_gluon_web_template_parser(lua_State *L);
#endif

View File

@ -0,0 +1,419 @@
/*
* LuCI Template - Parser implementation
*
* Copyright (C) 2009-2012 Jo-Philipp Wich <jow@openwrt.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "template_parser.h"
#include "template_utils.h"
#include "template_lmo.h"
/* leading and trailing code for different types */
static const char *const gen_code[9][2] = {
{NULL, NULL},
{"write(\"", "\")"},
{NULL, NULL},
{"write(tostring(", " or \"\"))"},
{"include(\"", "\")"},
{"write(\"", "\")"},
{"write(\"", "\")"},
{NULL, " "},
{}
};
/* Simple strstr() like function that takes len arguments for both haystack and needle. */
static char *strfind(char *haystack, int hslen, const char *needle, int ndlen)
{
int match = 0;
int i, j;
for( i = 0; i < hslen; i++ )
{
if( haystack[i] == needle[0] )
{
match = ((ndlen == 1) || ((i + ndlen) <= hslen));
for( j = 1; (j < ndlen) && ((i + j) < hslen); j++ )
{
if( haystack[i+j] != needle[j] )
{
match = 0;
break;
}
}
if( match )
return &haystack[i];
}
}
return NULL;
}
struct template_parser * template_open(const char *file)
{
struct stat s;
struct template_parser *parser;
if (!(parser = malloc(sizeof(*parser))))
goto err;
memset(parser, 0, sizeof(*parser));
parser->fd = -1;
parser->file = file;
if (stat(file, &s))
goto err;
if ((parser->fd = open(file, O_RDONLY)) < 0)
goto err;
parser->size = s.st_size;
parser->data = mmap(NULL, parser->size, PROT_READ, MAP_PRIVATE,
parser->fd, 0);
if (parser->data != MAP_FAILED)
{
parser->off = parser->data;
parser->cur_chunk.type = T_TYPE_INIT;
parser->cur_chunk.s = parser->data;
parser->cur_chunk.e = parser->data;
return parser;
}
err:
template_close(parser);
return NULL;
}
struct template_parser * template_string(const char *str, uint32_t len)
{
struct template_parser *parser;
if (!str) {
errno = EINVAL;
return NULL;
}
if (!(parser = malloc(sizeof(*parser))))
goto err;
memset(parser, 0, sizeof(*parser));
parser->fd = -1;
parser->size = len;
parser->data = (char*)str;
parser->off = parser->data;
parser->cur_chunk.type = T_TYPE_INIT;
parser->cur_chunk.s = parser->data;
parser->cur_chunk.e = parser->data;
return parser;
err:
template_close(parser);
return NULL;
}
void template_close(struct template_parser *parser)
{
if (!parser)
return;
if (parser->gc != NULL)
free(parser->gc);
/* if file is not set, we were parsing a string */
if (parser->file) {
if ((parser->data != NULL) && (parser->data != MAP_FAILED))
munmap(parser->data, parser->size);
if (parser->fd >= 0)
close(parser->fd);
}
free(parser);
}
static void template_text(struct template_parser *parser, const char *e)
{
const char *s = parser->off;
if (s < (parser->data + parser->size))
{
if (parser->strip_after)
{
while ((s <= e) && isspace(*s))
s++;
}
parser->cur_chunk.type = T_TYPE_TEXT;
}
else
{
parser->cur_chunk.type = T_TYPE_EOF;
}
parser->cur_chunk.line = parser->line;
parser->cur_chunk.s = s;
parser->cur_chunk.e = e;
}
static void template_code(struct template_parser *parser, const char *e)
{
const char *s = parser->off;
parser->strip_before = 0;
parser->strip_after = 0;
if (*s == '-')
{
parser->strip_before = 1;
for (s++; (s <= e) && (*s == ' ' || *s == '\t'); s++);
}
if (*(e-1) == '-')
{
parser->strip_after = 1;
for (e--; (e >= s) && (*e == ' ' || *e == '\t'); e--);
}
switch (*s)
{
/* comment */
case '#':
s++;
parser->cur_chunk.type = T_TYPE_COMMENT;
break;
/* include */
case '+':
s++;
parser->cur_chunk.type = T_TYPE_INCLUDE;
break;
/* translate */
case ':':
s++;
parser->cur_chunk.type = T_TYPE_I18N;
break;
/* translate raw */
case '_':
s++;
parser->cur_chunk.type = T_TYPE_I18N_RAW;
break;
/* expr */
case '=':
s++;
parser->cur_chunk.type = T_TYPE_EXPR;
break;
/* code */
default:
parser->cur_chunk.type = T_TYPE_CODE;
break;
}
parser->cur_chunk.line = parser->line;
parser->cur_chunk.s = s;
parser->cur_chunk.e = e;
}
static const char *
template_format_chunk(struct template_parser *parser, size_t *sz)
{
const char *s, *p;
const char *head, *tail;
struct template_chunk *c = &parser->prv_chunk;
struct template_buffer *buf;
*sz = 0;
s = parser->gc = NULL;
if (parser->strip_before && c->type == T_TYPE_TEXT)
{
while ((c->e > c->s) && isspace(*(c->e - 1)))
c->e--;
}
/* empty chunk */
if (c->s == c->e)
{
if (c->type == T_TYPE_EOF)
{
*sz = 0;
s = NULL;
}
else
{
*sz = 1;
s = " ";
}
}
/* format chunk */
else if ((buf = buf_init(c->e - c->s)) != NULL)
{
if ((head = gen_code[c->type][0]) != NULL)
buf_append(buf, head, strlen(head));
switch (c->type)
{
case T_TYPE_TEXT:
luastr_escape(buf, c->s, c->e - c->s, 0);
break;
case T_TYPE_EXPR:
buf_append(buf, c->s, c->e - c->s);
for (p = c->s; p < c->e; p++)
parser->line += (*p == '\n');
break;
case T_TYPE_INCLUDE:
luastr_escape(buf, c->s, c->e - c->s, 0);
break;
case T_TYPE_I18N:
luastr_translate(buf, c->s, c->e - c->s, 1);
break;
case T_TYPE_I18N_RAW:
luastr_translate(buf, c->s, c->e - c->s, 0);
break;
case T_TYPE_CODE:
buf_append(buf, c->s, c->e - c->s);
for (p = c->s; p < c->e; p++)
parser->line += (*p == '\n');
break;
}
if ((tail = gen_code[c->type][1]) != NULL)
buf_append(buf, tail, strlen(tail));
*sz = buf_length(buf);
s = parser->gc = buf_destroy(buf);
if (!*sz)
{
*sz = 1;
s = " ";
}
}
return s;
}
const char *template_reader(lua_State *L, void *ud, size_t *sz)
{
struct template_parser *parser = ud;
int rem = parser->size - (parser->off - parser->data);
char *tag;
parser->prv_chunk = parser->cur_chunk;
/* free previous string */
if (parser->gc)
{
free(parser->gc);
parser->gc = NULL;
}
/* before tag */
if (!parser->in_expr)
{
if ((tag = strfind(parser->off, rem, "<%", 2)) != NULL)
{
template_text(parser, tag);
parser->off = tag + 2;
parser->in_expr = 1;
}
else
{
template_text(parser, parser->data + parser->size);
parser->off = parser->data + parser->size;
}
}
/* inside tag */
else
{
if ((tag = strfind(parser->off, rem, "%>", 2)) != NULL)
{
template_code(parser, tag);
parser->off = tag + 2;
parser->in_expr = 0;
}
else
{
/* unexpected EOF */
template_code(parser, parser->data + parser->size);
*sz = 1;
return "\033";
}
}
return template_format_chunk(parser, sz);
}
int template_error(lua_State *L, struct template_parser *parser)
{
const char *err = luaL_checkstring(L, -1);
const char *off = parser->prv_chunk.s;
const char *ptr;
char msg[1024];
int line = 0;
int chunkline = 0;
if ((ptr = strfind((char *)err, strlen(err), "]:", 2)) != NULL)
{
chunkline = atoi(ptr + 2) - parser->prv_chunk.line;
while (*ptr)
{
if (*ptr++ == ' ')
{
err = ptr;
break;
}
}
}
if (strfind((char *)err, strlen(err), "'char(27)'", 10) != NULL)
{
off = parser->data + parser->size;
err = "'%>' expected before end of file";
chunkline = 0;
}
for (ptr = parser->data; ptr < off; ptr++)
if (*ptr == '\n')
line++;
snprintf(msg, sizeof(msg), "Syntax error in %s:%d: %s",
parser->file ? parser->file : "[string]", line + chunkline, err ? err : "(unknown error)");
lua_pushnil(L);
lua_pushinteger(L, line + chunkline);
lua_pushstring(L, msg);
return 3;
}

View File

@ -0,0 +1,80 @@
/*
* LuCI Template - Parser header
*
* Copyright (C) 2009 Jo-Philipp Wich <jow@openwrt.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef _TEMPLATE_PARSER_H_
#define _TEMPLATE_PARSER_H_
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <string.h>
#include <ctype.h>
#include <errno.h>
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
/* code types */
#define T_TYPE_INIT 0
#define T_TYPE_TEXT 1
#define T_TYPE_COMMENT 2
#define T_TYPE_EXPR 3
#define T_TYPE_INCLUDE 4
#define T_TYPE_I18N 5
#define T_TYPE_I18N_RAW 6
#define T_TYPE_CODE 7
#define T_TYPE_EOF 8
struct template_chunk {
const char *s;
const char *e;
int type;
int line;
};
/* parser state */
struct template_parser {
int fd;
uint32_t size;
char *data;
char *off;
char *gc;
int line;
int in_expr;
int strip_before;
int strip_after;
struct template_chunk prv_chunk;
struct template_chunk cur_chunk;
const char *file;
};
struct template_parser * template_open(const char *file);
struct template_parser * template_string(const char *str, uint32_t len);
void template_close(struct template_parser *parser);
const char *template_reader(lua_State *L, void *ud, size_t *sz);
int template_error(lua_State *L, struct template_parser *parser);
#endif

View File

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

View File

@ -0,0 +1,51 @@
/*
* LuCI Template - Utility header
*
* Copyright (C) 2010-2012 Jo-Philipp Wich <jow@openwrt.org>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef _TEMPLATE_UTILS_H_
#define _TEMPLATE_UTILS_H_
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
/* buffer object */
struct template_buffer {
char *data;
char *dptr;
unsigned int size;
unsigned int fill;
};
struct template_buffer * buf_init(int size);
int buf_append(struct template_buffer *buf, const char *s, int len);
char * buf_destroy(struct template_buffer *buf);
/* read buffer length */
static inline int buf_length(struct template_buffer *buf)
{
return buf->fill;
}
char * pcdata(const char *s, unsigned int l);
void luastr_escape(struct template_buffer *out, const char *s, unsigned int l, int escape_xml);
void luastr_translate(struct template_buffer *out, const char *s, unsigned int l, int escape_xml);
#endif

View File

@ -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))