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:
parent
a06541623e
commit
e4b74be506
128
contrib/i18n-scan.pl
Executable file
128
contrib/i18n-scan.pl
Executable 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;
|
||||
}
|
53
package/gluon-web/Makefile
Normal file
53
package/gluon-web/Makefile
Normal 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))
|
14
package/gluon-web/files/lib/gluon/web/view/csrftoken.html
Normal file
14
package/gluon-web/files/lib/gluon/web/view/csrftoken.html
Normal 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>
|
9
package/gluon-web/files/lib/gluon/web/view/error404.html
Normal file
9
package/gluon-web/files/lib/gluon/web/view/error404.html
Normal 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>
|
9
package/gluon-web/files/lib/gluon/web/view/error500.html
Normal file
9
package/gluon-web/files/lib/gluon/web/view/error500.html
Normal 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>
|
3
package/gluon-web/files/lib/gluon/web/view/layout.html
Normal file
3
package/gluon-web/files/lib/gluon/web/view/layout.html
Normal file
@ -0,0 +1,3 @@
|
||||
<%
|
||||
include("themes/" .. theme .. "/layout")
|
||||
%>
|
@ -0,0 +1,20 @@
|
||||
<div<%=
|
||||
attr("data-prefix", id) ..
|
||||
attr("data-dynlist", {
|
||||
type = self.datatype,
|
||||
optional = self.datatype and self.optional,
|
||||
}) ..
|
||||
attr("data-size", self.size) ..
|
||||
attr("data-placeholder", self.placeholder)
|
||||
%>>
|
||||
<%
|
||||
for i, val in ipairs(self:cfgvalue()) do
|
||||
%>
|
||||
<input class="gluon-input-text" value="<%=pcdata(val)%>" data-update="change" type="text"<%=
|
||||
attr("id", id .. "." .. i) ..
|
||||
attr("name", id) ..
|
||||
attr("size", self.size) ..
|
||||
attr("placeholder", self.placeholder)
|
||||
%> /><br />
|
||||
<% end %>
|
||||
</div>
|
28
package/gluon-web/files/lib/gluon/web/view/model/form.html
Normal file
28
package/gluon-web/files/lib/gluon/web/view/model/form.html
Normal 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>
|
@ -0,0 +1,5 @@
|
||||
<input class="gluon-input-checkbox" data-update="click change" type="checkbox" value="1"<%=
|
||||
attr("id", id) .. attr("name", id) ..
|
||||
attr("checked", self:cfgvalue() and "checked")
|
||||
%> />
|
||||
<label<%= attr("for", id)%>></label>
|
41
package/gluon-web/files/lib/gluon/web/view/model/lvalue.html
Normal file
41
package/gluon-web/files/lib/gluon/web/view/model/lvalue.html
Normal file
@ -0,0 +1,41 @@
|
||||
<%
|
||||
local i, key
|
||||
local br = self.orientation == "horizontal" and ' ' 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 %>
|
@ -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>
|
@ -0,0 +1,3 @@
|
||||
<textarea class="gluon-input-textarea" <% if not self.size then %> style="width: 100%"<% else %> cols="<%=self.size%>"<% end %> data-update="change"<%= attr("name", id) .. attr("id", id) .. attr("rows", self.rows) .. attr("wrap", self.wrap) %>>
|
||||
<%-=pcdata(self:cfgvalue())-%>
|
||||
</textarea>
|
12
package/gluon-web/files/lib/gluon/web/view/model/value.html
Normal file
12
package/gluon-web/files/lib/gluon/web/view/model/value.html
Normal 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)
|
||||
%> />
|
@ -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>
|
@ -0,0 +1,6 @@
|
||||
<%
|
||||
for _, map in ipairs(maps) do
|
||||
map:render(renderer)
|
||||
end
|
||||
%>
|
||||
<script type="text/javascript" src="<%=resource%>/gluon-web.js"></script>
|
8
package/gluon-web/files/lib/gluon/web/www/index.html
Normal file
8
package/gluon-web/files/lib/gluon/web/www/index.html
Normal 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
56
package/gluon-web/i18n/de.po
Normal file
56
package/gluon-web/i18n/de.po
Normal 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 ""
|
54
package/gluon-web/i18n/fr.po
Normal file
54
package/gluon-web/i18n/fr.po
Normal 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 ""
|
45
package/gluon-web/i18n/gluon-web.pot
Normal file
45
package/gluon-web/i18n/gluon-web.pot
Normal 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 ""
|
531
package/gluon-web/javascript/gluon-web.js
Normal file
531
package/gluon-web/javascript/gluon-web.js
Normal 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();
|
||||
})();
|
||||
})();
|
3
package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon
Executable file
3
package/gluon-web/luasrc/lib/gluon/web/www/cgi-bin/gluon
Executable file
@ -0,0 +1,3 @@
|
||||
#!/usr/bin/lua
|
||||
require "gluon.web.cgi"
|
||||
gluon.web.cgi.run()
|
38
package/gluon-web/luasrc/usr/lib/lua/gluon/web/cgi.lua
Normal file
38
package/gluon-web/luasrc/usr/lib/lua/gluon/web/cgi.lua
Normal 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
|
258
package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua
Normal file
258
package/gluon-web/luasrc/usr/lib/lua/gluon/web/dispatcher.lua
Normal 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
|
123
package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua
Normal file
123
package/gluon-web/luasrc/usr/lib/lua/gluon/web/http.lua
Normal 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
|
268
package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua
Normal file
268
package/gluon-web/luasrc/usr/lib/lua/gluon/web/http/protocol.lua
Normal 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
|
466
package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua
Normal file
466
package/gluon-web/luasrc/usr/lib/lua/gluon/web/model.lua
Normal 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
|
@ -0,0 +1,167 @@
|
||||
-- Copyright 2010 Jo-Philipp Wich <jow@openwrt.org>
|
||||
-- Copyright 2017 Matthias Schiffer <mschiffer@universe-factory.net>
|
||||
-- Licensed to the public under the Apache License 2.0.
|
||||
|
||||
local tonumber = tonumber
|
||||
|
||||
|
||||
module "gluon.web.model.datatypes"
|
||||
|
||||
|
||||
function bool(val)
|
||||
if val == "1" or val == "yes" or val == "on" or val == "true" then
|
||||
return true
|
||||
elseif val == "0" or val == "no" or val == "off" or val == "false" then
|
||||
return true
|
||||
elseif val == "" or val == nil then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
local function dec(val)
|
||||
if val:match('^%-?%d*%.?%d+$') then
|
||||
return tonumber(val)
|
||||
end
|
||||
end
|
||||
|
||||
local function int(val)
|
||||
if val:match('^%-?%d+$') then
|
||||
return tonumber(val)
|
||||
end
|
||||
end
|
||||
|
||||
function uinteger(val)
|
||||
local n = int(val)
|
||||
return (n ~= nil and n >= 0)
|
||||
end
|
||||
|
||||
function integer(val)
|
||||
return (int(val) ~= nil)
|
||||
end
|
||||
|
||||
function ufloat(val)
|
||||
local n = dec(val)
|
||||
return (n ~= nil and n >= 0)
|
||||
end
|
||||
|
||||
function float(val)
|
||||
return (dec(val) ~= nil)
|
||||
end
|
||||
|
||||
function ipaddr(val)
|
||||
return ip4addr(val) or ip6addr(val)
|
||||
end
|
||||
|
||||
function ip4addr(val)
|
||||
local g = '(%d%d?%d?)'
|
||||
local v1, v2, v3, v4 = val:match('^'..((g..'%.'):rep(3))..g..'$')
|
||||
local n1, n2, n3, n4 = tonumber(v1), tonumber(v2), tonumber(v3), tonumber(v4)
|
||||
|
||||
if not (n1 and n2 and n3 and n4) then return false end
|
||||
|
||||
return (
|
||||
(n1 >= 0) and (n1 <= 255) and
|
||||
(n2 >= 0) and (n2 <= 255) and
|
||||
(n3 >= 0) and (n3 <= 255) and
|
||||
(n4 >= 0) and (n4 <= 255)
|
||||
)
|
||||
end
|
||||
|
||||
function ip6addr(val)
|
||||
local g1 = '%x%x?%x?%x?'
|
||||
|
||||
if not val:match('::') then
|
||||
return val:match('^'..((g1..':'):rep(7))..g1..'$') ~= nil
|
||||
end
|
||||
|
||||
if
|
||||
val:match(':::') or val:match('::.+::') or
|
||||
val:match('^:[^:]') or val:match('[^:]:$')
|
||||
then
|
||||
return false
|
||||
end
|
||||
|
||||
local g0 = '%x?%x?%x?%x?'
|
||||
for i = 2, 7 do
|
||||
if val:match('^'..((g0..':'):rep(i))..g0..'$') then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if val:match('^'..((g1..':'):rep(7))..':$') then
|
||||
return true
|
||||
end
|
||||
if val:match('^:'..((':'..g1):rep(7))..'$') then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function wpakey(val)
|
||||
if #val == 64 then
|
||||
return (val:match("^%x+$") ~= nil)
|
||||
else
|
||||
return (#val >= 8) and (#val <= 63)
|
||||
end
|
||||
end
|
||||
|
||||
function range(val, vmin, vmax)
|
||||
return min(val, vmin) and max(val, vmax)
|
||||
end
|
||||
|
||||
function min(val, min)
|
||||
val = dec(val)
|
||||
min = tonumber(min)
|
||||
|
||||
if val ~= nil and min ~= nil then
|
||||
return (val >= min)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function max(val, max)
|
||||
val = dec(val)
|
||||
max = tonumber(max)
|
||||
|
||||
if val ~= nil and max ~= nil then
|
||||
return (val <= max)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function irange(val, vmin, vmax)
|
||||
return integer(val) and range(val, vmin, vmax)
|
||||
end
|
||||
|
||||
function imin(val, vmin)
|
||||
return integer(val) and min(val, vmin)
|
||||
end
|
||||
|
||||
function imax(val, vmax)
|
||||
return integer(val) and max(val, vmax)
|
||||
end
|
||||
|
||||
function minlength(val, min)
|
||||
min = tonumber(min)
|
||||
|
||||
if min ~= nil then
|
||||
return (#val >= min)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function maxlength(val, max)
|
||||
max = tonumber(max)
|
||||
|
||||
if max ~= nil then
|
||||
return (#val <= max)
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
92
package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua
Normal file
92
package/gluon-web/luasrc/usr/lib/lua/gluon/web/template.lua
Normal 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
|
100
package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua
Normal file
100
package/gluon-web/luasrc/usr/lib/lua/gluon/web/util.lua
Normal 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
|
16
package/gluon-web/src/Makefile
Normal file
16
package/gluon-web/src/Makefile
Normal 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
|
288
package/gluon-web/src/template_lmo.c
Normal file
288
package/gluon-web/src/template_lmo.c
Normal 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;
|
||||
}
|
81
package/gluon-web/src/template_lmo.h
Normal file
81
package/gluon-web/src/template_lmo.h
Normal 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
|
121
package/gluon-web/src/template_lualib.c
Normal file
121
package/gluon-web/src/template_lualib.c
Normal 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;
|
||||
}
|
30
package/gluon-web/src/template_lualib.h
Normal file
30
package/gluon-web/src/template_lualib.h
Normal 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
|
419
package/gluon-web/src/template_parser.c
Normal file
419
package/gluon-web/src/template_parser.c
Normal 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;
|
||||
}
|
80
package/gluon-web/src/template_parser.h
Normal file
80
package/gluon-web/src/template_parser.h
Normal 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
|
384
package/gluon-web/src/template_utils.c
Normal file
384
package/gluon-web/src/template_utils.c
Normal 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, """, 5);
|
||||
else
|
||||
buf_append(out, "\\\"", 2);
|
||||
break;
|
||||
|
||||
case '\n':
|
||||
buf_append(out, "\\n", 2);
|
||||
break;
|
||||
|
||||
case '\'':
|
||||
case '&':
|
||||
case '<':
|
||||
case '>':
|
||||
if (escape_xml)
|
||||
{
|
||||
esl = snprintf(esq, sizeof(esq), "&#%i;", *ptr);
|
||||
buf_append(out, esq, esl);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
buf_putchar(out, *ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void luastr_translate(struct template_buffer *out, const char *s, unsigned int l, int escape_xml)
|
||||
{
|
||||
char *tr;
|
||||
int trlen;
|
||||
|
||||
if (!lmo_translate(s, l, &tr, &trlen))
|
||||
luastr_escape(out, tr, trlen, escape_xml);
|
||||
else
|
||||
luastr_escape(out, s, l, escape_xml);
|
||||
}
|
51
package/gluon-web/src/template_utils.h
Normal file
51
package/gluon-web/src/template_utils.h
Normal 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
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user