Initial commit gluon-luci-admin
based on freifunk-gluon/packages@9d364a6fbc
This commit is contained in:
parent
638aef092e
commit
7e1713945a
39
gluon-luci-admin/Makefile
Normal file
39
gluon-luci-admin/Makefile
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Copyright (C) 2013 Nils Schneider <nils at nilsschneider.net>
|
||||||
|
# This is free software, licensed under the Apache 2.0 license.
|
||||||
|
|
||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=gluon-luci-admin
|
||||||
|
PKG_VERSION:=0.1
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
|
||||||
|
PKG_BUILD_DIR := $(BUILD_DIR)/$(PKG_NAME)
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
define Package/gluon-luci-admin
|
||||||
|
SECTION:=gluon
|
||||||
|
CATEGORY:=Gluon
|
||||||
|
TITLE:=Luci based simple administration interface for mesh nodes
|
||||||
|
DEPENDS:=+gluon-luci-core +gluon-config-mode
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/gluon-luci-admin/description
|
||||||
|
Luci based config mode
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Prepare
|
||||||
|
mkdir -p $(PKG_BUILD_DIR)
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Configure
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/gluon-luci-admin/install
|
||||||
|
$(CP) ./files/* $(1)/
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,gluon-luci-admin))
|
|
@ -0,0 +1,60 @@
|
||||||
|
--[[
|
||||||
|
LuCI - Lua Configuration Interface
|
||||||
|
|
||||||
|
Copyright 2008 Steven Barth <steven@midlink.org>
|
||||||
|
Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
]]--
|
||||||
|
|
||||||
|
module("luci.controller.admin.index", package.seeall)
|
||||||
|
|
||||||
|
function index()
|
||||||
|
local uci_state = luci.model.uci.cursor_state()
|
||||||
|
local configmode = uci_state:get_first("gluon-config-mode", "wizard", "running", "0") == "1"
|
||||||
|
|
||||||
|
-- Disable gluon-luci-admin when configmode is not enabled
|
||||||
|
if not configmode then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local root = node()
|
||||||
|
if not root.lock then
|
||||||
|
root.target = alias("admin")
|
||||||
|
root.index = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local page = entry({"admin"}, alias("admin", "index"), "Expertmode", 10)
|
||||||
|
page.sysauth = "root"
|
||||||
|
if configmode then
|
||||||
|
-- force root to be logged in when running in configmode
|
||||||
|
page.sysauth_authenticator = function() return "root" end
|
||||||
|
else
|
||||||
|
page.sysauth_authenticator = "htmlauth"
|
||||||
|
end
|
||||||
|
page.index = true
|
||||||
|
|
||||||
|
entry({"admin", "index"}, cbi("admin/remote"), "Remotezugriff", 1).ignoreindex = true
|
||||||
|
|
||||||
|
if not configmode then
|
||||||
|
entry({"admin", "logout"}, call("action_logout"), "Logout")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_logout()
|
||||||
|
local dsp = require "luci.dispatcher"
|
||||||
|
local sauth = require "luci.sauth"
|
||||||
|
if dsp.context.authsession then
|
||||||
|
sauth.kill(dsp.context.authsession)
|
||||||
|
dsp.context.urltoken.stok = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
luci.http.header("Set-Cookie", "sysauth=; path=" .. dsp.build_url())
|
||||||
|
luci.http.redirect(luci.dispatcher.build_url())
|
||||||
|
end
|
|
@ -0,0 +1,155 @@
|
||||||
|
--[[
|
||||||
|
LuCI - Lua Configuration Interface
|
||||||
|
|
||||||
|
Copyright 2008 Steven Barth <steven@midlink.org>
|
||||||
|
Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
]]--
|
||||||
|
|
||||||
|
module("luci.controller.admin.upgrade", package.seeall)
|
||||||
|
|
||||||
|
function index()
|
||||||
|
entry({"admin", "upgrade"}, call("action_upgrade"), "Firmware aktualisieren", 90)
|
||||||
|
end
|
||||||
|
|
||||||
|
function action_upgrade()
|
||||||
|
require("luci.model.uci")
|
||||||
|
|
||||||
|
local tmpfile = "/tmp/firmware.img"
|
||||||
|
|
||||||
|
local function image_supported()
|
||||||
|
-- XXX: yay...
|
||||||
|
return ( 0 == os.execute(
|
||||||
|
". /lib/functions.sh; " ..
|
||||||
|
"include /lib/upgrade; " ..
|
||||||
|
"platform_check_image %q >/dev/null"
|
||||||
|
% tmpfile
|
||||||
|
) )
|
||||||
|
end
|
||||||
|
|
||||||
|
local function image_checksum()
|
||||||
|
return (luci.sys.exec("md5sum %q" % tmpfile):match("^([^%s]+)"))
|
||||||
|
end
|
||||||
|
|
||||||
|
local function storage_size()
|
||||||
|
local size = 0
|
||||||
|
if nixio.fs.access("/proc/mtd") then
|
||||||
|
for l in io.lines("/proc/mtd") do
|
||||||
|
local d, s, e, n = l:match('^([^%s]+)%s+([^%s]+)%s+([^%s]+)%s+"([^%s]+)"')
|
||||||
|
if n == "linux" then
|
||||||
|
size = tonumber(s, 16)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
elseif nixio.fs.access("/proc/partitions") then
|
||||||
|
for l in io.lines("/proc/partitions") do
|
||||||
|
local x, y, b, n = l:match('^%s*(%d+)%s+(%d+)%s+([^%s]+)%s+([^%s]+)')
|
||||||
|
if b and n and not n:match('[0-9]') then
|
||||||
|
size = tonumber(b) * 1024
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return size
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
-- Install upload handler
|
||||||
|
local file
|
||||||
|
luci.http.setfilehandler(
|
||||||
|
function(meta, chunk, eof)
|
||||||
|
if not nixio.fs.access(tmpfile) and not file and chunk and #chunk > 0 then
|
||||||
|
file = io.open(tmpfile, "w")
|
||||||
|
end
|
||||||
|
if file and chunk then
|
||||||
|
file:write(chunk)
|
||||||
|
end
|
||||||
|
if file and eof then
|
||||||
|
file:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
-- Determine state
|
||||||
|
local keep_avail = true
|
||||||
|
local step = tonumber(luci.http.formvalue("step") or 1)
|
||||||
|
local has_image = nixio.fs.access(tmpfile)
|
||||||
|
local has_support = image_supported()
|
||||||
|
local has_platform = nixio.fs.access("/lib/upgrade/platform.sh")
|
||||||
|
local has_upload = luci.http.formvalue("image")
|
||||||
|
|
||||||
|
--
|
||||||
|
-- This is step 1-3, which does the user interaction and
|
||||||
|
-- image upload.
|
||||||
|
--
|
||||||
|
|
||||||
|
-- Step 1: file upload, error on unsupported image format
|
||||||
|
if not has_image or not has_support or step == 1 then
|
||||||
|
-- If there is an image but user has requested step 1
|
||||||
|
-- or type is not supported, then remove it.
|
||||||
|
if has_image then
|
||||||
|
nixio.fs.unlink(tmpfile)
|
||||||
|
end
|
||||||
|
|
||||||
|
luci.template.render("admin/upgrade", {
|
||||||
|
step=1,
|
||||||
|
bad_image=(has_image and not has_support or false),
|
||||||
|
keepavail=keep_avail,
|
||||||
|
supported=has_platform
|
||||||
|
} )
|
||||||
|
|
||||||
|
-- Step 2: present uploaded file, show checksum, confirmation
|
||||||
|
elseif step == 2 then
|
||||||
|
luci.template.render("admin/upgrade", {
|
||||||
|
step=2,
|
||||||
|
checksum=image_checksum(),
|
||||||
|
filesize=nixio.fs.stat(tmpfile).size,
|
||||||
|
flashsize=storage_size(),
|
||||||
|
keepconfig=(keep_avail and luci.http.formvalue("keepcfg") == "1")
|
||||||
|
} )
|
||||||
|
|
||||||
|
-- Step 3: load iframe which calls the actual flash procedure
|
||||||
|
elseif step == 3 then
|
||||||
|
-- invoke sysupgrade
|
||||||
|
local keepcfg = keep_avail and luci.http.formvalue("keepcfg") == "1"
|
||||||
|
fork_exec("/sbin/sysupgrade %s %q" %
|
||||||
|
{ keepcfg and "" or "-n"
|
||||||
|
, tmpfile
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
luci.template.render("admin/upgrade", { step=3 } )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function fork_exec(command)
|
||||||
|
local pid = nixio.fork()
|
||||||
|
if pid > 0 then
|
||||||
|
return
|
||||||
|
elseif pid == 0 then
|
||||||
|
-- change to root dir
|
||||||
|
nixio.chdir("/")
|
||||||
|
|
||||||
|
-- patch stdin, out, err to /dev/null
|
||||||
|
local null = nixio.open("/dev/null", "w+")
|
||||||
|
if null then
|
||||||
|
nixio.dup(null, nixio.stderr)
|
||||||
|
nixio.dup(null, nixio.stdout)
|
||||||
|
nixio.dup(null, nixio.stdin)
|
||||||
|
if null:fileno() > 2 then
|
||||||
|
null:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- replace with target command
|
||||||
|
nixio.exec("/bin/sh", "-c", command)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,89 @@
|
||||||
|
--[[
|
||||||
|
LuCI - Lua Configuration Interface
|
||||||
|
|
||||||
|
Copyright 2008 Steven Barth <steven@midlink.org>
|
||||||
|
Copyright 2011 Jo-Philipp Wich <xm@subsignal.org>
|
||||||
|
Copyright 2013 Nils Schneider <nils@nilsschneider.net>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
]]--
|
||||||
|
|
||||||
|
local fs = require "nixio.fs"
|
||||||
|
|
||||||
|
local m, s, pw1, pw2
|
||||||
|
|
||||||
|
m = Map("system", "Remotezugriff")
|
||||||
|
m.submit = "Speichern"
|
||||||
|
m.reset = "Zurücksetzen"
|
||||||
|
m.pageaction = false
|
||||||
|
m.template = "admin/expertmode"
|
||||||
|
|
||||||
|
if fs.access("/etc/config/dropbear") then
|
||||||
|
s = m:section(TypedSection, "_keys", nil,
|
||||||
|
"Hier hast du die Möglichkeit SSH-Keys (einen pro Zeile) zu hinterlegen:")
|
||||||
|
|
||||||
|
s.addremove = false
|
||||||
|
s.anonymous = true
|
||||||
|
|
||||||
|
function s.cfgsections()
|
||||||
|
return { "_keys" }
|
||||||
|
end
|
||||||
|
|
||||||
|
local keys
|
||||||
|
|
||||||
|
keys = s:option(TextValue, "_data", "")
|
||||||
|
keys.wrap = "off"
|
||||||
|
keys.rows = 5
|
||||||
|
keys.rmempty = false
|
||||||
|
|
||||||
|
function keys.cfgvalue()
|
||||||
|
return fs.readfile("/etc/dropbear/authorized_keys") or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
function keys.write(self, section, value)
|
||||||
|
if value then
|
||||||
|
fs.writefile("/etc/dropbear/authorized_keys", value:gsub("\r\n", "\n"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
s = m:section(TypedSection, "_pass", nil,
|
||||||
|
"Alternativ kannst du auch ein Passwort setzen. Wähle bitte ein sicheres Passwort, das du nirgendswo anders verwendest.")
|
||||||
|
|
||||||
|
s.addremove = false
|
||||||
|
s.anonymous = true
|
||||||
|
|
||||||
|
pw1 = s:option(Value, "pw1", "Passwort")
|
||||||
|
pw1.password = true
|
||||||
|
|
||||||
|
pw2 = s:option(Value, "pw2", "Wiederholung")
|
||||||
|
pw2.password = true
|
||||||
|
|
||||||
|
function s.cfgsections()
|
||||||
|
return { "_pass" }
|
||||||
|
end
|
||||||
|
|
||||||
|
function m.on_commit(map)
|
||||||
|
local v1 = pw1:formvalue("_pass")
|
||||||
|
local v2 = pw2:formvalue("_pass")
|
||||||
|
|
||||||
|
if v1 and v2 and #v1 > 0 and #v2 > 0 then
|
||||||
|
if v1 == v2 then
|
||||||
|
if luci.sys.user.setpasswd(luci.dispatcher.context.authuser, v1) == 0 then
|
||||||
|
m.message = "Passwort geändert."
|
||||||
|
else
|
||||||
|
m.errmessage = "Das Passwort konnte nicht geändert werden."
|
||||||
|
end
|
||||||
|
else
|
||||||
|
m.errmessage = "Die beiden Passwörter stimmen nicht überein."
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return m
|
|
@ -0,0 +1,56 @@
|
||||||
|
<%- if self.message then %>
|
||||||
|
<p class="message success"><%=self.message%></p>
|
||||||
|
<%- end %>
|
||||||
|
<%- if self.errmessage then %>
|
||||||
|
<p class="message error"><%=self.errmessage%></p>
|
||||||
|
<%- end %>
|
||||||
|
<% if not self.embedded then %>
|
||||||
|
<form method="post" enctype="multipart/form-data" action="<%=REQUEST_URI%>">
|
||||||
|
<div>
|
||||||
|
<script type="text/javascript" src="<%=resource%>/cbi.js"></script>
|
||||||
|
<input type="hidden" name="cbi.submit" value="1" />
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<div class="cbi-map" id="cbi-<%=self.config%>">
|
||||||
|
<% if self.title and #self.title > 0 then %><h2><a id="content" name="content"><%=self.title%></a></h2><% end %>
|
||||||
|
<% if self.description and #self.description > 0 then %><div class="cbi-map-descr"><%=self.description%></div><% end %>
|
||||||
|
<% self:render_children() %>
|
||||||
|
</div>
|
||||||
|
<% if not self.embedded then %>
|
||||||
|
<div class="cbi-page-actions">
|
||||||
|
<%-
|
||||||
|
if type(self.hidden) == "table" then
|
||||||
|
for k, v in pairs(self.hidden) do
|
||||||
|
-%>
|
||||||
|
<input type="hidden" id="<%=k%>" name="<%=k%>" value="<%=pcdata(v)%>" />
|
||||||
|
<%-
|
||||||
|
end
|
||||||
|
end
|
||||||
|
%>
|
||||||
|
<% if redirect then %>
|
||||||
|
<div style="float:left">
|
||||||
|
<input class="cbi-button cbi-button-link" type="button" value="<%:Back to Overview%>" onclick="location.href='<%=pcdata(redirect)%>'" />
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
<%- if self.flow and self.flow.skip then %>
|
||||||
|
<input class="cbi-button cbi-button-skip" type="submit" name="cbi.skip" value="<%:Skip%>" />
|
||||||
|
<% end %>
|
||||||
|
<%- if self.submit ~= false then %>
|
||||||
|
<input class="cbi-button cbi-button-save" type="submit" name="cbi.apply" value="
|
||||||
|
<%- if not self.submit then -%><%-:Submit-%><%-else-%><%=self.submit%><%end-%>
|
||||||
|
" />
|
||||||
|
<% end %>
|
||||||
|
<%- if self.reset ~= false then %>
|
||||||
|
<input class="cbi-button cbi-button-reset" type="reset" value="
|
||||||
|
<%- if not self.reset then -%><%-:Reset-%><%-else-%><%=self.reset%><%end-%>
|
||||||
|
" />
|
||||||
|
<% end %>
|
||||||
|
<%- if self.cancel ~= false and self.on_cancel then %>
|
||||||
|
<input class="cbi-button cbi-button-reset" type="submit" name="cbi.cancel" value="
|
||||||
|
<%- if not self.cancel then -%><%-:Cancel-%><%-else-%><%=self.cancel%><%end-%>
|
||||||
|
" />
|
||||||
|
<% end %>
|
||||||
|
<script type="text/javascript">cbi_d_update();</script>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<% end %>
|
105
gluon-luci-admin/files/usr/lib/lua/luci/view/admin/upgrade.htm
Normal file
105
gluon-luci-admin/files/usr/lib/lua/luci/view/admin/upgrade.htm
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<%#
|
||||||
|
LuCI - Lua Configuration Interface
|
||||||
|
Copyright 2008 Steven Barth <steven@midlink.org>
|
||||||
|
Copyright 2008-2009 Jo-Philipp Wich <xm@subsignal.org>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
$Id$
|
||||||
|
|
||||||
|
-%>
|
||||||
|
|
||||||
|
<%+header%>
|
||||||
|
|
||||||
|
<h2>Firmware aktualisieren</h2>
|
||||||
|
|
||||||
|
<% if step == 1 then %>
|
||||||
|
<% if supported then %>
|
||||||
|
<form method="post" action="<%=REQUEST_URI%>" enctype="multipart/form-data">
|
||||||
|
<p>
|
||||||
|
Hier kannst du ein manuelles Firmwareupdate durchführen.
|
||||||
|
</p>
|
||||||
|
<% if bad_image then %>
|
||||||
|
<p class="error">Die übermittelte Firmwaredatei kann nicht verwendet werden.</p>
|
||||||
|
<% end %>
|
||||||
|
<div>
|
||||||
|
<h3>Firmware image</h3>
|
||||||
|
<input type="hidden" name="step" value="2" />
|
||||||
|
<input type="file" size="30" name="image" />
|
||||||
|
<% if keepavail then -%>
|
||||||
|
<br/>
|
||||||
|
<input type="checkbox" name="keepcfg" value="1" checked="checked" />
|
||||||
|
<label for="keepcfg">Einstellungen beibehalten</label>
|
||||||
|
<% end -%>
|
||||||
|
|
||||||
|
<div class="cbi-page-actions right">
|
||||||
|
<input class="cbi-button cbi-button-apply" type="submit" value="Upload image" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<% else %>
|
||||||
|
<p class="error">Auf diesem Gerät kann kein Upgrade durchgeführt werden.
|
||||||
|
Bitte führe das Upgrade manuell durch.</p>
|
||||||
|
<% end %>
|
||||||
|
<% elseif step == 2 then %>
|
||||||
|
<p>
|
||||||
|
Die Firmwaredatei wurde übermittelt. Bitte vergleiche MD5-Checksumme
|
||||||
|
und Dateigröße und klicke anschließend auf "Fortfahren".
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<% if flashsize > 0 and filesize > flashsize then %>
|
||||||
|
<p class="error">Die Firmware passt nicht in den Speicher des Gerätes.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<ul>
|
||||||
|
<li>md5sum: <code><%=checksum%></code></li>
|
||||||
|
<li>Größe: <%
|
||||||
|
function byte_format(byte)
|
||||||
|
local suff = {"B", "KB", "MB", "GB", "TB"}
|
||||||
|
for i=1, 5 do
|
||||||
|
if byte > 1024 and i < 5 then
|
||||||
|
byte = byte / 1024
|
||||||
|
else
|
||||||
|
return string.format("%.2f %s", byte, suff[i])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
write(byte_format(filesize))
|
||||||
|
|
||||||
|
if flashsize > 0 then
|
||||||
|
write(luci.i18n.translatef(
|
||||||
|
" (%s available)",
|
||||||
|
w.byte_format(flashsize)
|
||||||
|
))
|
||||||
|
end
|
||||||
|
%></li>
|
||||||
|
</ul>
|
||||||
|
</p>
|
||||||
|
<div class="cbi-page-actions right">
|
||||||
|
<form style="display:inline">
|
||||||
|
<input type="hidden" name="step" value="3" />
|
||||||
|
<input type="hidden" name="keepcfg" value="<%=keepconfig and "1" or "0"%>" />
|
||||||
|
<input class="cbi-button cbi-button-apply" type="submit" value="Fortfahren" />
|
||||||
|
</form>
|
||||||
|
<form style="display:inline">
|
||||||
|
<input type="hidden" name="step" value="1" />
|
||||||
|
<input type="hidden" name="keepcfg" value="<%=keepconfig and "1" or "0"%>" />
|
||||||
|
<input class="cbi-button cbi-button-reset" type="submit" value="Abbrechen" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<% elseif step == 3 then %>
|
||||||
|
<p>
|
||||||
|
Die Firmware wird jetzt aktualisiert.
|
||||||
|
<strong>UNTERBRECHE AUF KEINEN FALL DIE STROMVERSORGUNG!</strong>
|
||||||
|
Dieser Vorgang wird einige Minuten dauern. Anschließend startet
|
||||||
|
das Gerät automatisch neu.
|
||||||
|
</p>
|
||||||
|
<% end %>
|
||||||
|
<%+footer%>
|
||||||
|
|
Loading…
Reference in a new issue