first commit

main
ben 1 year ago
commit 261246b444

@ -0,0 +1,46 @@
#
# Copyright (C) 2008-2015 The LuCI Team <luci@lists.subsignal.org>
#
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-base
LUCI_TYPE:=mod
LUCI_BASENAME:=base
LUCI_TITLE:=LuCI core libraries
LUCI_DEPENDS:=+lua +luci-lib-nixio +luci-lib-ip +rpcd +libubus-lua +luci-lib-jsonc +liblucihttp-lua +luci-lib-base +rpcd-mod-file +rpcd-mod-luci +cgi-io
PKG_LICENSE:=MIT
HOST_BUILD_DIR:=$(BUILD_DIR_HOST)/$(PKG_NAME)
include $(INCLUDE_DIR)/host-build.mk
define Package/luci-base/conffiles
/etc/luci-uploads
/etc/config/luci
/etc/config/ucitrack
endef
include ../../luci.mk
define Host/Configure
endef
define Host/Compile
$(MAKE) -C src/ clean po2lmo jsmin
endef
define Host/Install
$(INSTALL_DIR) $(1)/bin
$(INSTALL_BIN) src/po2lmo $(1)/bin/po2lmo
$(INSTALL_BIN) src/jsmin $(1)/bin/jsmin
endef
$(eval $(call HostBuild))
# call BuildPackage - OpenWrt buildroot signature

@ -0,0 +1,5 @@
#!/usr/bin/lua
require "luci.cacheloader"
require "luci.sgi.cgi"
luci.dispatcher.indexcache = "/tmp/luci-indexcache"
luci.sgi.cgi.run()

@ -0,0 +1,802 @@
/*
LuCI - Lua Configuration Interface
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008-2018 Jo-Philipp Wich <jo@mein.io>
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
*/
var cbi_d = [];
var cbi_strings = { path: {}, label: {} };
function s8(bytes, off) {
var n = bytes[off];
return (n > 0x7F) ? (n - 256) >>> 0 : n;
}
function u16(bytes, off) {
return ((bytes[off + 1] << 8) + bytes[off]) >>> 0;
}
function sfh(s) {
if (s === null || s.length === 0)
return null;
var bytes = [];
for (var i = 0; i < s.length; i++) {
var ch = s.charCodeAt(i);
if (ch <= 0x7F)
bytes.push(ch);
else if (ch <= 0x7FF)
bytes.push(((ch >>> 6) & 0x1F) | 0xC0,
( ch & 0x3F) | 0x80);
else if (ch <= 0xFFFF)
bytes.push(((ch >>> 12) & 0x0F) | 0xE0,
((ch >>> 6) & 0x3F) | 0x80,
( ch & 0x3F) | 0x80);
else if (code <= 0x10FFFF)
bytes.push(((ch >>> 18) & 0x07) | 0xF0,
((ch >>> 12) & 0x3F) | 0x80,
((ch >> 6) & 0x3F) | 0x80,
( ch & 0x3F) | 0x80);
}
if (!bytes.length)
return null;
var hash = (bytes.length >>> 0),
len = (bytes.length >>> 2),
off = 0, tmp;
while (len--) {
hash += u16(bytes, off);
tmp = ((u16(bytes, off + 2) << 11) ^ hash) >>> 0;
hash = ((hash << 16) ^ tmp) >>> 0;
hash += hash >>> 11;
off += 4;
}
switch ((bytes.length & 3) >>> 0) {
case 3:
hash += u16(bytes, off);
hash = (hash ^ (hash << 16)) >>> 0;
hash = (hash ^ (s8(bytes, off + 2) << 18)) >>> 0;
hash += hash >>> 11;
break;
case 2:
hash += u16(bytes, off);
hash = (hash ^ (hash << 11)) >>> 0;
hash += hash >>> 17;
break;
case 1:
hash += s8(bytes, off);
hash = (hash ^ (hash << 10)) >>> 0;
hash += hash >>> 1;
break;
}
hash = (hash ^ (hash << 3)) >>> 0;
hash += hash >>> 5;
hash = (hash ^ (hash << 4)) >>> 0;
hash += hash >>> 17;
hash = (hash ^ (hash << 25)) >>> 0;
hash += hash >>> 6;
return (0x100000000 + hash).toString(16).substr(1);
}
var plural_function = null;
function trimws(s) {
return String(s).trim().replace(/[ \t\n]+/g, ' ');
}
function _(s, c) {
var k = (c != null ? trimws(c) + '\u0001' : '') + trimws(s);
return (window.TR && TR[sfh(k)]) || s;
}
function N_(n, s, p, c) {
if (plural_function == null && window.TR)
plural_function = new Function('n', (TR['00000000'] || 'plural=(n != 1);') + 'return +plural');
var i = plural_function ? plural_function(n) : (n != 1),
k = (c != null ? trimws(c) + '\u0001' : '') + trimws(s) + '\u0002' + i.toString();
return (window.TR && TR[sfh(k)]) || (i ? p : s);
}
function cbi_d_add(field, dep, index) {
var obj = (typeof(field) === 'string') ? document.getElementById(field) : field;
if (obj) {
var entry
for (var i=0; i<cbi_d.length; i++) {
if (cbi_d[i].id == obj.id) {
entry = cbi_d[i];
break;
}
}
if (!entry) {
entry = {
"node": obj,
"id": obj.id,
"parent": obj.parentNode.id,
"deps": [],
"index": index
};
cbi_d.unshift(entry);
}
entry.deps.push(dep)
}
}
function cbi_d_checkvalue(target, ref) {
var value = null,
query = 'input[id="'+target+'"], input[name="'+target+'"], ' +
'select[id="'+target+'"], select[name="'+target+'"]';
document.querySelectorAll(query).forEach(function(i) {
if (value === null && ((i.type !== 'radio' && i.type !== 'checkbox') || i.checked === true))
value = i.value;
});
return (((value !== null) ? value : "") == ref);
}
function cbi_d_check(deps) {
var reverse;
var def = false;
for (var i=0; i<deps.length; i++) {
var istat = true;
reverse = false;
for (var j in deps[i]) {
if (j == "!reverse") {
reverse = true;
} else if (j == "!default") {
def = true;
istat = false;
} else {
istat = (istat && cbi_d_checkvalue(j, deps[i][j]))
}
}
if (istat ^ reverse) {
return true;
}
}
return def;
}
function cbi_d_update() {
var state = false;
for (var i=0; i<cbi_d.length; i++) {
var entry = cbi_d[i];
var node = document.getElementById(entry.id);
var parent = document.getElementById(entry.parent);
if (node && node.parentNode && !cbi_d_check(entry.deps)) {
node.parentNode.removeChild(node);
state = true;
}
else if (parent && (!node || !node.parentNode) && cbi_d_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 (entry && entry.parent)
cbi_tag_last(parent);
if (state)
cbi_d_update();
else if (parent)
parent.dispatchEvent(new CustomEvent('dependency-update', { bubbles: true }));
}
function cbi_init() {
var nodes;
document.querySelectorAll('.cbi-dropdown').forEach(function(node) {
cbi_dropdown_init(node);
node.addEventListener('cbi-dropdown-change', cbi_d_update);
});
nodes = document.querySelectorAll('[data-strings]');
for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
var str = JSON.parse(node.getAttribute('data-strings'));
for (var key in str) {
for (var key2 in str[key]) {
var dst = cbi_strings[key] || (cbi_strings[key] = { });
dst[key2] = str[key][key2];
}
}
}
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++)
cbi_d_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++)
node.addEventListener(event, cbi_d_update);
}
nodes = document.querySelectorAll('[data-choices]');
for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
var choices = JSON.parse(node.getAttribute('data-choices')),
options = {};
for (var j = 0; j < choices[0].length; j++)
options[choices[0][j]] = choices[1][j];
var def = (node.getAttribute('data-optional') === 'true')
? node.placeholder || '' : null;
var cb = new L.ui.Combobox(node.value, options, {
name: node.getAttribute('name'),
sort: choices[0],
select_placeholder: def || _('-- Please choose --'),
custom_placeholder: node.getAttribute('data-manual') || _('-- custom --')
});
var n = cb.render();
n.addEventListener('cbi-dropdown-change', cbi_d_update);
node.parentNode.replaceChild(n, node);
}
nodes = document.querySelectorAll('[data-dynlist]');
for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
var choices = JSON.parse(node.getAttribute('data-dynlist')),
values = JSON.parse(node.getAttribute('data-values') || '[]'),
options = null;
if (choices[0] && choices[0].length) {
options = {};
for (var j = 0; j < choices[0].length; j++)
options[choices[0][j]] = choices[1][j];
}
var dl = new L.ui.DynamicList(values, options, {
name: node.getAttribute('data-prefix'),
sort: choices[0],
datatype: choices[2],
optional: choices[3],
placeholder: node.getAttribute('data-placeholder')
});
var n = dl.render();
n.addEventListener('cbi-dynlist-change', cbi_d_update);
node.parentNode.replaceChild(n, node);
}
nodes = document.querySelectorAll('[data-type]');
for (var i = 0, node; (node = nodes[i]) !== undefined; i++) {
cbi_validate_field(node, node.getAttribute('data-optional') === 'true',
node.getAttribute('data-type'));
}
document.querySelectorAll('.cbi-tooltip:not(:empty)').forEach(function(s) {
s.parentNode.classList.add('cbi-tooltip-container');
});
document.querySelectorAll('.cbi-section-remove > input[name^="cbi.rts"]').forEach(function(i) {
var handler = function(ev) {
var bits = this.name.split(/\./),
section = document.getElementById('cbi-' + bits[2] + '-' + bits[3]);
section.style.opacity = (ev.type === 'mouseover') ? 0.5 : '';
};
i.addEventListener('mouseover', handler);
i.addEventListener('mouseout', handler);
});
var tasks = [];
document.querySelectorAll('[data-ui-widget]').forEach(function(node) {
var args = JSON.parse(node.getAttribute('data-ui-widget') || '[]'),
widget = new (Function.prototype.bind.apply(L.ui[args[0]], args)),
markup = widget.render();
tasks.push(Promise.resolve(markup).then(function(markup) {
markup.addEventListener('widget-change', cbi_d_update);
node.parentNode.replaceChild(markup, node);
}));
});
Promise.all(tasks).then(cbi_d_update);
}
function cbi_validate_form(form, errmsg)
{
/* if triggered by a section removal or addition, don't validate */
if (form.cbi_state == 'add-section' || form.cbi_state == 'del-section')
return true;
if (form.cbi_validators) {
for (var i = 0; i < form.cbi_validators.length; i++) {
var validator = form.cbi_validators[i];
if (!validator() && errmsg) {
alert(errmsg);
return false;
}
}
}
return true;
}
function cbi_validate_named_section_add(input)
{
var button = input.parentNode.parentNode.querySelector('.cbi-button-add');
if (input.value !== '') {
button.disabled = false;
}
else {
button.disabled = true;
}
}
function cbi_validate_reset(form)
{
window.setTimeout(
function() { cbi_validate_form(form, null) }, 100
);
return true;
}
function cbi_validate_field(cbid, optional, type)
{
var field = isElem(cbid) ? cbid : document.getElementById(cbid);
var validatorFn;
try {
var cbiValidator = L.validation.create(field, type, optional);
validatorFn = cbiValidator.validate.bind(cbiValidator);
}
catch(e) {
validatorFn = null;
};
if (validatorFn !== null) {
var form = findParent(field, 'form');
if (!form.cbi_validators)
form.cbi_validators = [ ];
form.cbi_validators.push(validatorFn);
field.addEventListener("blur", validatorFn);
field.addEventListener("keyup", validatorFn);
field.addEventListener("cbi-dropdown-change", validatorFn);
if (matchesElem(field, 'select')) {
field.addEventListener("change", validatorFn);
field.addEventListener("click", validatorFn);
}
validatorFn();
}
}
function cbi_row_swap(elem, up, store)
{
var tr = findParent(elem.parentNode, '.cbi-section-table-row');
if (!tr)
return false;
tr.classList.remove('flash');
if (up) {
var prev = tr.previousElementSibling;
if (prev && prev.classList.contains('cbi-section-table-row'))
tr.parentNode.insertBefore(tr, prev);
else
return;
}
else {
var next = tr.nextElementSibling ? tr.nextElementSibling.nextElementSibling : null;
if (next && next.classList.contains('cbi-section-table-row'))
tr.parentNode.insertBefore(tr, next);
else if (!next)
tr.parentNode.appendChild(tr);
else
return;
}
var ids = [ ];
for (var i = 0, n = 0; i < tr.parentNode.childNodes.length; i++) {
var node = tr.parentNode.childNodes[i];
if (node.classList && node.classList.contains('cbi-section-table-row')) {
node.classList.remove('cbi-rowstyle-1');
node.classList.remove('cbi-rowstyle-2');
node.classList.add((n++ % 2) ? 'cbi-rowstyle-2' : 'cbi-rowstyle-1');
if (/-([^\-]+)$/.test(node.id))
ids.push(RegExp.$1);
}
}
var input = document.getElementById(store);
if (input)
input.value = ids.join(' ');
window.scrollTo(0, tr.offsetTop);
void tr.offsetWidth;
tr.classList.add('flash');
return false;
}
function cbi_tag_last(container)
{
var last;
for (var i = 0; i < container.childNodes.length; i++) {
var c = container.childNodes[i];
if (matchesElem(c, 'div')) {
c.classList.remove('cbi-value-last');
last = c;
}
}
if (last)
last.classList.add('cbi-value-last');
}
function cbi_submit(elem, name, value, action)
{
var form = elem.form || findParent(elem, 'form');
if (!form)
return false;
if (action)
form.action = action;
if (name) {
var hidden = form.querySelector('input[type="hidden"][name="%s"]'.format(name)) ||
E('input', { type: 'hidden', name: name });
hidden.value = value || '1';
form.appendChild(hidden);
}
form.submit();
return true;
}
String.prototype.format = function()
{
if (!RegExp)
return;
var html_esc = [/&/g, '&#38;', /"/g, '&#34;', /'/g, '&#39;', /</g, '&#60;', />/g, '&#62;'];
var quot_esc = [/"/g, '&#34;', /'/g, '&#39;'];
function esc(s, r) {
var t = typeof(s);
if (s == null || t === 'object' || t === 'function')
return '';
if (t !== 'string')
s = String(s);
for (var i = 0; i < r.length; i += 2)
s = s.replace(r[i], r[i+1]);
return s;
}
var str = this;
var out = '';
var re = /^(([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X|q|h|j|t|m))/;
var a = b = [], numSubstitutions = 0, numMatches = 0;
while (a = re.exec(str)) {
var m = a[1];
var leftpart = a[2], pPad = a[3], pJustify = a[4], pMinLength = a[5];
var pPrecision = a[6], pType = a[7];
numMatches++;
if (pType == '%') {
subst = '%';
}
else {
if (numSubstitutions < arguments.length) {
var param = arguments[numSubstitutions++];
var pad = '';
if (pPad && pPad.substr(0,1) == "'")
pad = leftpart.substr(1,1);
else if (pPad)
pad = pPad;
else
pad = ' ';
var justifyRight = true;
if (pJustify && pJustify === "-")
justifyRight = false;
var minLength = -1;
if (pMinLength)
minLength = +pMinLength;
var precision = -1;
if (pPrecision && pType == 'f')
precision = +pPrecision.substring(1);
var subst = param;
switch(pType) {
case 'b':
subst = Math.floor(+param || 0).toString(2);
break;
case 'c':
subst = String.fromCharCode(+param || 0);
break;
case 'd':
subst = Math.floor(+param || 0).toFixed(0);
break;
case 'u':
var n = +param || 0;
subst = Math.floor((n < 0) ? 0x100000000 + n : n).toFixed(0);
break;
case 'f':
subst = (precision > -1)
? ((+param || 0.0)).toFixed(precision)
: (+param || 0.0);
break;
case 'o':
subst = Math.floor(+param || 0).toString(8);
break;
case 's':
subst = param;
break;
case 'x':
subst = Math.floor(+param || 0).toString(16).toLowerCase();
break;
case 'X':
subst = Math.floor(+param || 0).toString(16).toUpperCase();
break;
case 'h':
subst = esc(param, html_esc);
break;
case 'q':
subst = esc(param, quot_esc);
break;
case 't':
var td = 0;
var th = 0;
var tm = 0;
var ts = (param || 0);
if (ts > 59) {
tm = Math.floor(ts / 60);
ts = (ts % 60);
}
if (tm > 59) {
th = Math.floor(tm / 60);
tm = (tm % 60);
}
if (th > 23) {
td = Math.floor(th / 24);
th = (th % 24);
}
subst = (td > 0)
? String.format('%dd %dh %dm %ds', td, th, tm, ts)
: String.format('%dh %dm %ds', th, tm, ts);
break;
case 'm':
var mf = pMinLength ? +pMinLength : 1000;
var pr = pPrecision ? ~~(10 * +('0' + pPrecision)) : 2;
var i = 0;
var val = (+param || 0);
var units = [ ' ', ' K', ' M', ' G', ' T', ' P', ' E' ];
for (i = 0; (i < units.length) && (val > mf); i++)
val /= mf;
if (i)
subst = val.toFixed(pr) + units[i] + (mf == 1024 ? 'i' : '');
else
subst = val + ' ';
pMinLength = null;
break;
}
}
}
if (pMinLength) {
subst = subst.toString();
for (var i = subst.length; i < pMinLength; i++)
if (pJustify == '-')
subst = subst + ' ';
else
subst = pad + subst;
}
out += leftpart + subst;
str = str.substr(m.length);
}
return out + str;
}
String.prototype.nobr = function()
{
return this.replace(/[\s\n]+/g, '&#160;');
}
String.format = function()
{
var a = [ ];
for (var i = 1; i < arguments.length; i++)
a.push(arguments[i]);
return ''.format.apply(arguments[0], a);
}
String.nobr = function()
{
var a = [ ];
for (var i = 1; i < arguments.length; i++)
a.push(arguments[i]);
return ''.nobr.apply(arguments[0], a);
}
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = function (callback, thisArg) {
thisArg = thisArg || window;
for (var i = 0; i < this.length; i++) {
callback.call(thisArg, this[i], i, this);
}
};
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(f) {
window.setTimeout(function() {
f(new Date().getTime())
}, 1000/30);
};
}
function isElem(e) { return L.dom.elem(e) }
function toElem(s) { return L.dom.parse(s) }
function matchesElem(node, selector) { return L.dom.matches(node, selector) }
function findParent(node, selector) { return L.dom.parent(node, selector) }
function E() { return L.dom.create.apply(L.dom, arguments) }
if (typeof(window.CustomEvent) !== 'function') {
function CustomEvent(event, params) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
}
function cbi_dropdown_init(sb) {
if (sb && L.dom.findClassInstance(sb) instanceof L.ui.Dropdown)
return;
var dl = new L.ui.Dropdown(sb, null, { name: sb.getAttribute('name') });
return dl.bind(sb);
}
function cbi_update_table(table, data, placeholder) {
var target = isElem(table) ? table : document.querySelector(table);
if (!isElem(target))
return;
var t = L.dom.findClassInstance(target);
if (!(t instanceof L.ui.Table)) {
t = new L.ui.Table(target);
L.dom.bindClassInstance(target, t);
}
t.update(data, placeholder);
}
function showModal(title, children)
{
return L.showModal(title, children);
}
function hideModal()
{
return L.hideModal();
}
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('validation-failure', function(ev) {
if (ev.target === document.activeElement)
L.showTooltip(ev);
});
document.addEventListener('validation-success', function(ev) {
if (ev.target === document.activeElement)
L.hideTooltip(ev);
});
L.require('ui').then(function(ui) {
document.querySelectorAll('.table').forEach(cbi_update_table);
});
});

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<defs>
<radialGradient id="a" cx="0" cy="1" r="1">
<stop offset="0" stop-color="#89b"/>
<stop offset="1" stop-color="#def"/>
</radialGradient>
</defs>
<g stroke="#000" stroke-width="2">
<path d="M9 2h23l2 1 9 9 1 2v31l-2 1H9l-1-1V4l1-2z" fill="url(#a)"/>
<path d="M32 2v11l1 1h11" fill="url(#a)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 389 B

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<defs>
<radialGradient id="a" cx="0" cy="0" r="1">
<stop offset="0" stop-color="#ddf"/>
<stop offset="1" stop-color="#46b"/>
</radialGradient>
</defs>
<g stroke="#024" stroke-width="2">
<path d="M44 14l1 2v25l-1 1H4l-1-1V18v0l1-6 1-2h17l2 2v1l1 1z" fill="url(#a)"/>
<path d="M3 18h21l1-1v-3" fill="none"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 415 B

@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
<defs>
<radialGradient id="a" cx="0" cy="1" r="1">
<stop offset="0" stop-color="#89b"/>
<stop offset="1" stop-color="#def"/>
</radialGradient>
</defs>
<g stroke="#000" stroke-width="2">
<path d="M9 2h23l2 1 9 9 1 2v31l-2 1H9l-1-1V4l1-2z" fill="url(#a)"/>
<path d="M32 2v11l1 1h11" fill="url(#a)"/>
</g>
<path d="M13 33h13v6l12-10-12-11v7H13z" fill="#035"/>
</svg>

After

Width:  |  Height:  |  Size: 444 B

@ -0,0 +1,560 @@
'use strict';
'require uci';
'require rpc';
'require tools.prng as random';
function initFirewallState() {
return L.resolveDefault(uci.load('firewall'));
}
function parseEnum(s, values) {
if (s == null)
return null;
s = String(s).toUpperCase();
if (s == '')
return null;
for (var i = 0; i < values.length; i++)
if (values[i].toUpperCase().indexOf(s) == 0)
return values[i];
return null;
}
function parsePolicy(s, defaultValue) {
return parseEnum(s, ['DROP', 'REJECT', 'ACCEPT']) || (arguments.length < 2 ? null : defaultValue);
}
var Firewall, AbstractFirewallItem, Defaults, Zone, Forwarding, Redirect, Rule;
function lookupZone(name) {
var z = uci.get('firewall', name);
if (z != null && z['.type'] == 'zone')
return new Zone(z['.name']);
var sections = uci.sections('firewall', 'zone');
for (var i = 0; i < sections.length; i++) {
if (sections[i].name != name)
continue;
return new Zone(sections[i]['.name']);
}
return null;
}
function getColorForName(forName) {
if (forName == null)
return '#eeeeee';
else if (forName == 'lan')
return '#90f090';
else if (forName == 'wan')
return '#f09090';
return random.derive_color(forName);
}
Firewall = L.Class.extend({
getDefaults: function() {
return initFirewallState().then(function() {
return new Defaults();
});
},
newZone: function() {
return initFirewallState().then(L.bind(function() {
var name = 'newzone',
count = 1;
while (this.getZone(name) != null)
name = 'newzone%d'.format(++count);
return this.addZone(name);
}, this));
},
addZone: function(name) {
return initFirewallState().then(L.bind(function() {
if (name == null || !/^[a-zA-Z0-9_]+$/.test(name))
return null;
if (lookupZone(name) != null)
return null;
var d = new Defaults(),
z = uci.add('firewall', 'zone');
uci.set('firewall', z, 'name', name);
uci.set('firewall', z, 'input', d.getInput() || 'DROP');
uci.set('firewall', z, 'output', d.getOutput() || 'DROP');
uci.set('firewall', z, 'forward', d.getForward() || 'DROP');
return new Zone(z);
}, this));
},
getZone: function(name) {
return initFirewallState().then(function() {
return lookupZone(name);
});
},
getZones: function() {
return initFirewallState().then(function() {
var sections = uci.sections('firewall', 'zone'),
zones = [];
for (var i = 0; i < sections.length; i++)
zones.push(new Zone(sections[i]['.name']));
zones.sort(function(a, b) { return a.getName() > b.getName() });
return zones;
});
},
getZoneByNetwork: function(network) {
return initFirewallState().then(function() {
var sections = uci.sections('firewall', 'zone');
for (var i = 0; i < sections.length; i++)
if (L.toArray(sections[i].network).indexOf(network) != -1)
return new Zone(sections[i]['.name']);
return null;
});
},
deleteZone: function(name) {
return initFirewallState().then(function() {
var section = uci.get('firewall', name),
found = false;
if (section != null && section['.type'] == 'zone') {
found = true;
name = section.name;
uci.remove('firewall', section['.name']);
}
else if (name != null) {
var sections = uci.sections('firewall', 'zone');
for (var i = 0; i < sections.length; i++) {
if (sections[i].name != name)
continue;
found = true;
uci.remove('firewall', sections[i]['.name']);
}
}
if (found == true) {
sections = uci.sections('firewall');
for (var i = 0; i < sections.length; i++) {
if (sections[i]['.type'] != 'rule' &&
sections[i]['.type'] != 'redirect' &&
sections[i]['.type'] != 'forwarding')
continue;
if (sections[i].src == name || sections[i].dest == name)
uci.remove('firewall', sections[i]['.name']);
}
}
return found;
});
},
renameZone: function(oldName, newName) {
return initFirewallState().then(L.bind(function() {
if (oldName == null || newName == null || !/^[a-zA-Z0-9_]+$/.test(newName))
return false;
if (lookupZone(newName) != null)
return false;
var sections = uci.sections('firewall', 'zone'),
found = false;
for (var i = 0; i < sections.length; i++) {
if (sections[i].name != oldName)
continue;
uci.set('firewall', sections[i]['.name'], 'name', newName);
found = true;
}
if (found == true) {
sections = uci.sections('firewall');
for (var i = 0; i < sections.length; i++) {
if (sections[i]['.type'] != 'rule' &&
sections[i]['.type'] != 'redirect' &&
sections[i]['.type'] != 'forwarding')
continue;
if (sections[i].src == oldName)
uci.set('firewall', sections[i]['.name'], 'src', newName);
if (sections[i].dest == oldName)
uci.set('firewall', sections[i]['.name'], 'dest', newName);
}
}
return found;
}, this));
},
deleteNetwork: function(network) {
return this.getZones().then(L.bind(function(zones) {
var rv = false;
for (var i = 0; i < zones.length; i++)
if (zones[i].deleteNetwork(network))
rv = true;
return rv;
}, this));
},
getColorForName: getColorForName,
getZoneColorStyle: function(zone) {
var hex = (zone instanceof Zone) ? zone.getColor() : getColorForName((zone != null && zone != '*') ? zone : null);
return '--zone-color-rgb:%d, %d, %d; background-color:rgb(var(--zone-color-rgb))'.format(
parseInt(hex.substring(1, 3), 16),
parseInt(hex.substring(3, 5), 16),
parseInt(hex.substring(5, 7), 16)
);
},
});
AbstractFirewallItem = L.Class.extend({
get: function(option) {
return uci.get('firewall', this.sid, option);
},
set: function(option, value) {
return uci.set('firewall', this.sid, option, value);
}
});
Defaults = AbstractFirewallItem.extend({
__init__: function() {
var sections = uci.sections('firewall', 'defaults');
for (var i = 0; i < sections.length; i++) {
this.sid = sections[i]['.name'];
break;
}
if (this.sid == null)
this.sid = uci.add('firewall', 'defaults');
},
isSynFlood: function() {
return (this.get('syn_flood') == '1');
},
isDropInvalid: function() {
return (this.get('drop_invalid') == '1');
},
getInput: function() {
return parsePolicy(this.get('input'), 'DROP');
},
getOutput: function() {
return parsePolicy(this.get('output'), 'DROP');
},
getForward: function() {
return parsePolicy(this.get('forward'), 'DROP');
}
});
Zone = AbstractFirewallItem.extend({
__init__: function(name) {
var section = uci.get('firewall', name);
if (section != null && section['.type'] == 'zone') {
this.sid = name;
this.data = section;
}
else if (name != null) {
var sections = uci.get('firewall', 'zone');
for (var i = 0; i < sections.length; i++) {
if (sections[i].name != name)
continue;
this.sid = sections[i]['.name'];
this.data = sections[i];
break;
}
}
},
isMasquerade: function() {
return (this.get('masq') == '1');
},
getName: function() {
return this.get('name');
},
getNetwork: function() {
return this.get('network');
},
getInput: function() {
return parsePolicy(this.get('input'), (new Defaults()).getInput());
},
getOutput: function() {
return parsePolicy(this.get('output'), (new Defaults()).getOutput());
},
getForward: function() {
return parsePolicy(this.get('forward'), (new Defaults()).getForward());
},
addNetwork: function(network) {
var section = uci.get('network', network);
if (section == null || section['.type'] != 'interface')
return false;
var newNetworks = this.getNetworks();
if (newNetworks.filter(function(net) { return net == network }).length)
return false;
newNetworks.push(network);
this.set('network', newNetworks);
return true;
},
deleteNetwork: function(network) {
var oldNetworks = this.getNetworks(),
newNetworks = oldNetworks.filter(function(net) { return net != network });
if (newNetworks.length > 0)
this.set('network', newNetworks);
else
this.set('network', null);
return (newNetworks.length < oldNetworks.length);
},
getNetworks: function() {
return L.toArray(this.get('network'));
},
clearNetworks: function() {
this.set('network', null);
},
getDevices: function() {
return L.toArray(this.get('device'));
},
getSubnets: function() {
return L.toArray(this.get('subnet'));
},
getForwardingsBy: function(what) {
var sections = uci.sections('firewall', 'forwarding'),
forwards = [];
for (var i = 0; i < sections.length; i++) {
if (sections[i].src == null || sections[i].dest == null)
continue;
if (sections[i][what] != this.getName())
continue;
forwards.push(new Forwarding(sections[i]['.name']));
}
return forwards;
},
addForwardingTo: function(dest) {
var forwards = this.getForwardingsBy('src'),
zone = lookupZone(dest);
if (zone == null || zone.getName() == this.getName())
return null;
for (var i = 0; i < forwards.length; i++)
if (forwards[i].getDestination() == zone.getName())
return null;
var sid = uci.add('firewall', 'forwarding');
uci.set('firewall', sid, 'src', this.getName());
uci.set('firewall', sid, 'dest', zone.getName());
return new Forwarding(sid);
},
addForwardingFrom: function(src) {
var forwards = this.getForwardingsBy('dest'),
zone = lookupZone(src);
if (zone == null || zone.getName() == this.getName())
return null;
for (var i = 0; i < forwards.length; i++)
if (forwards[i].getSource() == zone.getName())
return null;
var sid = uci.add('firewall', 'forwarding');
uci.set('firewall', sid, 'src', zone.getName());
uci.set('firewall', sid, 'dest', this.getName());
return new Forwarding(sid);
},
deleteForwardingsBy: function(what) {
var sections = uci.sections('firewall', 'forwarding'),
found = false;
for (var i = 0; i < sections.length; i++) {
if (sections[i].src == null || sections[i].dest == null)
continue;
if (sections[i][what] != this.getName())
continue;
uci.remove('firewall', sections[i]['.name']);
found = true;
}
return found;
},
deleteForwarding: function(forwarding) {
if (!(forwarding instanceof Forwarding))
return false;
var section = uci.get('firewall', forwarding.sid);
if (!section || section['.type'] != 'forwarding')
return false;
uci.remove('firewall', section['.name']);
return true;
},
addRedirect: function(options) {
var sid = uci.add('firewall', 'redirect');
if (options != null && typeof(options) == 'object')
for (var key in options)
if (options.hasOwnProperty(key))
uci.set('firewall', sid, key, options[key]);
uci.set('firewall', sid, 'src', this.getName());
return new Redirect(sid);
},
addRule: function(options) {
var sid = uci.add('firewall', 'rule');
if (options != null && typeof(options) == 'object')
for (var key in options)
if (options.hasOwnProperty(key))
uci.set('firewall', sid, key, options[key]);
uci.set('firewall', sid, 'src', this.getName());
return new Rule(sid);
},
getColor: function(forName) {
var name = (arguments.length > 0 ? forName : this.getName());
return getColorForName(name);
}
});
Forwarding = AbstractFirewallItem.extend({
__init__: function(sid) {
this.sid = sid;
},
getSource: function() {
return this.get('src');
},
getDestination: function() {
return this.get('dest');
},
getSourceZone: function() {
return lookupZone(this.getSource());
},
getDestinationZone: function() {
return lookupZone(this.getDestination());
}
});
Rule = AbstractFirewallItem.extend({
getSource: function() {
return this.get('src');
},
getDestination: function() {
return this.get('dest');
},
getSourceZone: function() {
return lookupZone(this.getSource());
},
getDestinationZone: function() {
return lookupZone(this.getDestination());
}
});
Redirect = AbstractFirewallItem.extend({
getSource: function() {
return this.get('src');
},
getDestination: function() {
return this.get('dest');
},
getSourceZone: function() {
return lookupZone(this.getSource());
},
getDestinationZone: function() {
return lookupZone(this.getDestination());
}
});
return Firewall;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,429 @@
'use strict';
'require rpc';
'require request';
'require baseclass';
/**
* @typedef {Object} FileStatEntry
* @memberof LuCI.fs
* @property {string} name - Name of the directory entry
* @property {string} type - Type of the entry, one of `block`, `char`, `directory`, `fifo`, `symlink`, `file`, `socket` or `unknown`
* @property {number} size - Size in bytes
* @property {number} mode - Access permissions
* @property {number} atime - Last access time in seconds since epoch
* @property {number} mtime - Last modification time in seconds since epoch
* @property {number} ctime - Last change time in seconds since epoch
* @property {number} inode - Inode number
* @property {number} uid - Numeric owner id
* @property {number} gid - Numeric group id
*/
/**
* @typedef {Object} FileExecResult
* @memberof LuCI.fs
*
* @property {number} code - The exit code of the invoked command
* @property {string} [stdout] - The stdout produced by the command, if any
* @property {string} [stderr] - The stderr produced by the command, if any
*/
var callFileList, callFileStat, callFileRead, callFileWrite, callFileRemove,
callFileExec, callFileMD5;
callFileList = rpc.declare({
object: 'file',
method: 'list',
params: [ 'path' ]
});
callFileStat = rpc.declare({
object: 'file',
method: 'stat',
params: [ 'path' ]
});
callFileRead = rpc.declare({
object: 'file',
method: 'read',
params: [ 'path' ]
});
callFileWrite = rpc.declare({
object: 'file',
method: 'write',
params: [ 'path', 'data', 'mode' ]
});
callFileRemove = rpc.declare({
object: 'file',
method: 'remove',
params: [ 'path' ]
});
callFileExec = rpc.declare({
object: 'file',
method: 'exec',
params: [ 'command', 'params', 'env' ]
});
callFileMD5 = rpc.declare({
object: 'file',
method: 'md5',
params: [ 'path' ]
});
var rpcErrors = [
null,
'InvalidCommandError',
'InvalidArgumentError',
'MethodNotFoundError',
'NotFoundError',
'NoDataError',
'PermissionError',
'TimeoutError',
'UnsupportedError'
];
function handleRpcReply(expect, rc) {
if (typeof(rc) == 'number' && rc != 0) {
var e = new Error(rpc.getStatusText(rc)); e.name = rpcErrors[rc] || 'Error';
throw e;
}
if (expect) {
var type = Object.prototype.toString;
for (var key in expect) {
if (rc != null && key != '')
rc = rc[key];
if (rc == null || type.call(rc) != type.call(expect[key])) {
var e = new Error(_('Unexpected reply data format')); e.name = 'TypeError';
throw e;
}
break;
}
}
return rc;
}
function handleCgiIoReply(res) {
if (!res.ok || res.status != 200) {
var e = new Error(res.statusText);
switch (res.status) {
case 400:
e.name = 'InvalidArgumentError';
break;
case 403:
e.name = 'PermissionError';
break;
case 404:
e.name = 'NotFoundError';
break;
default:
e.name = 'Error';
}
throw e;
}
switch (this.type) {
case 'blob':
return res.blob();
case 'json':
return res.json();
default:
return res.text();
}
}
/**
* @class fs
* @memberof LuCI
* @hideconstructor
* @classdesc
*
* Provides high level utilities to wrap file system related RPC calls.
* To import the class in views, use `'require fs'`, to import it in
* external JavaScript, use `L.require("fs").then(...)`.
*/
var FileSystem = baseclass.extend(/** @lends LuCI.fs.prototype */ {
/**
* Obtains a listing of the specified directory.
*
* @param {string} path
* The directory path to list.
*
* @returns {Promise<LuCI.fs.FileStatEntry[]>}
* Returns a promise resolving to an array of stat detail objects or
* rejecting with an error stating the failure reason.
*/
list: function(path) {
return callFileList(path).then(handleRpcReply.bind(this, { entries: [] }));
},
/**
* Return file stat information on the specified path.
*
* @param {string} path
* The filesystem path to stat.
*
* @returns {Promise<LuCI.fs.FileStatEntry>}
* Returns a promise resolving to a stat detail object or
* rejecting with an error stating the failure reason.
*/
stat: function(path) {
return callFileStat(path).then(handleRpcReply.bind(this, { '': {} }));
},
/**
* Read the contents of the given file and return them.
* Note: this function is unsuitable for obtaining binary data.
*
* @param {string} path
* The file path to read.
*
* @returns {Promise<string>}
* Returns a promise resolving to a string containing the file contents or
* rejecting with an error stating the failure reason.
*/
read: function(path) {
return callFileRead(path).then(handleRpcReply.bind(this, { data: '' }));
},
/**
* Write the given data to the specified file path.
* If the specified file path does not exist, it will be created, given
* sufficient permissions.
*
* Note: `data` will be converted to a string using `String(data)` or to
* `''` when it is `null`.
*
* @param {string} path
* The file path to write to.
*
* @param {*} [data]
* The file data to write. If it is null, it will be set to an empty
* string.
*
* @param {number} [mode]
* The permissions to use on file creation. Default is 420 (0644).
*
* @returns {Promise<number>}
* Returns a promise resolving to `0` or rejecting with an error stating
* the failure reason.
*/
write: function(path, data, mode) {
data = (data != null) ? String(data) : '';
mode = (mode != null) ? mode : 420; // 0644
return callFileWrite(path, data, mode).then(handleRpcReply.bind(this, { '': 0 }));
},
/**
* Unlink the given file.
*
* @param {string}
* The file path to remove.
*
* @returns {Promise<number>}
* Returns a promise resolving to `0` or rejecting with an error stating
* the failure reason.
*/
remove: function(path) {
return callFileRemove(path).then(handleRpcReply.bind(this, { '': 0 }));
},
/**
* Execute the specified command, optionally passing params and
* environment variables.
*
* Note: The `command` must be either the path to an executable,
* or a basename without arguments in which case it will be searched
* in $PATH. If specified, the values given in `params` will be passed
* as arguments to the command.
*
* The key/value pairs in the optional `env` table are translated to
* `setenv()` calls prior to running the command.
*
* @param {string} command
* The command to invoke.
*
* @param {string[]} [params]
* The arguments to pass to the command.
*
* @param {Object.<string, string>} [env]
* Environment variables to set.
*
* @returns {Promise<LuCI.fs.FileExecResult>}
* Returns a promise resolving to an object describing the execution
* results or rejecting with an error stating the failure reason.
*/
exec: function(command, params, env) {
if (!Array.isArray(params))
params = null;
if (!L.isObject(env))
env = null;
return callFileExec(command, params, env).then(handleRpcReply.bind(this, { '': {} }));
},
/**
* Read the contents of the given file, trim leading and trailing white
* space and return the trimmed result. In case of errors, return an empty
* string instead.
*
* Note: this function is useful to read single-value files in `/sys`
* or `/proc`.
*
* This function is guaranteed to not reject its promises, on failure,
* an empty string will be returned.
*
* @param {string} path
* The file path to read.
*
* @returns {Promise<string>}
* Returns a promise resolving to the file contents or the empty string
* on failure.
*/
trimmed: function(path) {
return L.resolveDefault(this.read(path), '').then(function(s) {
return s.trim();
});
},
/**
* Read the contents of the given file, split it into lines, trim
* leading and trailing white space of each line and return the
* resulting array.
*
* This function is guaranteed to not reject its promises, on failure,
* an empty array will be returned.
*
* @param {string} path
* The file path to read.
*
* @returns {Promise<string[]>}
* Returns a promise resolving to an array containing the stripped lines
* of the given file or `[]` on failure.
*/
lines: function(path) {
return L.resolveDefault(this.read(path), '').then(function(s) {
var lines = [];
s = s.trim();
if (s != '') {
var l = s.split(/\n/);
for (var i = 0; i < l.length; i++)
lines.push(l[i].trim());
}
return lines;
});
},
/**
* Read the contents of the given file and return them, bypassing ubus.
*
* This function will read the requested file through the cgi-io
* helper applet at `/cgi-bin/cgi-download` which bypasses the ubus rpc
* transport. This is useful to fetch large file contents which might
* exceed the ubus message size limits or which contain binary data.
*
* The cgi-io helper will enforce the same access permission rules as
* the ubus based read call.
*
* @param {string} path
* The file path to read.
*
* @param {string} [type=text]
* The expected type of read file contents. Valid values are `text` to
* interpret the contents as string, `json` to parse the contents as JSON
* or `blob` to return the contents as Blob instance.
*
* @returns {Promise<*>}
* Returns a promise resolving with the file contents interpreted according
* to the specified type or rejecting with an error stating the failure
* reason.
*/
read_direct: function(path, type) {
var postdata = 'sessionid=%s&path=%s'
.format(encodeURIComponent(L.env.sessionid), encodeURIComponent(path));
return request.post(L.env.cgi_base + '/cgi-download', postdata, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: (type == 'blob') ? 'blob' : 'text'
}).then(handleCgiIoReply.bind({ type: type }));
},
/**
* Execute the specified command, bypassing ubus.
*
* Note: The `command` must be either the path to an executable,
* or a basename without arguments in which case it will be searched
* in $PATH. If specified, the values given in `params` will be passed
* as arguments to the command.
*
* This function will invoke the requested commands through the cgi-io
* helper applet at `/cgi-bin/cgi-exec` which bypasses the ubus rpc
* transport. This is useful to fetch large command outputs which might
* exceed the ubus message size limits or which contain binary data.
*
* The cgi-io helper will enforce the same access permission rules as
* the ubus based exec call.
*
* @param {string} command
* The command to invoke.
*
* @param {string[]} [params]
* The arguments to pass to the command.
*
* @param {string} [type=text]
* The expected output type of the invoked program. Valid values are
* `text` to interpret the output as string, `json` to parse the output
* as JSON or `blob` to return the output as Blob instance.
*
* @param {boolean} [latin1=false]
* Whether to encode the command line as Latin1 instead of UTF-8. This
* is usually not needed but can be useful for programs that cannot
* handle UTF-8 input.
*
* @returns {Promise<*>}
* Returns a promise resolving with the command stdout output interpreted
* according to the specified type or rejecting with an error stating the
* failure reason.
*/
exec_direct: function(command, params, type, latin1) {
var cmdstr = String(command)
.replace(/\\/g, '\\\\').replace(/(\s)/g, '\\$1');
if (Array.isArray(params))
for (var i = 0; i < params.length; i++)
cmdstr += ' ' + String(params[i])
.replace(/\\/g, '\\\\').replace(/(\s)/g, '\\$1');
if (latin1)
cmdstr = escape(cmdstr).replace(/\+/g, '%2b');
else
cmdstr = encodeURIComponent(cmdstr);
var postdata = 'sessionid=%s&command=%s'
.format(encodeURIComponent(L.env.sessionid), cmdstr);
return request.post(L.env.cgi_base + '/cgi-exec', postdata, {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
responseType: (type == 'blob') ? 'blob' : 'text'
}).then(handleCgiIoReply.bind({ type: type }));
}
});
return FileSystem;

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 489 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 440 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 656 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 745 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 480 B

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -0,0 +1,5 @@
/* Licensed under the BSD license. Copyright 2014 - Bram Stein. All rights reserved.
* https://github.com/bramstein/promis */
(function(){'use strict';var f,g=[];function l(a){g.push(a);1==g.length&&f()}function m(){for(;g.length;)g[0](),g.shift()}f=function(){setTimeout(m)};function n(a){this.a=p;this.b=void 0;this.f=[];var b=this;try{a(function(a){q(b,a)},function(a){r(b,a)})}catch(c){r(b,c)}}var p=2;function t(a){return new n(function(b,c){c(a)})}function u(a){return new n(function(b){b(a)})}function q(a,b){if(a.a==p){if(b==a)throw new TypeError;var c=!1;try{var d=b&&b.then;if(null!=b&&"object"==typeof b&&"function"==typeof d){d.call(b,function(b){c||q(a,b);c=!0},function(b){c||r(a,b);c=!0});return}}catch(e){c||r(a,e);return}a.a=0;a.b=b;v(a)}}
function r(a,b){if(a.a==p){if(b==a)throw new TypeError;a.a=1;a.b=b;v(a)}}function v(a){l(function(){if(a.a!=p)for(;a.f.length;){var b=a.f.shift(),c=b[0],d=b[1],e=b[2],b=b[3];try{0==a.a?"function"==typeof c?e(c.call(void 0,a.b)):e(a.b):1==a.a&&("function"==typeof d?e(d.call(void 0,a.b)):b(a.b))}catch(h){b(h)}}})}n.prototype.g=function(a){return this.c(void 0,a)};n.prototype.c=function(a,b){var c=this;return new n(function(d,e){c.f.push([a,b,d,e]);v(c)})};
function w(a){return new n(function(b,c){function d(c){return function(d){h[c]=d;e+=1;e==a.length&&b(h)}}var e=0,h=[];0==a.length&&b(h);for(var k=0;k<a.length;k+=1)u(a[k]).c(d(k),c)})}function x(a){return new n(function(b,c){for(var d=0;d<a.length;d+=1)u(a[d]).c(b,c)})};window.Promise||(window.Promise=n,window.Promise.resolve=u,window.Promise.reject=t,window.Promise.race=x,window.Promise.all=w,window.Promise.prototype.then=n.prototype.c,window.Promise.prototype["catch"]=n.prototype.g,window.Promise.prototype.finally=function(a){return this.c(a,a)});}());

@ -0,0 +1,42 @@
'use strict';
'require rpc';
'require form';
'require network';
var callFileRead = rpc.declare({
object: 'file',
method: 'read',
params: [ 'path' ],
expect: { data: '' },
filter: function(value) { return value.trim() }
});
return network.registerProtocol('dhcp', {
getI18n: function() {
return _('DHCP client');
},
renderFormOptions: function(s) {
var o;
o = s.taboption('general', form.Value, 'hostname', _('Hostname to send when requesting DHCP'));
o.default = '';
o.value('', _('Send the hostname of this device'));
o.value('*', _('Do not send a hostname'));
o.datatype = 'or(hostname, "*")';
o.load = function(section_id) {
return callFileRead('/proc/sys/kernel/hostname').then(L.bind(function(hostname) {
this.placeholder = hostname;
return form.Value.prototype.load.apply(this, [section_id]);
}, this));
};
o = s.taboption('advanced', form.Flag, 'broadcast', _('Use broadcast flag'), _('Required for certain ISPs, e.g. Charter with DOCSIS 3'));
o.default = o.disabled;
o = s.taboption('advanced', form.Value, 'clientid', _('Client ID to send when requesting DHCP'));
o.datatype = 'hexstring';
s.taboption('advanced', form.Value, 'vendorid', _('Vendor Class to send when requesting DHCP'));
}
});

@ -0,0 +1,8 @@
'use strict';
'require network';
return network.registerProtocol('none', {
getI18n: function() {
return _('Unmanaged');
}
});

@ -0,0 +1,196 @@
'use strict';
'require form';
'require network';
'require validation';
function isCIDR(value) {
return Array.isArray(value) || /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\/(\d{1,2}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/.test(value);
}
function calculateBroadcast(s, use_cfgvalue) {
var readfn = use_cfgvalue ? 'cfgvalue' : 'formvalue',
addropt = s.children.filter(function(o) { return o.option == 'ipaddr'})[0],
addrvals = addropt ? L.toArray(addropt[readfn](s.section)) : [],
maskopt = s.children.filter(function(o) { return o.option == 'netmask'})[0],
maskval = maskopt ? maskopt[readfn](s.section) : null,
firstsubnet = maskval ? addrvals[0] + '/' + maskval : addrvals.filter(function(a) { return a.indexOf('/') > 0 })[0];
if (firstsubnet == null)
return null;
var addr_mask = firstsubnet.split('/'),
addr = validation.parseIPv4(addr_mask[0]),
mask = addr_mask[1];
if (!isNaN(mask))
mask = validation.parseIPv4(network.prefixToMask(+mask));
else
mask = validation.parseIPv4(mask);
var bc = [
addr[0] | (~mask[0] >>> 0 & 255),
addr[1] | (~mask[1] >>> 0 & 255),
addr[2] | (~mask[2] >>> 0 & 255),
addr[3] | (~mask[3] >>> 0 & 255)
];
return bc.join('.');
}
function validateBroadcast(section_id, value) {
var opt = this.map.lookupOption('broadcast', section_id),
node = opt ? this.map.findElement('id', opt[0].cbid(section_id)) : null,
addr = node ? calculateBroadcast(this.section, false) : null;
if (node != null) {
if (addr != null)
node.querySelector('input').setAttribute('placeholder', addr);
else
node.querySelector('input').removeAttribute('placeholder');
}
return true;
}
return network.registerProtocol('static', {
CBIIPValue: form.Value.extend({
handleSwitch: function(section_id, option_index, ev) {
var maskopt = this.map.lookupOption('netmask', section_id);
if (maskopt == null || !this.isValid(section_id))
return;
var maskval = maskopt[0].formvalue(section_id),
addrval = this.formvalue(section_id),
prefix = maskval ? network.maskToPrefix(maskval) : 32;
if (prefix == null)
return;
this.datatype = 'or(cidr4,ipmask4)';
var parent = L.dom.parent(ev.target, '.cbi-value-field');
L.dom.content(parent, form.DynamicList.prototype.renderWidget.apply(this, [
section_id,
option_index,
addrval ? '%s/%d'.format(addrval, prefix) : ''
]));
var masknode = this.map.findElement('id', maskopt[0].cbid(section_id));
if (masknode) {
parent = L.dom.parent(masknode, '.cbi-value');
parent.parentNode.removeChild(parent);
}
},
renderWidget: function(section_id, option_index, cfgvalue) {
var maskopt = this.map.lookupOption('netmask', section_id),
widget = isCIDR(cfgvalue) ? 'DynamicList' : 'Value';
if (widget == 'DynamicList') {
this.datatype = 'or(cidr4,ipmask4)';
this.placeholder = _('Add IPv4 address…');
}
else {
this.datatype = 'ip4addr("nomask")';
}
var node = form[widget].prototype.renderWidget.apply(this, [ section_id, option_index, cfgvalue ]);
if (widget == 'Value')
L.dom.append(node, E('button', {
'class': 'cbi-button cbi-button-neutral',
'title': _('Switch to CIDR list notation'),
'aria-label': _('Switch to CIDR list notation'),
'click': L.bind(this.handleSwitch, this, section_id, option_index)
}, '…'));
return node;
},
validate: validateBroadcast
}),
CBINetmaskValue: form.Value.extend({
render: function(option_index, section_id, in_table) {
var addropt = this.section.children.filter(function(o) { return o.option == 'ipaddr' })[0],
addrval = addropt ? addropt.cfgvalue(section_id) : null;
if (addrval != null && isCIDR(addrval))
return E([]);
this.value('255.255.255.0');
this.value('255.255.0.0');
this.value('255.0.0.0');
return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
},
validate: validateBroadcast
}),
CBIGatewayValue: form.Value.extend({
datatype: 'ip4addr("nomask")',
render: function(option_index, section_id, in_table) {
return network.getWANNetworks().then(L.bind(function(wans) {
if (wans.length == 1) {
var gwaddr = wans[0].getGatewayAddr();
this.placeholder = gwaddr ? '%s (%s)'.format(gwaddr, wans[0].getName()) : '';
}
return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
}, this));
},
validate: function(section_id, value) {
var addropt = this.section.children.filter(function(o) { return o.option == 'ipaddr' })[0],
addrval = addropt ? L.toArray(addropt.cfgvalue(section_id)) : null;
if (addrval != null) {
for (var i = 0; i < addrval.length; i++) {
var addr = addrval[i].split('/')[0];
if (value == addr)
return _('The gateway address must not be a local IP address');
}
}
return true;
}
}),
CBIBroadcastValue: form.Value.extend({
datatype: 'ip4addr("nomask")',
render: function(option_index, section_id, in_table) {
this.placeholder = calculateBroadcast(this.section, true);
return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
}
}),
getI18n: function() {
return _('Static address');
},
renderFormOptions: function(s) {
var o;
s.taboption('general', this.CBIIPValue, 'ipaddr', _('IPv4 address'));
s.taboption('general', this.CBINetmaskValue, 'netmask', _('IPv4 netmask'));
s.taboption('general', this.CBIGatewayValue, 'gateway', _('IPv4 gateway'));
s.taboption('general', this.CBIBroadcastValue, 'broadcast', _('IPv4 broadcast'));
o = s.taboption('general', form.DynamicList, 'ip6addr', _('IPv6 address'));
o.datatype = 'ip6addr';
o.placeholder = _('Add IPv6 address…');
o.depends('ip6assign', '');
o = s.taboption('general', form.Value, 'ip6gw', _('IPv6 gateway'));
o.datatype = 'ip6addr("nomask")';
o.depends('ip6assign', '');
o = s.taboption('general', form.Value, 'ip6prefix', _('IPv6 routed prefix'), _('Public prefix routed to this device for distribution to clients.'));
o.datatype = 'ip6addr';
o.depends('ip6assign', '');
}
});

@ -0,0 +1,485 @@
'use strict';
'require baseclass';
'require request';
var rpcRequestID = 1,
rpcSessionID = L.env.sessionid || '00000000000000000000000000000000',
rpcBaseURL = L.url('admin/ubus'),
rpcInterceptorFns = [];
/**
* @class rpc
* @memberof LuCI
* @hideconstructor
* @classdesc
*
* The `LuCI.rpc` class provides high level ubus JSON-RPC abstractions
* and means for listing and invoking remove RPC methods.
*/
return baseclass.extend(/** @lends LuCI.rpc.prototype */ {
/* privates */
call: function(req, cb, nobatch) {
var q = '';
if (Array.isArray(req)) {
if (req.length == 0)
return Promise.resolve([]);
for (var i = 0; i < req.length; i++)
if (req[i].params)
q += '%s%s.%s'.format(
q ? ';' : '/',
req[i].params[1],
req[i].params[2]
);
}
return request.post(rpcBaseURL + q, req, {
timeout: (L.env.rpctimeout || 20) * 1000,
nobatch: nobatch,
credentials: true
}).then(cb, cb);
},
parseCallReply: function(req, res) {
var msg = null;
if (res instanceof Error)
return req.reject(res);
try {
if (!res.ok)
L.raise('RPCError', 'RPC call to %s/%s failed with HTTP error %d: %s',
req.object, req.method, res.status, res.statusText || '?');
msg = res.json();
}
catch (e) {
return req.reject(e);
}
/*
* The interceptor args are intentionally swapped.
* Response is passed as first arg to align with Request class interceptors
*/
Promise.all(rpcInterceptorFns.map(function(fn) { return fn(msg, req) }))
.then(this.handleCallReply.bind(this, req, msg))
.catch(req.reject);
},
handleCallReply: function(req, msg) {
var type = Object.prototype.toString,
ret = null;
try {
/* verify message frame */
if (!L.isObject(msg) || msg.jsonrpc != '2.0')
L.raise('RPCError', 'RPC call to %s/%s returned invalid message frame',
req.object, req.method);
/* check error condition */
if (L.isObject(msg.error) && msg.error.code && msg.error.message)
L.raise('RPCError', 'RPC call to %s/%s failed with error %d: %s',
req.object, req.method, msg.error.code, msg.error.message || '?');
}
catch (e) {
return req.reject(e);
}
if (!req.object && !req.method) {
ret = msg.result;
}
else if (Array.isArray(msg.result)) {
if (req.raise && msg.result[0] !== 0)
L.raise('RPCError', 'RPC call to %s/%s failed with ubus code %d: %s',
req.object, req.method, msg.result[0], this.getStatusText(msg.result[0]));
ret = (msg.result.length > 1) ? msg.result[1] : msg.result[0];
}
if (req.expect) {
for (var key in req.expect) {
if (ret != null && key != '')
ret = ret[key];
if (ret == null || type.call(ret) != type.call(req.expect[key]))
ret = req.expect[key];
break;
}
}
/* apply filter */
if (typeof(req.filter) == 'function') {
req.priv[0] = ret;
req.priv[1] = req.params;
ret = req.filter.apply(this, req.priv);
}
req.resolve(ret);
},
/**
* Lists available remote ubus objects or the method signatures of
* specific objects.
*
* This function has two signatures and is sensitive to the number of
* arguments passed to it:
* - `list()` -
* Returns an array containing the names of all remote `ubus` objects
* - `list("objname", ...)`
* Returns method signatures for each given `ubus` object name.
*
* @param {...string} [objectNames]
* If any object names are given, this function will return the method
* signatures of each given object.
*
* @returns {Promise<Array<string>|Object<string, Object<string, Object<string, string>>>>}
* When invoked without arguments, this function will return a promise
* resolving to an array of `ubus` object names. When invoked with one or
* more arguments, a promise resolving to an object describing the method
* signatures of each requested `ubus` object name will be returned.
*/
list: function() {
var msg = {
jsonrpc: '2.0',
id: rpcRequestID++,
method: 'list',
params: arguments.length ? this.varargs(arguments) : undefined
};
return new Promise(L.bind(function(resolveFn, rejectFn) {
/* store request info */
var req = {
resolve: resolveFn,
reject: rejectFn
};
/* call rpc */
this.call(msg, this.parseCallReply.bind(this, req));
}, this));
},
/**
* @typedef {Object} DeclareOptions
* @memberof LuCI.rpc
*
* @property {string} object
* The name of the remote `ubus` object to invoke.
*
* @property {string} method
* The name of the remote `ubus` method to invoke.
*
* @property {string[]} [params]
* Lists the named parameters expected by the remote `ubus` RPC method.
* The arguments passed to the resulting generated method call function
* will be mapped to named parameters in the order they appear in this
* array.
*
* Extraneous parameters passed to the generated function will not be
* sent to the remote procedure but are passed to the
* {@link LuCI.rpc~filterFn filter function} if one is specified.
*
* Examples:
* - `params: [ "foo", "bar" ]` -
* When the resulting call function is invoked with `fn(true, false)`,
* the corresponding args object sent to the remote procedure will be
* `{ foo: true, bar: false }`.
* - `params: [ "test" ], filter: function(reply, args, extra) { ... }` -
* When the resultung generated function is invoked with
* `fn("foo", "bar", "baz")` then `{ "test": "foo" }` will be sent as
* argument to the remote procedure and the filter function will be
* invoked with `filterFn(reply, [ "foo" ], "bar", "baz")`
*
* @property {Object<string,*>} [expect]
* Describes the expected return data structure. The given object is
* supposed to contain a single key selecting the value to use from
* the returned `ubus` reply object. The value of the sole key within
* the `expect` object is used to infer the expected type of the received
* `ubus` reply data.
*
* If the received data does not contain `expect`'s key, or if the
* type of the data differs from the type of the value in the expect
* object, the expect object's value is returned as default instead.
*
* The key in the `expect` object may be an empty string (`''`) in which
* case the entire reply object is selected instead of one of its subkeys.
*
* If the `expect` option is omitted, the received reply will be returned
* as-is, regardless of its format or type.
*
* Examples:
* - `expect: { '': { error: 'Invalid response' } }` -
* This requires the entire `ubus` reply to be a plain JavaScript
* object. If the reply isn't an object but e.g. an array or a numeric
* error code instead, it will get replaced with
* `{ error: 'Invalid response' }` instead.
* - `expect: { results: [] }` -
* This requires the received `ubus` reply to be an object containing
* a key `results` with an array as value. If the received reply does
* not contain such a key, or if `reply.results` points to a non-array
* value, the empty array (`[]`) will be used instead.
* - `expect: { success: false }` -
* This requires the received `ubus` reply to be an object containing
* a key `success` with a boolean value. If the reply does not contain
* `success` or if `reply.success` is not a boolean value, `false` will
* be returned as default instead.
*
* @property {LuCI.rpc~filterFn} [filter]
* Specfies an optional filter function which is invoked to transform the
* received reply data before it is returned to the caller.
*
* @property {boolean} [reject=false]
* If set to `true`, non-zero ubus call status codes are treated as fatal
* error and lead to the rejection of the call promise. The default
* behaviour is to resolve with the call return code value instead.
*/
/**
* The filter function is invoked to transform a received `ubus` RPC call
* reply before returning it to the caller.
*
* @callback LuCI.rpc~filterFn
*
* @param {*} data
* The received `ubus` reply data or a subset of it as described in the
* `expect` option of the RPC call declaration. In case of remote call
* errors, `data` is numeric `ubus` error code instead.
*
* @param {Array<*>} args
* The arguments the RPC method has been invoked with.
*
* @param {...*} extraArgs
* All extraneous arguments passed to the RPC method exceeding the number
* of arguments describes in the RPC call declaration.
*
* @return {*}
* The return value of the filter function will be returned to the caller
* of the RPC method as-is.
*/
/**
* The generated invocation function is returned by
* {@link LuCI.rpc#declare rpc.declare()} and encapsulates a single
* RPC method call.
*
* Calling this function will execute a remote `ubus` HTTP call request
* using the arguments passed to it as arguments and return a promise
* resolving to the received reply values.
*
* @callback LuCI.rpc~invokeFn
*
* @param {...*} params
* The parameters to pass to the remote procedure call. The given
* positional arguments will be named to named RPC parameters according
* to the names specified in the `params` array of the method declaration.
*
* Any additional parameters exceeding the amount of arguments in the
* `params` declaration are passed as private extra arguments to the
* declared filter function.
*
* @return {Promise<*>}
* Returns a promise resolving to the result data of the remote `ubus`
* RPC method invocation, optionally substituted and filtered according
* to the `expect` and `filter` declarations.
*/
/**
* Describes a remote RPC call procedure and returns a function
* implementing it.
*
* @param {LuCI.rpc.DeclareOptions} options
* If any object names are given, this function will return the method
* signatures of each given object.
*
* @returns {LuCI.rpc~invokeFn}
* Returns a new function implementing the method call described in
* `options`.
*/
declare: function(options) {
return Function.prototype.bind.call(function(rpc, options) {
var args = this.varargs(arguments, 2);
return new Promise(function(resolveFn, rejectFn) {
/* build parameter object */
var p_off = 0;
var params = { };
if (Array.isArray(options.params))
for (p_off = 0; p_off < options.params.length; p_off++)
params[options.params[p_off]] = args[p_off];
/* all remaining arguments are private args */
var priv = [ undefined, undefined ];
for (; p_off < args.length; p_off++)
priv.push(args[p_off]);
/* store request info */
var req = {
expect: options.expect,
filter: options.filter,
resolve: resolveFn,
reject: rejectFn,
params: params,
priv: priv,
object: options.object,
method: options.method,
raise: options.reject
};
/* build message object */
var msg = {
jsonrpc: '2.0',
id: rpcRequestID++,
method: 'call',
params: [
rpcSessionID,
options.object,
options.method,
params
]
};
/* call rpc */
rpc.call(msg, rpc.parseCallReply.bind(rpc, req), options.nobatch);
});
}, this, this, options);
},
/**
* Returns the current RPC session id.
*
* @returns {string}
* Returns the 32 byte session ID string used for authenticating remote
* requests.
*/
getSessionID: function() {
return rpcSessionID;
},
/**
* Set the RPC session id to use.
*
* @param {string} sid
* Sets the 32 byte session ID string used for authenticating remote
* requests.
*/
setSessionID: function(sid) {
rpcSessionID = sid;
},
/**
* Returns the current RPC base URL.
*
* @returns {string}
* Returns the RPC URL endpoint to issue requests against.
*/
getBaseURL: function() {
return rpcBaseURL;
},
/**
* Set the RPC base URL to use.
*
* @param {string} sid
* Sets the RPC URL endpoint to issue requests against.
*/
setBaseURL: function(url) {
rpcBaseURL = url;
},
/**
* Translates a numeric `ubus` error code into a human readable
* description.
*
* @param {number} statusCode
* The numeric status code.
*
* @returns {string}
* Returns the textual description of the code.
*/
getStatusText: function(statusCode) {
switch (statusCode) {
case 0: return _('Command OK');
case 1: return _('Invalid command');
case 2: return _('Invalid argument');
case 3: return _('Method not found');
case 4: return _('Resource not found');
case 5: return _('No data received');
case 6: return _('Permission denied');
case 7: return _('Request timeout');
case 8: return _('Not supported');
case 9: return _('Unspecified error');
case 10: return _('Connection lost');
default: return _('Unknown error code');
}
},
/**
* Registered interceptor functions are invoked before the standard reply
* parsing and handling logic.
*
* By returning rejected promises, interceptor functions can cause the
* invocation function to fail, regardless of the received reply.
*
* Interceptors may also modify their message argument in-place to
* rewrite received replies before they're processed by the standard
* response handling code.
*
* A common use case for such functions is to detect failing RPC replies
* due to expired authentication in order to trigger a new login.
*
* @callback LuCI.rpc~interceptorFn
*
* @param {*} msg
* The unprocessed, JSON decoded remote RPC method call reply.
*
* Since interceptors run before the standard parsing logic, the reply
* data is not verified for correctness or filtered according to
* `expect` and `filter` specifications in the declarations.
*
* @param {Object} req
* The related request object which is an extended variant of the
* declaration object, allowing access to internals of the invocation
* function such as `filter`, `expect` or `params` values.
*
* @return {Promise<*>|*}
* Interceptor functions may return a promise to defer response
* processing until some delayed work completed. Any values the returned
* promise resolves to are ignored.
*
* When the returned promise rejects with an error, the invocation
* function will fail too, forwarding the error to the caller.
*/
/**
* Registers a new interceptor function.
*
* @param {LuCI.rpc~interceptorFn} interceptorFn
* The inteceptor function to register.
*
* @returns {LuCI.rpc~interceptorFn}
* Returns the given function value.
*/
addInterceptor: function(interceptorFn) {
if (typeof(interceptorFn) == 'function')
rpcInterceptorFns.push(interceptorFn);
return interceptorFn;
},
/**
* Removes a registered interceptor function.
*
* @param {LuCI.rpc~interceptorFn} interceptorFn
* The inteceptor function to remove.
*
* @returns {boolean}
* Returns `true` if the given function has been removed or `false`
* if it has not been found.
*/
removeInterceptor: function(interceptorFn) {
var oldlen = rpcInterceptorFns.length, i = oldlen;
while (i--)
if (rpcInterceptorFns[i] === interceptorFn)
rpcInterceptorFns.splice(i, 1);
return (rpcInterceptorFns.length < oldlen);
}
});

@ -0,0 +1,111 @@
'use strict';
var s = [0x0000, 0x0000, 0x0000, 0x0000];
function mul(a, b) {
var r = [0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000, 0x0000];
for (var j = 0; j < 4; j++) {
var k = 0;
for (var i = 0; i < 4; i++) {
var t = a[i] * b[j] + r[i+j] + k;
r[i+j] = t & 0xffff;
k = t >>> 16;
}
r[j+4] = k;
}
r.length = 4;
return r;
}
function add(a, n) {
var r = [0x0000, 0x0000, 0x0000, 0x0000],
k = n;
for (var i = 0; i < 4; i++) {
var t = a[i] + k;
r[i] = t & 0xffff;
k = t >>> 16;
}
return r;
}
function shr(a, n) {
var r = [a[0], a[1], a[2], a[3], 0x0000],
i = 4,
k = 0;
for (; n > 16; n -= 16, i--)
for (var j = 0; j < 4; j++)
r[j] = r[j+1];
for (; i > 0; i--) {
var s = r[i-1];
r[i-1] = (s >>> n) | k;
k = ((s & ((1 << n) - 1)) << (16 - n));
}
r.length = 4;
return r;
}
return L.Class.extend({
seed: function(n) {
n = (n - 1)|0;
s[0] = n & 0xffff;
s[1] = n >>> 16;
s[2] = 0;
s[3] = 0;
},
int: function() {
s = mul(s, [0x7f2d, 0x4c95, 0xf42d, 0x5851]);
s = add(s, 1);
var r = shr(s, 33);
return (r[1] << 16) | r[0];
},
get: function() {
var r = (this.int() % 0x7fffffff) / 0x7fffffff, l, u;
switch (arguments.length) {
case 0:
return r;
case 1:
l = 1;
u = arguments[0]|0;
break;
case 2:
l = arguments[0]|0;
u = arguments[1]|0;
break;
}
return Math.floor(r * (u - l + 1)) + l;
},
derive_color: function(string) {
this.seed(parseInt(sfh(string), 16));
var r = this.get(128),
g = this.get(128),
min = 0,
max = 128;
if ((r + g) < 128)
min = 128 - r - g;
else
max = 255 - r - g;
var b = min + Math.floor(this.get() * (max - min));
return '#%02x%02x%02x'.format(0xff - r, 0xff - g, 0xff - b);
}
});

@ -0,0 +1,629 @@
'use strict';
'require ui';
'require form';
'require network';
'require firewall';
'require fs';
function getUsers() {
return fs.lines('/etc/passwd').then(function(lines) {
return lines.map(function(line) { return line.split(/:/)[0] });
});
}
function getGroups() {
return fs.lines('/etc/group').then(function(lines) {
return lines.map(function(line) { return line.split(/:/)[0] });
});
}
var CBIZoneSelect = form.ListValue.extend({
__name__: 'CBI.ZoneSelect',
load: function(section_id) {
return Promise.all([ firewall.getZones(), network.getNetworks() ]).then(L.bind(function(zn) {
this.zones = zn[0];
this.networks = zn[1];
return this.super('load', section_id);
}, this));
},
filter: function(section_id, value) {
return true;
},
lookupZone: function(name) {
return this.zones.filter(function(zone) { return zone.getName() == name })[0];
},
lookupNetwork: function(name) {
return this.networks.filter(function(network) { return network.getName() == name })[0];
},
renderWidget: function(section_id, option_index, cfgvalue) {
var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
isOutputOnly = false,
choices = {};
if (this.option == 'dest') {
for (var i = 0; i < this.section.children.length; i++) {
var opt = this.section.children[i];
if (opt.option == 'src') {
var val = opt.cfgvalue(section_id) || opt.default;
isOutputOnly = (val == null || val == '');
break;
}
}
this.title = isOutputOnly ? _('Output zone') : _('Destination zone');
}
if (this.allowlocal) {
choices[''] = E('span', {
'class': 'zonebadge',
'style': firewall.getZoneColorStyle(null)
}, [
E('strong', _('Device')),
(this.allowany || this.allowlocal)
? E('span', ' (%s)'.format(this.option != 'dest' ? _('output') : _('input'))) : ''
]);
}
else if (!this.multiple && (this.rmempty || this.optional)) {
choices[''] = E('span', {
'class': 'zonebadge',
'style': firewall.getZoneColorStyle(null)
}, E('em', _('unspecified')));
}
if (this.allowany) {
choices['*'] = E('span', {
'class': 'zonebadge',
'style': firewall.getZoneColorStyle(null)
}, [
E('strong', _('Any zone')),
(this.allowany && this.allowlocal && !isOutputOnly) ? E('span', ' (%s)'.format(_('forward'))) : ''
]);
}
for (var i = 0; i < this.zones.length; i++) {
var zone = this.zones[i],
name = zone.getName(),
networks = zone.getNetworks(),
ifaces = [];
if (!this.filter(section_id, name))
continue;
for (var j = 0; j < networks.length; j++) {
var network = this.lookupNetwork(networks[j]);
if (!network)
continue;
var span = E('span', {
'class': 'ifacebadge' + (network.getName() == this.network ? ' ifacebadge-active' : '')
}, network.getName() + ': ');
var devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
for (var k = 0; k < devices.length; k++) {
span.appendChild(E('img', {
'title': devices[k].getI18n(),
'src': L.resource('icons/%s%s.png'.format(devices[k].getType(), devices[k].isUp() ? '' : '_disabled'))
}));
}
if (!devices.length)
span.appendChild(E('em', _('(empty)')));
ifaces.push(span);
}
if (!ifaces.length)
ifaces.push(E('em', _('(empty)')));
choices[name] = E('span', {
'class': 'zonebadge',
'style': firewall.getZoneColorStyle(zone)
}, [ E('strong', name) ].concat(ifaces));
}
var widget = new ui.Dropdown(values, choices, {
id: this.cbid(section_id),
sort: true,
multiple: this.multiple,
optional: this.optional || this.rmempty,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
validate: L.bind(this.validate, this, section_id),
create: !this.nocreate,
create_markup: '' +
'<li data-value="{{value}}">' +
'<span class="zonebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
'<strong>{{value}}:</strong> <em>('+_('create')+')</em>' +
'</span>' +
'</li>'
});
var elem = widget.render();
if (this.option == 'src') {
elem.addEventListener('cbi-dropdown-change', L.bind(function(ev) {
var opt = this.map.lookupOption('dest', section_id),
val = ev.detail.instance.getValue();
if (opt == null)
return;
var cbid = opt[0].cbid(section_id),
label = document.querySelector('label[for="widget.%s"]'.format(cbid)),
node = document.getElementById(cbid);
L.dom.content(label, val == '' ? _('Output zone') : _('Destination zone'));
if (val == '') {
if (L.dom.callClassMethod(node, 'getValue') == '')
L.dom.callClassMethod(node, 'setValue', '*');
var emptyval = node.querySelector('[data-value=""]'),
anyval = node.querySelector('[data-value="*"]');
L.dom.content(anyval.querySelector('span'), E('strong', _('Any zone')));
if (emptyval != null)
emptyval.parentNode.removeChild(emptyval);
}
else {
var anyval = node.querySelector('[data-value="*"]'),
emptyval = node.querySelector('[data-value=""]');
if (emptyval == null) {
emptyval = anyval.cloneNode(true);
emptyval.removeAttribute('display');
emptyval.removeAttribute('selected');
emptyval.setAttribute('data-value', '');
}
if (opt[0].allowlocal)
L.dom.content(emptyval.querySelector('span'), [
E('strong', _('Device')), E('span', ' (%s)'.format(_('input')))
]);
L.dom.content(anyval.querySelector('span'), [
E('strong', _('Any zone')), E('span', ' (%s)'.format(_('forward')))
]);
anyval.parentNode.insertBefore(emptyval, anyval);
}
}, this));
}
else if (isOutputOnly) {
var emptyval = elem.querySelector('[data-value=""]');
emptyval.parentNode.removeChild(emptyval);
}
return elem;
},
});
var CBIZoneForwards = form.DummyValue.extend({
__name__: 'CBI.ZoneForwards',
load: function(section_id) {
return Promise.all([
firewall.getDefaults(),
firewall.getZones(),
network.getNetworks(),
network.getDevices()
]).then(L.bind(function(dznd) {
this.defaults = dznd[0];
this.zones = dznd[1];
this.networks = dznd[2];
this.devices = dznd[3];
return this.super('load', section_id);
}, this));
},
renderZone: function(zone) {
var name = zone.getName(),
networks = zone.getNetworks(),
devices = zone.getDevices(),
subnets = zone.getSubnets(),
ifaces = [];
for (var j = 0; j < networks.length; j++) {
var network = this.networks.filter(function(net) { return net.getName() == networks[j] })[0];
if (!network)
continue;
var span = E('span', {
'class': 'ifacebadge' + (network.getName() == this.network ? ' ifacebadge-active' : '')
}, network.getName() + ': ');
var subdevs = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
for (var k = 0; k < subdevs.length && subdevs[k]; k++) {
span.appendChild(E('img', {
'title': subdevs[k].getI18n(),
'src': L.resource('icons/%s%s.png'.format(subdevs[k].getType(), subdevs[k].isUp() ? '' : '_disabled'))
}));
}
if (!subdevs.length)
span.appendChild(E('em', _('(empty)')));
ifaces.push(span);
}
for (var i = 0; i < devices.length; i++) {
var device = this.devices.filter(function(dev) { return dev.getName() == devices[i] })[0],
title = device ? device.getI18n() : _('Absent Interface'),
type = device ? device.getType() : 'ethernet',
up = device ? device.isUp() : false;
ifaces.push(E('span', { 'class': 'ifacebadge' }, [
E('img', {
'title': title,
'src': L.resource('icons/%s%s.png'.format(type, up ? '' : '_disabled'))
}),
device ? device.getName() : devices[i]
]));
}
if (subnets.length > 0)
ifaces.push(E('span', { 'class': 'ifacebadge' }, [ '{ %s }'.format(subnets.join('; ')) ]));
if (!ifaces.length)
ifaces.push(E('span', { 'class': 'ifacebadge' }, E('em', _('(empty)'))));
return E('label', {
'class': 'zonebadge cbi-tooltip-container',
'style': firewall.getZoneColorStyle(zone)
}, [
E('strong', name),
E('div', { 'class': 'cbi-tooltip' }, ifaces)
]);
},
renderWidget: function(section_id, option_index, cfgvalue) {
var value = (cfgvalue != null) ? cfgvalue : this.default,
zone = this.zones.filter(function(z) { return z.getName() == value })[0];
if (!zone)
return E([]);
var forwards = zone.getForwardingsBy('src'),
dzones = [];
for (var i = 0; i < forwards.length; i++) {
var dzone = forwards[i].getDestinationZone();
if (!dzone)
continue;
dzones.push(this.renderZone(dzone));
}
if (!dzones.length)
dzones.push(E('label', { 'class': 'zonebadge zonebadge-empty' },
E('strong', this.defaults.getForward())));
return E('div', { 'class': 'zone-forwards' }, [
E('div', { 'class': 'zone-src' }, this.renderZone(zone)),
E('span', '⇒'),
E('div', { 'class': 'zone-dest' }, dzones)
]);
},
});
var CBINetworkSelect = form.ListValue.extend({
__name__: 'CBI.NetworkSelect',
load: function(section_id) {
return network.getNetworks().then(L.bind(function(networks) {
this.networks = networks;
return this.super('load', section_id);
}, this));
},
filter: function(section_id, value) {
return true;
},
renderIfaceBadge: function(network) {
var span = E('span', { 'class': 'ifacebadge' }, network.getName() + ': '),
devices = network.isBridge() ? network.getDevices() : L.toArray(network.getDevice());
for (var j = 0; j < devices.length && devices[j]; j++) {
span.appendChild(E('img', {
'title': devices[j].getI18n(),
'src': L.resource('icons/%s%s.png'.format(devices[j].getType(), devices[j].isUp() ? '' : '_disabled'))
}));
}
if (!devices.length) {
span.appendChild(E('em', { 'class': 'hide-close' }, _('(no interfaces attached)')));
span.appendChild(E('em', { 'class': 'hide-open' }, '-'));
}
return span;
},
renderWidget: function(section_id, option_index, cfgvalue) {
var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
choices = {},
checked = {};
for (var i = 0; i < values.length; i++)
checked[values[i]] = true;
values = [];
if (!this.multiple && (this.rmempty || this.optional))
choices[''] = E('em', _('unspecified'));
for (var i = 0; i < this.networks.length; i++) {
var network = this.networks[i],
name = network.getName();
if (name == this.exclude || !this.filter(section_id, name))
continue;
if (name == 'loopback' && !this.loopback)
continue;
if (this.novirtual && network.isVirtual())
continue;
if (checked[name])
values.push(name);
choices[name] = this.renderIfaceBadge(network);
}
var widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
id: this.cbid(section_id),
sort: true,
multiple: this.multiple,
optional: this.optional || this.rmempty,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
datatype: this.multiple ? 'list(uciname)' : 'uciname',
validate: L.bind(this.validate, this, section_id),
create: !this.nocreate,
create_markup: '' +
'<li data-value="{{value}}">' +
'<span class="ifacebadge" style="background:repeating-linear-gradient(45deg,rgba(204,204,204,0.5),rgba(204,204,204,0.5) 5px,rgba(255,255,255,0.5) 5px,rgba(255,255,255,0.5) 10px)">' +
'{{value}}: <em>('+_('create')+')</em>' +
'</span>' +
'</li>'
});
return widget.render();
},
textvalue: function(section_id) {
var cfgvalue = this.cfgvalue(section_id),
values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
rv = E([]);
for (var i = 0; i < (this.networks || []).length; i++) {
var network = this.networks[i],
name = network.getName();
if (values.indexOf(name) == -1)
continue;
if (rv.length)
L.dom.append(rv, ' ');
L.dom.append(rv, this.renderIfaceBadge(network));
}
if (!rv.firstChild)
rv.appendChild(E('em', _('unspecified')));
return rv;
},
});
var CBIDeviceSelect = form.ListValue.extend({
__name__: 'CBI.DeviceSelect',
load: function(section_id) {
return Promise.all([
network.getDevices(),
this.noaliases ? null : network.getNetworks()
]).then(L.bind(function(data) {
this.devices = data[0];
this.networks = data[1];
return this.super('load', section_id);
}, this));
},
filter: function(section_id, value) {
return true;
},
renderWidget: function(section_id, option_index, cfgvalue) {
var values = L.toArray((cfgvalue != null) ? cfgvalue : this.default),
choices = {},
checked = {},
order = [];
for (var i = 0; i < values.length; i++)
checked[values[i]] = true;
values = [];
if (!this.multiple && (this.rmempty || this.optional))
choices[''] = E('em', _('unspecified'));
for (var i = 0; i < this.devices.length; i++) {
var device = this.devices[i],
name = device.getName(),
type = device.getType();
if (name == 'lo' || name == this.exclude || !this.filter(section_id, name))
continue;
if (this.noaliases && type == 'alias')
continue;
if (this.nobridges && type == 'bridge')
continue;
if (this.noinactive && device.isUp() == false)
continue;
var item = E([
E('img', {
'title': device.getI18n(),
'src': L.resource('icons/%s%s.png'.format(type, device.isUp() ? '' : '_disabled'))
}),
E('span', { 'class': 'hide-open' }, [ name ]),
E('span', { 'class': 'hide-close'}, [ device.getI18n() ])
]);
var networks = device.getNetworks();
if (networks.length > 0)
L.dom.append(item.lastChild, [ ' (', networks.map(function(n) { return n.getName() }).join(', '), ')' ]);
if (checked[name])
values.push(name);
choices[name] = item;
order.push(name);
}
if (this.networks != null) {
for (var i = 0; i < this.networks.length; i++) {
var net = this.networks[i],
device = network.instantiateDevice('@%s'.format(net.getName()), net),
name = device.getName();
if (name == '@loopback' || name == this.exclude || !this.filter(section_id, name))
continue;
if (this.noinactive && net.isUp() == false)
continue;
var item = E([
E('img', {
'title': device.getI18n(),
'src': L.resource('icons/alias%s.png'.format(net.isUp() ? '' : '_disabled'))
}),
E('span', { 'class': 'hide-open' }, [ name ]),
E('span', { 'class': 'hide-close'}, [ device.getI18n() ])
]);
if (checked[name])
values.push(name);
choices[name] = item;
order.push(name);
}
}
if (!this.nocreate) {
var keys = Object.keys(checked).sort(L.naturalCompare);
for (var i = 0; i < keys.length; i++) {
if (choices.hasOwnProperty(keys[i]))
continue;
choices[keys[i]] = E([
E('img', {
'title': _('Absent Interface'),
'src': L.resource('icons/ethernet_disabled.png')
}),
E('span', { 'class': 'hide-open' }, [ keys[i] ]),
E('span', { 'class': 'hide-close'}, [ '%s: "%h"'.format(_('Absent Interface'), keys[i]) ])
]);
values.push(keys[i]);
order.push(keys[i]);
}
}
var widget = new ui.Dropdown(this.multiple ? values : values[0], choices, {
id: this.cbid(section_id),
sort: order,
multiple: this.multiple,
optional: this.optional || this.rmempty,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
select_placeholder: E('em', _('unspecified')),
display_items: this.display_size || this.size || 3,
dropdown_items: this.dropdown_size || this.size || 5,
validate: L.bind(this.validate, this, section_id),
create: !this.nocreate,
create_markup: '' +
'<li data-value="{{value}}">' +
'<img title="'+_('Custom Interface')+': &quot;{{value}}&quot;" src="'+L.resource('icons/ethernet_disabled.png')+'" />' +
'<span class="hide-open">{{value}}</span>' +
'<span class="hide-close">'+_('Custom Interface')+': "{{value}}"</span>' +
'</li>'
});
return widget.render();
},
});
var CBIUserSelect = form.ListValue.extend({
__name__: 'CBI.UserSelect',
load: function(section_id) {
return getUsers().then(L.bind(function(users) {
delete this.keylist;
delete this.vallist;
for (var i = 0; i < users.length; i++) {
this.value(users[i]);
}
return this.super('load', section_id);
}, this));
},
filter: function(section_id, value) {
return true;
},
});
var CBIGroupSelect = form.ListValue.extend({
__name__: 'CBI.GroupSelect',
load: function(section_id) {
return getGroups().then(L.bind(function(groups) {
for (var i = 0; i < groups.length; i++) {
this.value(groups[i]);
}
return this.super('load', section_id);
}, this));
},
filter: function(section_id, value) {
return true;
},
});
return L.Class.extend({
ZoneSelect: CBIZoneSelect,
ZoneForwards: CBIZoneForwards,
NetworkSelect: CBINetworkSelect,
DeviceSelect: CBIDeviceSelect,
UserSelect: CBIUserSelect,
GroupSelect: CBIGroupSelect,
});

@ -0,0 +1,988 @@
'use strict';
'require rpc';
'require baseclass';
function isEmpty(object, ignore) {
for (var property in object)
if (object.hasOwnProperty(property) && property != ignore)
return false;
return true;
}
/**
* @class uci
* @memberof LuCI
* @hideconstructor
* @classdesc
*
* The `LuCI.uci` class utilizes {@link LuCI.rpc} to declare low level
* remote UCI `ubus` procedures and implements a local caching and data
* manipulation layer on top to allow for synchroneous operations on
* UCI configuration data.
*/
return baseclass.extend(/** @lends LuCI.uci.prototype */ {
__init__: function() {
this.state = {
newidx: 0,
values: { },
creates: { },
changes: { },
deletes: { },
reorder: { }
};
this.loaded = {};
},
callLoad: rpc.declare({
object: 'uci',
method: 'get',
params: [ 'config' ],
expect: { values: { } },
reject: true
}),
callOrder: rpc.declare({
object: 'uci',
method: 'order',
params: [ 'config', 'sections' ],
reject: true
}),
callAdd: rpc.declare({
object: 'uci',
method: 'add',
params: [ 'config', 'type', 'name', 'values' ],
expect: { section: '' },
reject: true
}),
callSet: rpc.declare({
object: 'uci',
method: 'set',
params: [ 'config', 'section', 'values' ],
reject: true
}),
callDelete: rpc.declare({
object: 'uci',
method: 'delete',
params: [ 'config', 'section', 'options' ],
reject: true
}),
callApply: rpc.declare({
object: 'uci',
method: 'apply',
params: [ 'timeout', 'rollback' ],
reject: true
}),
callConfirm: rpc.declare({
object: 'uci',
method: 'confirm',
reject: true
}),
/**
* Generates a new, unique section ID for the given configuration.
*
* Note that the generated ID is temporary, it will get replaced by an
* identifier in the form `cfgXXXXXX` once the configuration is saved
* by the remote `ubus` UCI api.
*
* @param {string} config
* The configuration to generate the new section ID for.
*
* @returns {string}
* A newly generated, unique section ID in the form `newXXXXXX`
* where `X` denotes a hexadecimal digit.
*/
createSID: function(conf) {
var v = this.state.values,
n = this.state.creates,
sid;
do {
sid = "new%06x".format(Math.random() * 0xFFFFFF);
} while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
return sid;
},
/**
* Resolves a given section ID in extended notation to the internal
* section ID value.
*
* @param {string} config
* The configuration to resolve the section ID for.
*
* @param {string} sid
* The section ID to resolve. If the ID is in the form `@typename[#]`,
* it will get resolved to an internal anonymous ID in the forms
* `cfgXXXXXX`/`newXXXXXX` or to the name of a section in case it points
* to a named section. When the given ID is not in extended notation,
* it will be returned as-is.
*
* @returns {string|null}
* Returns the resolved section ID or the original given ID if it was
* not in extended notation. Returns `null` when an extended ID could
* not be resolved to existing section ID.
*/
resolveSID: function(conf, sid) {
if (typeof(sid) != 'string')
return sid;
var m = /^@([a-zA-Z0-9_-]+)\[(-?[0-9]+)\]$/.exec(sid);
if (m) {
var type = m[1],
pos = +m[2],
sections = this.sections(conf, type),
section = sections[pos >= 0 ? pos : sections.length + pos];
return section ? section['.name'] : null;
}
return sid;
},
/* private */
reorderSections: function() {
var v = this.state.values,
n = this.state.creates,
r = this.state.reorder,
tasks = [];
if (Object.keys(r).length === 0)
return Promise.resolve();
/*
gather all created and existing sections, sort them according
to their index value and issue an uci order call
*/
for (var c in r) {
var o = [ ];
if (n[c])
for (var s in n[c])
o.push(n[c][s]);
for (var s in v[c])
o.push(v[c][s]);
if (o.length > 0) {
o.sort(function(a, b) {
return (a['.index'] - b['.index']);
});
var sids = [ ];
for (var i = 0; i < o.length; i++)
sids.push(o[i]['.name']);
tasks.push(this.callOrder(c, sids));
}
}
this.state.reorder = { };
return Promise.all(tasks);
},
/* private */
loadPackage: function(packageName) {
if (this.loaded[packageName] == null)
return (this.loaded[packageName] = this.callLoad(packageName));
return Promise.resolve(this.loaded[packageName]);
},
/**
* Loads the given UCI configurations from the remote `ubus` api.
*
* Loaded configurations are cached and only loaded once. Subsequent
* load operations of the same configurations will return the cached
* data.
*
* To force reloading a configuration, it has to be unloaded with
* {@link LuCI.uci#unload uci.unload()} first.
*
* @param {string|string[]} config
* The name of the configuration or an array of configuration
* names to load.
*
* @returns {Promise<string[]>}
* Returns a promise resolving to the names of the configurations
* that have been successfully loaded.
*/
load: function(packages) {
var self = this,
pkgs = [ ],
tasks = [];
if (!Array.isArray(packages))
packages = [ packages ];
for (var i = 0; i < packages.length; i++)
if (!self.state.values[packages[i]]) {
pkgs.push(packages[i]);
tasks.push(self.loadPackage(packages[i]));
}
return Promise.all(tasks).then(function(responses) {
for (var i = 0; i < responses.length; i++)
self.state.values[pkgs[i]] = responses[i];
if (responses.length)
document.dispatchEvent(new CustomEvent('uci-loaded'));
return pkgs;
});
},
/**
* Unloads the given UCI configurations from the local cache.
*
* @param {string|string[]} config
* The name of the configuration or an array of configuration
* names to unload.
*/
unload: function(packages) {
if (!Array.isArray(packages))
packages = [ packages ];
for (var i = 0; i < packages.length; i++) {
delete this.state.values[packages[i]];
delete this.state.creates[packages[i]];
delete this.state.changes[packages[i]];
delete this.state.deletes[packages[i]];
delete this.loaded[packages[i]];
}
},
/**
* Adds a new section of the given type to the given configuration,
* optionally named according to the given name.
*
* @param {string} config
* The name of the configuration to add the section to.
*
* @param {string} type
* The type of the section to add.
*
* @param {string} [name]
* The name of the section to add. If the name is omitted, an anonymous
* section will be added instead.
*
* @returns {string}
* Returns the section ID of the newly added section which is equivalent
* to the given name for non-anonymous sections.
*/
add: function(conf, type, name) {
var n = this.state.creates,
sid = name || this.createSID(conf);
if (!n[conf])
n[conf] = { };
n[conf][sid] = {
'.type': type,
'.name': sid,
'.create': name,
'.anonymous': !name,
'.index': 1000 + this.state.newidx++
};
return sid;
},
/**
* Removes the section with the given ID from the given configuration.
*
* @param {string} config
* The name of the configuration to remove the section from.
*
* @param {string} sid
* The ID of the section to remove.
*/
remove: function(conf, sid) {
var v = this.state.values,
n = this.state.creates,
c = this.state.changes,
d = this.state.deletes;
/* requested deletion of a just created section */
if (n[conf] && n[conf][sid]) {
delete n[conf][sid];
}
else if (v[conf] && v[conf][sid]) {
if (c[conf])
delete c[conf][sid];
if (!d[conf])
d[conf] = { };
d[conf][sid] = true;
}
},
/**
* A section object represents the options and their corresponding values
* enclosed within a configuration section, as well as some additional
* meta data such as sort indexes and internal ID.
*
* Any internal metadata fields are prefixed with a dot which is isn't
* an allowed character for normal option names.
*
* @typedef {Object<string, boolean|number|string|string[]>} SectionObject
* @memberof LuCI.uci
*
* @property {boolean} .anonymous
* The `.anonymous` property specifies whether the configuration is
* anonymous (`true`) or named (`false`).
*
* @property {number} .index
* The `.index` property specifes the sort order of the section.
*
* @property {string} .name
* The `.name` property holds the name of the section object. It may be
* either an anonymous ID in the form `cfgXXXXXX` or `newXXXXXX` with `X`
* being a hexadecimal digit or a string holding the name of the section.
*
* @property {string} .type
* The `.type` property contains the type of the corresponding uci
* section.
*
* @property {string|string[]} *
* A section object may contain an arbitrary number of further properties
* representing the uci option enclosed in the section.
*
* All option property names will be in the form `[A-Za-z0-9_]+` and
* either contain a string value or an array of strings, in case the
* underlying option is an UCI list.
*/
/**
* The sections callback is invoked for each section found within
* the given configuration and receives the section object and its
* associated name as arguments.
*
* @callback LuCI.uci~sectionsFn
*
* @param {LuCI.uci.SectionObject} section
* The section object.
*
* @param {string} sid
* The name or ID of the section.
*/
/**
* Enumerates the sections of the given configuration, optionally
* filtered by type.
*
* @param {string} config
* The name of the configuration to enumerate the sections for.
*
* @param {string} [type]
* Enumerate only sections of the given type. If omitted, enumerate
* all sections.
*
* @param {LuCI.uci~sectionsFn} [cb]
* An optional callback to invoke for each enumerated section.
*
* @returns {Array<LuCI.uci.SectionObject>}
* Returns a sorted array of the section objects within the given
* configuration, filtered by type of a type has been specified.
*/
sections: function(conf, type, cb) {
var sa = [ ],
v = this.state.values[conf],
n = this.state.creates[conf],
c = this.state.changes[conf],
d = this.state.deletes[conf];
if (!v)
return sa;
for (var s in v)
if (!d || d[s] !== true)
if (!type || v[s]['.type'] == type)
sa.push(Object.assign({ }, v[s], c ? c[s] : null));
if (n)
for (var s in n)
if (!type || n[s]['.type'] == type)
sa.push(Object.assign({ }, n[s]));
sa.sort(function(a, b) {
return a['.index'] - b['.index'];
});
for (var i = 0; i < sa.length; i++)
sa[i]['.index'] = i;
if (typeof(cb) == 'function')
for (var i = 0; i < sa.length; i++)
cb.call(this, sa[i], sa[i]['.name']);
return sa;
},
/**
* Gets the value of the given option within the specified section
* of the given configuration or the entire section object if the
* option name is omitted.
*
* @param {string} config
* The name of the configuration to read the value from.
*
* @param {string} sid
* The name or ID of the section to read.
*
* @param {string} [option]
* The option name to read the value from. If the option name is
* omitted or `null`, the entire section is returned instead.
*
* @returns {null|string|string[]|LuCI.uci.SectionObject}
* - Returns a string containing the option value in case of a
* plain UCI option.
* - Returns an array of strings containing the option values in
* case of `option` pointing to an UCI list.
* - Returns a {@link LuCI.uci.SectionObject section object} if
* the `option` argument has been omitted or is `null`.
* - Returns `null` if the config, section or option has not been
* found or if the corresponding configuration is not loaded.
*/
get: function(conf, sid, opt) {
var v = this.state.values,
n = this.state.creates,
c = this.state.changes,
d = this.state.deletes;
sid = this.resolveSID(conf, sid);
if (sid == null)
return null;
/* requested option in a just created section */
if (n[conf] && n[conf][sid]) {
if (!n[conf])
return null;
if (opt == null)
return n[conf][sid];
return n[conf][sid][opt];
}
/* requested an option value */
if (opt != null) {
/* check whether option was deleted */
if (d[conf] && d[conf][sid])
if (d[conf][sid] === true || d[conf][sid][opt])
return null;
/* check whether option was changed */
if (c[conf] && c[conf][sid] && c[conf][sid][opt] != null)
return c[conf][sid][opt];
/* return base value */
if (v[conf] && v[conf][sid])
return v[conf][sid][opt];
return null;
}
/* requested an entire section */
if (v[conf]) {
/* check whether entire section was deleted */
if (d[conf] && d[conf][sid] === true)
return null;
var s = v[conf][sid] || null;
if (s) {
/* merge changes */
if (c[conf] && c[conf][sid])
for (var opt in c[conf][sid])
if (c[conf][sid][opt] != null)
s[opt] = c[conf][sid][opt];
/* merge deletions */
if (d[conf] && d[conf][sid])
for (var opt in d[conf][sid])
delete s[opt];
}
return s;
}
return null;
},
/**
* Sets the value of the given option within the specified section
* of the given configuration.
*
* If either config, section or option is null, or if `option` begins
* with a dot, the function will do nothing.
*
* @param {string} config
* The name of the configuration to set the option value in.
*
* @param {string} sid
* The name or ID of the section to set the option value in.
*
* @param {string} option
* The option name to set the value for.
*
* @param {null|string|string[]} value
* The option value to set. If the value is `null` or an empty string,
* the option will be removed, otherwise it will be set or overwritten
* with the given value.
*/
set: function(conf, sid, opt, val) {
var v = this.state.values,
n = this.state.creates,
c = this.state.changes,
d = this.state.deletes;
sid = this.resolveSID(conf, sid);
if (sid == null || opt == null || opt.charAt(0) == '.')
return;
if (n[conf] && n[conf][sid]) {
if (val != null)
n[conf][sid][opt] = val;
else
delete n[conf][sid][opt];
}
else if (val != null && val !== '') {
/* do not set within deleted section */
if (d[conf] && d[conf][sid] === true)
return;
/* only set in existing sections */
if (!v[conf] || !v[conf][sid])
return;
if (!c[conf])
c[conf] = {};
if (!c[conf][sid])
c[conf][sid] = {};
/* undelete option */
if (d[conf] && d[conf][sid]) {
if (isEmpty(d[conf][sid], opt))
delete d[conf][sid];
else
delete d[conf][sid][opt];
}
c[conf][sid][opt] = val;
}
else {
/* revert any change for to-be-deleted option */
if (c[conf] && c[conf][sid]) {
if (isEmpty(c[conf][sid], opt))
delete c[conf][sid];
else
delete c[conf][sid][opt];
}
/* only delete existing options */
if (v[conf] && v[conf][sid] && v[conf][sid].hasOwnProperty(opt)) {
if (!d[conf])
d[conf] = { };
if (!d[conf][sid])
d[conf][sid] = { };
if (d[conf][sid] !== true)
d[conf][sid][opt] = true;
}
}
},
/**
* Remove the given option within the specified section of the given
* configuration.
*
* This function is a convenience wrapper around
* `uci.set(config, section, option, null)`.
*
* @param {string} config
* The name of the configuration to remove the option from.
*
* @param {string} sid
* The name or ID of the section to remove the option from.
*
* @param {string} option
* The name of the option to remove.
*/
unset: function(conf, sid, opt) {
return this.set(conf, sid, opt, null);
},
/**
* Gets the value of the given option or the entire section object of
* the first found section of the specified type or the first found
* section of the entire configuration if no type is specfied.
*
* @param {string} config
* The name of the configuration to read the value from.
*
* @param {string} [type]
* The type of the first section to find. If it is `null`, the first
* section of the entire config is read, otherwise the first section
* matching the given type.
*
* @param {string} [option]
* The option name to read the value from. If the option name is
* omitted or `null`, the entire section is returned instead.
*
* @returns {null|string|string[]|LuCI.uci.SectionObject}
* - Returns a string containing the option value in case of a
* plain UCI option.
* - Returns an array of strings containing the option values in
* case of `option` pointing to an UCI list.
* - Returns a {@link LuCI.uci.SectionObject section object} if
* the `option` argument has been omitted or is `null`.
* - Returns `null` if the config, section or option has not been
* found or if the corresponding configuration is not loaded.
*/
get_first: function(conf, type, opt) {
var sid = null;
this.sections(conf, type, function(s) {
if (sid == null)
sid = s['.name'];
});
return this.get(conf, sid, opt);
},
/**
* Sets the value of the given option within the first found section
* of the given configuration matching the specified type or within
* the first section of the entire config when no type has is specified.
*
* If either config, type or option is null, or if `option` begins
* with a dot, the function will do nothing.
*
* @param {string} config
* The name of the configuration to set the option value in.
*
* @param {string} [type]
* The type of the first section to find. If it is `null`, the first
* section of the entire config is written to, otherwise the first
* section matching the given type is used.
*
* @param {string} option
* The option name to set the value for.
*
* @param {null|string|string[]} value
* The option value to set. If the value is `null` or an empty string,
* the option will be removed, otherwise it will be set or overwritten
* with the given value.
*/
set_first: function(conf, type, opt, val) {
var sid = null;
this.sections(conf, type, function(s) {
if (sid == null)
sid = s['.name'];
});
return this.set(conf, sid, opt, val);
},
/**
* Removes the given option within the first found section of the given
* configuration matching the specified type or within the first section
* of the entire config when no type has is specified.
*
* This function is a convenience wrapper around
* `uci.set_first(config, type, option, null)`.
*
* @param {string} config
* The name of the configuration to set the option value in.
*
* @param {string} [type]
* The type of the first section to find. If it is `null`, the first
* section of the entire config is written to, otherwise the first
* section matching the given type is used.
*
* @param {string} option
* The option name to set the value for.
*/
unset_first: function(conf, type, opt) {
return this.set_first(conf, type, opt, null);
},
/**
* Move the first specified section within the given configuration
* before or after the second specified section.
*
* @param {string} config
* The configuration to move the section within.
*
* @param {string} sid1
* The ID of the section to move within the configuration.
*
* @param {string} [sid2]
* The ID of the target section for the move operation. If the
* `after` argument is `false` or not specified, the section named by
* `sid1` will be moved before this target section, if the `after`
* argument is `true`, the `sid1` section will be moved after this
* section.
*
* When the `sid2` argument is `null`, the section specified by `sid1`
* is moved to the end of the configuration.
*
* @param {boolean} [after=false]
* When `true`, the section `sid1` is moved after the section `sid2`,
* when `false`, the section `sid1` is moved before `sid2`.
*
* If `sid2` is null, then this parameter has no effect and the section
* `sid1` is moved to the end of the configuration instead.
*
* @returns {boolean}
* Returns `true` when the section was successfully moved, or `false`
* when either the section specified by `sid1` or by `sid2` is not found.
*/
move: function(conf, sid1, sid2, after) {
var sa = this.sections(conf),
s1 = null, s2 = null;
sid1 = this.resolveSID(conf, sid1);
sid2 = this.resolveSID(conf, sid2);
for (var i = 0; i < sa.length; i++) {
if (sa[i]['.name'] != sid1)
continue;
s1 = sa[i];
sa.splice(i, 1);
break;
}
if (s1 == null)
return false;
if (sid2 == null) {
sa.push(s1);
}
else {
for (var i = 0; i < sa.length; i++) {
if (sa[i]['.name'] != sid2)
continue;
s2 = sa[i];
sa.splice(i + !!after, 0, s1);
break;
}
if (s2 == null)
return false;
}
for (var i = 0; i < sa.length; i++)
this.get(conf, sa[i]['.name'])['.index'] = i;
this.state.reorder[conf] = true;
return true;
},
/**
* Submits all local configuration changes to the remove `ubus` api,
* adds, removes and reorders remote sections as needed and reloads
* all loaded configurations to resynchronize the local state with
* the remote configuration values.
*
* @returns {string[]}
* Returns a promise resolving to an array of configuration names which
* have been reloaded by the save operation.
*/
save: function() {
var v = this.state.values,
n = this.state.creates,
c = this.state.changes,
d = this.state.deletes,
r = this.state.reorder,
self = this,
snew = [ ],
pkgs = { },
tasks = [];
if (d)
for (var conf in d) {
for (var sid in d[conf]) {
var o = d[conf][sid];
if (o === true)
tasks.push(self.callDelete(conf, sid, null));
else
tasks.push(self.callDelete(conf, sid, Object.keys(o)));
}
pkgs[conf] = true;
}
if (n)
for (var conf in n) {
for (var sid in n[conf]) {
var p = {
config: conf,
values: { }
};
for (var k in n[conf][sid]) {
if (k == '.type')
p.type = n[conf][sid][k];
else if (k == '.create')
p.name = n[conf][sid][k];
else if (k.charAt(0) != '.')
p.values[k] = n[conf][sid][k];
}
snew.push(n[conf][sid]);
tasks.push(self.callAdd(p.config, p.type, p.name, p.values));
}
pkgs[conf] = true;
}
if (c)
for (var conf in c) {
for (var sid in c[conf])
tasks.push(self.callSet(conf, sid, c[conf][sid]));
pkgs[conf] = true;
}
if (r)
for (var conf in r)
pkgs[conf] = true;
return Promise.all(tasks).then(function(responses) {
/*
array "snew" holds references to the created uci sections,
use it to assign the returned names of the new sections
*/
for (var i = 0; i < snew.length; i++)
snew[i]['.name'] = responses[i];
return self.reorderSections();
}).then(function() {
pkgs = Object.keys(pkgs);
self.unload(pkgs);
return self.load(pkgs);
});
},
/**
* Instructs the remote `ubus` UCI api to commit all saved changes with
* rollback protection and attempts to confirm the pending commit
* operation to cancel the rollback timer.
*
* @param {number} [timeout=10]
* Override the confirmation timeout after which a rollback is triggered.
*
* @returns {Promise<number>}
* Returns a promise resolving/rejecting with the `ubus` RPC status code.
*/
apply: function(timeout) {
var self = this,
date = new Date();
if (typeof(timeout) != 'number' || timeout < 1)
timeout = 10;
return self.callApply(timeout, true).then(function(rv) {
if (rv != 0)
return Promise.reject(rv);
var try_deadline = date.getTime() + 1000 * timeout;
var try_confirm = function() {
return self.callConfirm().then(function(rv) {
if (rv != 0) {
if (date.getTime() < try_deadline)
window.setTimeout(try_confirm, 250);
else
return Promise.reject(rv);
}
return rv;
});
};
window.setTimeout(try_confirm, 1000);
});
},
/**
* An UCI change record is a plain array containing the change operation
* name as first element, the affected section ID as second argument
* and an optional third and fourth argument whose meanings depend on
* the operation.
*
* @typedef {string[]} ChangeRecord
* @memberof LuCI.uci
*
* @property {string} 0
* The operation name - may be one of `add`, `set`, `remove`, `order`,
* `list-add`, `list-del` or `rename`.
*
* @property {string} 1
* The section ID targeted by the operation.
*
* @property {string} 2
* The meaning of the third element depends on the operation.
* - For `add` it is type of the section that has been added
* - For `set` it either is the option name if a fourth element exists,
* or the type of a named section which has been added when the change
* entry only contains three elements.
* - For `remove` it contains the name of the option that has been
* removed.
* - For `order` it specifies the new sort index of the section.
* - For `list-add` it contains the name of the list option a new value
* has been added to.
* - For `list-del` it contains the name of the list option a value has
* been removed from.
* - For `rename` it contains the name of the option that has been
* renamed if a fourth element exists, else it contains the new name
* a section has been renamed to if the change entry only contains
* three elements.
*
* @property {string} 4
* The meaning of the fourth element depends on the operation.
* - For `set` it is the value an option has been set to.
* - For `list-add` it is the new value that has been added to a
* list option.
* - For `rename` it is the new name of an option that has been
* renamed.
*/
/**
* Fetches uncommitted UCI changes from the remote `ubus` RPC api.
*
* @method
* @returns {Promise<Object<string, Array<LuCI.uci.ChangeRecord>>>}
* Returns a promise resolving to an object containing the configuration
* names as keys and arrays of related change records as values.
*/
changes: rpc.declare({
object: 'uci',
method: 'changes',
expect: { changes: { } }
})
});

File diff suppressed because it is too large Load Diff

@ -0,0 +1,614 @@
'use strict';
'require baseclass';
function bytelen(x) {
return new Blob([x]).size;
}
var Validator = baseclass.extend({
__name__: 'Validation',
__init__: function(field, type, optional, vfunc, validatorFactory) {
this.field = field;
this.optional = optional;
this.vfunc = vfunc;
this.vstack = validatorFactory.compile(type);
this.factory = validatorFactory;
},
assert: function(condition, message) {
if (!condition) {
this.field.classList.add('cbi-input-invalid');
this.error = message;
return false;
}
this.field.classList.remove('cbi-input-invalid');
this.error = null;
return true;
},
apply: function(name, value, args) {
var func;
if (typeof(name) === 'function')
func = name;
else if (typeof(this.factory.types[name]) === 'function')
func = this.factory.types[name];
else
return false;
if (value != null)
this.value = value;
return func.apply(this, args);
},
validate: function() {
/* element is detached */
if (!findParent(this.field, 'body') && !findParent(this.field, '[data-field]'))
return true;
this.field.classList.remove('cbi-input-invalid');
this.value = (this.field.value != null) ? this.field.value : '';
this.error = null;
var valid;
if (this.value.length === 0)
valid = this.assert(this.optional, _('non-empty value'));
else
valid = this.vstack[0].apply(this, this.vstack[1]);
if (valid !== true) {
var message = _('Expecting: %s').format(this.error);
this.field.setAttribute('data-tooltip', message);
this.field.setAttribute('data-tooltip-style', 'error');
this.field.dispatchEvent(new CustomEvent('validation-failure', {
bubbles: true,
detail: {
message: message
}
}));
return false;
}
if (typeof(this.vfunc) == 'function')
valid = this.vfunc(this.value);
if (valid !== true) {
this.assert(false, valid);
this.field.setAttribute('data-tooltip', valid);
this.field.setAttribute('data-tooltip-style', 'error');
this.field.dispatchEvent(new CustomEvent('validation-failure', {
bubbles: true,
detail: {
message: valid
}
}));
return false;
}
this.field.removeAttribute('data-tooltip');
this.field.removeAttribute('data-tooltip-style');
this.field.dispatchEvent(new CustomEvent('validation-success', { bubbles: true }));
return true;
},
});
var ValidatorFactory = baseclass.extend({
__name__: 'ValidatorFactory',
create: function(field, type, optional, vfunc) {
return new Validator(field, type, optional, vfunc, this);
},
compile: function(code) {
var pos = 0;
var esc = false;
var depth = 0;
var stack = [ ];
code += ',';
for (var i = 0; i < code.length; i++) {
if (esc) {
esc = false;
continue;
}
switch (code.charCodeAt(i))
{
case 92:
esc = true;
break;
case 40:
case 44:
if (depth <= 0) {
if (pos < i) {
var label = code.substring(pos, i);
label = label.replace(/\\(.)/g, '$1');
label = label.replace(/^[ \t]+/g, '');
label = label.replace(/[ \t]+$/g, '');
if (label && !isNaN(label)) {
stack.push(parseFloat(label));
}
else if (label.match(/^(['"]).*\1$/)) {
stack.push(label.replace(/^(['"])(.*)\1$/, '$2'));
}
else if (typeof this.types[label] == 'function') {
stack.push(this.types[label]);
stack.push(null);
}
else {
L.raise('SyntaxError', 'Unhandled token "%s"', label);
}
}
pos = i+1;
}
depth += (code.charCodeAt(i) == 40);
break;
case 41:
if (--depth <= 0) {
if (typeof stack[stack.length-2] != 'function')
L.raise('SyntaxError', 'Argument list follows non-function');
stack[stack.length-1] = this.compile(code.substring(pos, i));
pos = i+1;
}
break;
}
}
return stack;
},
parseInteger: function(x) {
return (/^-?\d+$/.test(x) ? +x : NaN);
},
parseDecimal: function(x) {
return (/^-?\d+(?:\.\d+)?$/.test(x) ? +x : NaN);
},
parseIPv4: function(x) {
if (!x.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/))
return null;
if (RegExp.$1 > 255 || RegExp.$2 > 255 || RegExp.$3 > 255 || RegExp.$4 > 255)
return null;
return [ +RegExp.$1, +RegExp.$2, +RegExp.$3, +RegExp.$4 ];
},
parseIPv6: function(x) {
if (x.match(/^([a-fA-F0-9:]+):(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/)) {
var v6 = RegExp.$1, v4 = this.parseIPv4(RegExp.$2);
if (!v4)
return null;
x = v6 + ':' + (v4[0] * 256 + v4[1]).toString(16)
+ ':' + (v4[2] * 256 + v4[3]).toString(16);
}
if (!x.match(/^[a-fA-F0-9:]+$/))
return null;
var prefix_suffix = x.split(/::/);
if (prefix_suffix.length > 2)
return null;
var prefix = (prefix_suffix[0] || '0').split(/:/);
var suffix = prefix_suffix.length > 1 ? (prefix_suffix[1] || '0').split(/:/) : [];
if (suffix.length ? (prefix.length + suffix.length > 7)
: ((prefix_suffix.length < 2 && prefix.length < 8) || prefix.length > 8))
return null;
var i, word;
var words = [];
for (i = 0, word = parseInt(prefix[0], 16); i < prefix.length; word = parseInt(prefix[++i], 16))
if (prefix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
words.push(word);
else
return null;
for (i = 0; i < (8 - prefix.length - suffix.length); i++)
words.push(0);
for (i = 0, word = parseInt(suffix[0], 16); i < suffix.length; word = parseInt(suffix[++i], 16))
if (suffix[i].length <= 4 && !isNaN(word) && word <= 0xFFFF)
words.push(word);
else
return null;
return words;
},
types: {
integer: function() {
return this.assert(!isNaN(this.factory.parseInteger(this.value)), _('valid integer value'));
},
uinteger: function() {
return this.assert(this.factory.parseInteger(this.value) >= 0, _('positive integer value'));
},
float: function() {
return this.assert(!isNaN(this.factory.parseDecimal(this.value)), _('valid decimal value'));
},
ufloat: function() {
return this.assert(this.factory.parseDecimal(this.value) >= 0, _('positive decimal value'));
},
ipaddr: function(nomask) {
return this.assert(this.apply('ip4addr', null, [nomask]) || this.apply('ip6addr', null, [nomask]),
nomask ? _('valid IP address') : _('valid IP address or prefix'));
},
ip4addr: function(nomask) {
var re = nomask ? /^(\d+\.\d+\.\d+\.\d+)$/ : /^(\d+\.\d+\.\d+\.\d+)(?:\/(\d+\.\d+\.\d+\.\d+)|\/(\d{1,2}))?$/,
m = this.value.match(re);
return this.assert(m && this.factory.parseIPv4(m[1]) && (m[2] ? this.factory.parseIPv4(m[2]) : (m[3] ? this.apply('ip4prefix', m[3]) : true)),
nomask ? _('valid IPv4 address') : _('valid IPv4 address or network'));
},
ip6addr: function(nomask) {
var re = nomask ? /^([0-9a-fA-F:.]+)$/ : /^([0-9a-fA-F:.]+)(?:\/(\d{1,3}))?$/,
m = this.value.match(re);
return this.assert(m && this.factory.parseIPv6(m[1]) && (m[2] ? this.apply('ip6prefix', m[2]) : true),
nomask ? _('valid IPv6 address') : _('valid IPv6 address or prefix'));
},
ip4prefix: function() {
return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 32,
_('valid IPv4 prefix value (0-32)'));
},
ip6prefix: function() {
return this.assert(!isNaN(this.value) && this.value >= 0 && this.value <= 128,
_('valid IPv6 prefix value (0-128)'));
},
cidr: function(negative) {
return this.assert(this.apply('cidr4', null, [negative]) || this.apply('cidr6', null, [negative]),
_('valid IPv4 or IPv6 CIDR'));
},
cidr4: function(negative) {
var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(-)?(\d{1,2})$/);
return this.assert(m && this.factory.parseIPv4(m[1]) && (negative || !m[2]) && this.apply('ip4prefix', m[3]),
_('valid IPv4 CIDR'));
},
cidr6: function(negative) {
var m = this.value.match(/^([0-9a-fA-F:.]+)\/(-)?(\d{1,3})$/);
return this.assert(m && this.factory.parseIPv6(m[1]) && (negative || !m[2]) && this.apply('ip6prefix', m[3]),
_('valid IPv6 CIDR'));
},
ipnet4: function() {
var m = this.value.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
return this.assert(m && this.factory.parseIPv4(m[1]) && this.factory.parseIPv4(m[2]), _('IPv4 network in address/netmask notation'));
},
ipnet6: function() {
var m = this.value.match(/^([0-9a-fA-F:.]+)\/([0-9a-fA-F:.]+)$/);
return this.assert(m && this.factory.parseIPv6(m[1]) && this.factory.parseIPv6(m[2]), _('IPv6 network in address/netmask notation'));
},
ip6hostid: function() {
if (this.value == "eui64" || this.value == "random")
return true;
var v6 = this.factory.parseIPv6(this.value);
return this.assert(!(!v6 || v6[0] || v6[1] || v6[2] || v6[3]), _('valid IPv6 host id'));
},
ipmask: function(negative) {
return this.assert(this.apply('ipmask4', null, [negative]) || this.apply('ipmask6', null, [negative]),
_('valid network in address/netmask notation'));
},
ipmask4: function(negative) {
return this.assert(this.apply('cidr4', null, [negative]) || this.apply('ipnet4') || this.apply('ip4addr'),
_('valid IPv4 network'));
},
ipmask6: function(negative) {
return this.assert(this.apply('cidr6', null, [negative]) || this.apply('ipnet6') || this.apply('ip6addr'),
_('valid IPv6 network'));
},
port: function() {
var p = this.factory.parseInteger(this.value);
return this.assert(p >= 0 && p <= 65535, _('valid port value'));
},
portrange: function() {
if (this.value.match(/^(\d+)-(\d+)$/)) {
var p1 = +RegExp.$1;
var p2 = +RegExp.$2;
return this.assert(p1 <= p2 && p2 <= 65535,
_('valid port or port range (port1-port2)'));
}
return this.assert(this.apply('port'), _('valid port or port range (port1-port2)'));
},
macaddr: function(multicast) {
var m = this.value.match(/^([a-fA-F0-9]{2}):([a-fA-F0-9]{2}:){4}[a-fA-F0-9]{2}$/);
return this.assert(m != null && !(+m[1] & 1) == !multicast,
multicast ? _('valid multicast MAC address') : _('valid MAC address'));
},
host: function(ipv4only) {
return this.assert(this.apply('hostname') || this.apply(ipv4only == 1 ? 'ip4addr' : 'ipaddr', null, ['nomask']),
_('valid hostname or IP address'));
},
hostname: function(strict) {
if (this.value.length <= 253)
return this.assert(
(this.value.match(/^[a-zA-Z0-9_]+$/) != null ||
(this.value.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) &&
this.value.match(/[^0-9.]/))) &&
(!strict || !this.value.match(/^_/)),
_('valid hostname'));
return this.assert(false, _('valid hostname'));
},
network: function() {
return this.assert(this.apply('uciname') || this.apply('hostname') || this.apply('ip4addr') || this.apply('ip6addr'),
_('valid UCI identifier, hostname or IP address range'));
},
hostport: function(ipv4only) {
var hp = this.value.split(/:/);
return this.assert(hp.length == 2 && this.apply('host', hp[0], [ipv4only]) && this.apply('port', hp[1]),
_('valid host:port'));
},
ip4addrport: function() {
var hp = this.value.split(/:/);
return this.assert(hp.length == 2 && this.apply('ip4addr', hp[0], [true]) && this.apply('port', hp[1]),
_('valid IPv4 address:port'));
},
ipaddrport: function(bracket) {
var m4 = this.value.match(/^([^\[\]:]+):(\d+)$/),
m6 = this.value.match((bracket == 1) ? /^\[(.+)\]:(\d+)$/ : /^([^\[\]]+):(\d+)$/);
if (m4)
return this.assert(this.apply('ip4addr', m4[1], [true]) && this.apply('port', m4[2]),
_('valid address:port'));
return this.assert(m6 && this.apply('ip6addr', m6[1], [true]) && this.apply('port', m6[2]),
_('valid address:port'));
},
wpakey: function() {
var v = this.value;
if (v.length == 64)
return this.assert(v.match(/^[a-fA-F0-9]{64}$/), _('valid hexadecimal WPA key'));
return this.assert((v.length >= 8) && (v.length <= 63), _('key between 8 and 63 characters'));
},
wepkey: function() {
var v = this.value;
if (v.substr(0, 2) === 's:')
v = v.substr(2);
if ((v.length == 10) || (v.length == 26))
return this.assert(v.match(/^[a-fA-F0-9]{10,26}$/), _('valid hexadecimal WEP key'));
return this.assert((v.length === 5) || (v.length === 13), _('key with either 5 or 13 characters'));
},
uciname: function() {
return this.assert(this.value.match(/^[a-zA-Z0-9_]+$/), _('valid UCI identifier'));
},
netdevname: function() {
var v = this.value;
if (v == '.' || v == '..')
return this.assert(false, _('valid network device name, not "." or ".."'));
return this.assert(v.match(/^[^:\/%\s]{1,15}$/), _('valid network device name between 1 and 15 characters not containing ":", "/", "%" or spaces'));
},
range: function(min, max) {
var val = this.factory.parseDecimal(this.value);
return this.assert(val >= +min && val <= +max, _('value between %f and %f').format(min, max));
},
min: function(min) {
return this.assert(this.factory.parseDecimal(this.value) >= +min, _('value greater or equal to %f').format(min));
},
max: function(max) {
return this.assert(this.factory.parseDecimal(this.value) <= +max, _('value smaller or equal to %f').format(max));
},
length: function(len) {
return this.assert(bytelen(this.value) == +len,
_('value with %d characters').format(len));
},
rangelength: function(min, max) {
var len = bytelen(this.value);
return this.assert((len >= +min) && (len <= +max),
_('value between %d and %d characters').format(min, max));
},
minlength: function(min) {
return this.assert(bytelen(this.value) >= +min,
_('value with at least %d characters').format(min));
},
maxlength: function(max) {
return this.assert(bytelen(this.value) <= +max,
_('value with at most %d characters').format(max));
},
or: function() {
var errors = [];
for (var i = 0; i < arguments.length; i += 2) {
if (typeof arguments[i] != 'function') {
if (arguments[i] == this.value)
return this.assert(true);
errors.push('"%s"'.format(arguments[i]));
i--;
}
else if (arguments[i].apply(this, arguments[i+1])) {
return this.assert(true);
}
else {
errors.push(this.error);
}
}
var t = _('One of the following: %s');
return this.assert(false, t.format('\n - ' + errors.join('\n - ')));
},
and: function() {
for (var i = 0; i < arguments.length; i += 2) {
if (typeof arguments[i] != 'function') {
if (arguments[i] != this.value)
return this.assert(false, '"%s"'.format(arguments[i]));
i--;
}
else if (!arguments[i].apply(this, arguments[i+1])) {
return this.assert(false, this.error);
}
}
return this.assert(true);
},
neg: function() {
this.value = this.value.replace(/^[ \t]*![ \t]*/, '');
if (arguments[0].apply(this, arguments[1]))
return this.assert(true);
return this.assert(false, _('Potential negation of: %s').format(this.error));
},
list: function(subvalidator, subargs) {
this.field.setAttribute('data-is-list', 'true');
var tokens = this.value.match(/[^ \t]+/g);
for (var i = 0; i < tokens.length; i++)
if (!this.apply(subvalidator, tokens[i], subargs))
return this.assert(false, this.error);
return this.assert(true);
},
phonedigit: function() {
return this.assert(this.value.match(/^[0-9\*#!\.]+$/),
_('valid phone digit (0-9, "*", "#", "!" or ".")'));
},
timehhmmss: function() {
return this.assert(this.value.match(/^[0-6][0-9]:[0-6][0-9]:[0-6][0-9]$/),
_('valid time (HH:MM:SS)'));
},
dateyyyymmdd: function() {
if (this.value.match(/^(\d\d\d\d)-(\d\d)-(\d\d)/)) {
var year = +RegExp.$1,
month = +RegExp.$2,
day = +RegExp.$3,
days_in_month = [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 ];
var is_leap_year = function(year) {
return ((!(year % 4) && (year % 100)) || !(year % 400));
}
var get_days_in_month = function(month, year) {
return (month === 2 && is_leap_year(year)) ? 29 : days_in_month[month - 1];
}
/* Firewall rules in the past don't make sense */
return this.assert(year >= 2015 && month && month <= 12 && day && day <= get_days_in_month(month, year),
_('valid date (YYYY-MM-DD)'));
}
return this.assert(false, _('valid date (YYYY-MM-DD)'));
},
unique: function(subvalidator, subargs) {
var ctx = this,
option = findParent(ctx.field, '[data-widget][data-name]'),
section = findParent(option, '.cbi-section'),
query = '[data-widget="%s"][data-name="%s"]'.format(option.getAttribute('data-widget'), option.getAttribute('data-name')),
unique = true;
section.querySelectorAll(query).forEach(function(sibling) {
if (sibling === option)
return;
var input = sibling.querySelector('[data-type]'),
values = input ? (input.getAttribute('data-is-list') ? input.value.match(/[^ \t]+/g) : [ input.value ]) : null;
if (values !== null && values.indexOf(ctx.value) !== -1)
unique = false;
});
if (!unique)
return this.assert(false, _('unique value'));
if (typeof(subvalidator) === 'function')
return this.apply(subvalidator, null, subargs);
return this.assert(true);
},
hexstring: function() {
return this.assert(this.value.match(/^([a-f0-9][a-f0-9]|[A-F0-9][A-F0-9])+$/),
_('hexadecimal encoded value'));
},
string: function() {
return true;
},
directory: function() {
return true;
},
file: function() {
return true;
},
device: function() {
return true;
}
}
});
return ValidatorFactory;

@ -0,0 +1 @@
/* replaced by luci.js */

@ -0,0 +1,12 @@
-- 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.
local config = require "luci.config"
local ccache = require "luci.ccache"
module "luci.cacheloader"
if config.ccache and config.ccache.enable == "1" then
ccache.cache_ondemand()
end

@ -0,0 +1,76 @@
-- 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.
local io = require "io"
local fs = require "nixio.fs"
local util = require "luci.util"
local nixio = require "nixio"
local debug = require "debug"
local string = require "string"
local package = require "package"
local type, loadfile = type, loadfile
module "luci.ccache"
function cache_ondemand(...)
if debug.getinfo(1, 'S').source ~= "=?" then
cache_enable(...)
end
end
function cache_enable(cachepath, mode)
cachepath = cachepath or "/tmp/luci-modulecache"
mode = mode or "r--r--r--"
local loader = package.loaders[2]
local uid = nixio.getuid()
if not fs.stat(cachepath) then
fs.mkdir(cachepath)
end
local function _encode_filename(name)
local encoded = ""
for i=1, #name do
encoded = encoded .. ("%2X" % string.byte(name, i))
end
return encoded
end
local function _load_sane(file)
local stat = fs.stat(file)
if stat and stat.uid == uid and stat.modestr == mode then
return loadfile(file)
end
end
local function _write_sane(file, func)
if nixio.getuid() == uid then
local fp = io.open(file, "w")
if fp then
fp:write(util.get_bytecode(func))
fp:close()
fs.chmod(file, mode)
end
end
end
package.loaders[2] = function(mod)
local encoded = cachepath .. "/" .. _encode_filename(mod)
local modcons = _load_sane(encoded)
if modcons then
return modcons
end
-- No cachefile
modcons = loader(mod)
if type(modcons) == "function" then
_write_sane(encoded, modcons)
end
return modcons
end
end

@ -0,0 +1,18 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Licensed to the public under the Apache License 2.0.
local util = require "luci.util"
module("luci.config",
function(m)
if pcall(require, "luci.model.uci") then
local config = util.threadlocal()
setmetatable(m, {
__index = function(tbl, key)
if not config[key] then
config[key] = luci.model.uci.cursor():get_all("luci", key)
end
return config[key]
end
})
end
end)

@ -0,0 +1,199 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Licensed to the public under the Apache License 2.0.
module("luci.controller.admin.index", package.seeall)
function action_logout()
local dsp = require "luci.dispatcher"
local utl = require "luci.util"
local sid = dsp.context.authsession
if sid then
utl.ubus("session", "destroy", { ubus_rpc_session = sid })
local url = dsp.build_url()
if luci.http.getenv('HTTPS') == 'on' then
luci.http.header("Set-Cookie", "sysauth_https=; expires=Thu, 01 Jan 1970 01:00:00 GMT; path=%s" % url)
end
luci.http.header("Set-Cookie", "sysauth_http=; expires=Thu, 01 Jan 1970 01:00:00 GMT; path=%s" % url)
end
luci.http.redirect(dsp.build_url())
end
function action_translations(lang)
local i18n = require "luci.i18n"
local http = require "luci.http"
local fs = require "nixio".fs
if lang and #lang > 0 then
lang = i18n.setlanguage(lang)
if lang then
local s = fs.stat("%s/base.%s.lmo" %{ i18n.i18ndir, lang })
if s then
http.header("Cache-Control", "public, max-age=31536000")
http.header("ETag", "%x-%x-%x" %{ s["ino"], s["size"], s["mtime"] })
end
end
end
http.prepare_content("application/javascript; charset=utf-8")
http.write("window.TR=")
http.write_json(i18n.dump())
end
local function ubus_reply(id, data, code, errmsg)
local reply = { jsonrpc = "2.0", id = id }
if errmsg then
reply.error = {
code = code,
message = errmsg
}
elseif type(code) == "table" then
reply.result = code
else
reply.result = { code, data }
end
return reply
end
local ubus_types = {
nil,
"array",
"object",
"string",
nil, -- INT64
"number",
nil, -- INT16,
"boolean",
"double"
}
local function ubus_access(sid, obj, fun)
local res, code = luci.util.ubus("session", "access", {
ubus_rpc_session = sid,
scope = "ubus",
object = obj,
["function"] = fun
})
return (type(res) == "table" and res.access == true)
end
local function ubus_request(req)
if type(req) ~= "table" or type(req.method) ~= "string" or req.jsonrpc ~= "2.0" or req.id == nil then
return ubus_reply(nil, nil, -32600, "Invalid request")
elseif req.method == "call" then
if type(req.params) ~= "table" or #req.params < 3 then
return ubus_reply(nil, nil, -32600, "Invalid parameters")
end
local sid, obj, fun, arg =
req.params[1], req.params[2], req.params[3], req.params[4] or {}
if type(arg) ~= "table" or arg.ubus_rpc_session ~= nil then
return ubus_reply(req.id, nil, -32602, "Invalid parameters")
end
if sid == "00000000000000000000000000000000" and luci.dispatcher.context.authsession then
sid = luci.dispatcher.context.authsession
end
if not ubus_access(sid, obj, fun) then
return ubus_reply(req.id, nil, -32002, "Access denied")
end
arg.ubus_rpc_session = sid
local res, code = luci.util.ubus(obj, fun, arg)
return ubus_reply(req.id, res, code or 0)
elseif req.method == "list" then
if req.params == nil or (type(req.params) == "table" and #req.params == 0) then
local objs = luci.util.ubus()
return ubus_reply(req.id, nil, objs)
elseif type(req.params) == "table" then
local n, rv = nil, {}
for n = 1, #req.params do
if type(req.params[n]) ~= "string" then
return ubus_reply(req.id, nil, -32602, "Invalid parameters")
end
local sig = luci.util.ubus(req.params[n])
if sig and type(sig) == "table" then
rv[req.params[n]] = {}
local m, p
for m, p in pairs(sig) do
if type(p) == "table" then
rv[req.params[n]][m] = {}
local pn, pt
for pn, pt in pairs(p) do
rv[req.params[n]][m][pn] = ubus_types[pt] or "unknown"
end
end
end
end
end
return ubus_reply(req.id, nil, rv)
else
return ubus_reply(req.id, nil, -32602, "Invalid parameters")
end
end
return ubus_reply(req.id, nil, -32601, "Method not found")
end
function action_ubus()
local parser = require "luci.jsonc".new()
luci.http.context.request:setfilehandler(function(_, s)
if not s then
return nil
end
local ok, err = parser:parse(s)
return (not err or nil)
end)
luci.http.context.request:content()
local json = parser:get()
if json == nil or type(json) ~= "table" then
luci.http.prepare_content("application/json")
luci.http.write_json(ubus_reply(nil, nil, -32700, "Parse error"))
return
end
local response
if #json == 0 then
response = ubus_request(json)
else
response = {}
local _, request
for _, request in ipairs(json) do
response[_] = ubus_request(request)
end
end
luci.http.prepare_content("application/json")
luci.http.write_json(response)
end
function action_menu()
local dsp = require "luci.dispatcher"
local http = require "luci.http"
local _, _, acls = dsp.is_authenticated({ methods = { "cookie:sysauth_https", "cookie:sysauth_http" } })
local menu = dsp.menu_json(acls or {}) or {}
http.prepare_content("application/json")
http.write_json(menu)
end

@ -0,0 +1,70 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Copyright 2010-2019 Jo-Philipp Wich <jo@mein.io>
-- Licensed to the public under the Apache License 2.0.
module("luci.controller.admin.uci", package.seeall)
local function ubus_state_to_http(errstr)
local map = {
["Invalid command"] = 400,
["Invalid argument"] = 400,
["Method not found"] = 404,
["Entry not found"] = 404,
["No data"] = 204,
["Permission denied"] = 403,
["Timeout"] = 504,
["Not supported"] = 500,
["Unknown error"] = 500,
["Connection failed"] = 503
}
local code = map[errstr] or 200
local msg = errstr or "OK"
luci.http.status(code, msg)
if code ~= 204 then
luci.http.prepare_content("text/plain")
luci.http.write(msg)
end
end
function action_apply_rollback()
local uci = require "luci.model.uci"
local token, errstr = uci:apply(true)
if token then
luci.http.prepare_content("application/json")
luci.http.write_json({ token = token })
else
ubus_state_to_http(errstr)
end
end
function action_apply_unchecked()
local uci = require "luci.model.uci"
local _, errstr = uci:apply(false)
ubus_state_to_http(errstr)
end
function action_confirm()
local uci = require "luci.model.uci"
local token = luci.http.formvalue("token")
local _, errstr = uci:confirm(token)
ubus_state_to_http(errstr)
end
function action_revert()
local uci = require "luci.model.uci"
local changes = uci:changes()
-- Collect files to be reverted
local _, errstr, r, tbl
for r, tbl in pairs(changes) do
_, errstr = uci:revert(r)
if errstr then
break
end
end
ubus_state_to_http(errstr or "OK")
end

File diff suppressed because it is too large Load Diff

@ -0,0 +1,220 @@
---[[
LuCI web dispatcher.
]]
module "luci.dispatcher"
---[[
Build the URL relative to the server webroot from given virtual path.
@class function
@name build_url
@param ... Virtual path
@return Relative URL
]]
---[[
Check whether a dispatch node shall be visible
@class function
@name node_visible
@param node Dispatch node
@return Boolean indicating whether the node should be visible
]]
---[[
Return a sorted table of visible children within a given node
@class function
@name node_childs
@param node Dispatch node
@return Ordered table of child node names
]]
---[[
Send a 404 error code and render the "error404" template if available.
@class function
@name error404
@param message Custom error message (optional)
@return false
]]
---[[
Send a 500 error code and render the "error500" template if available.
@class function
@name error500
@param message Custom error message (optional)#
@return false
]]
---[[
Dispatch an HTTP request.
@class function
@name httpdispatch
@param request LuCI HTTP Request object
]]
---[[
Dispatches a LuCI virtual path.
@class function
@name dispatch
@param request Virtual path
]]
---[[
Generate the dispatching index using the native file-cache based strategy.
@class function
@name createindex
]]
---[[
Create the dispatching tree from the index.
Build the index before if it does not exist yet.
@class function
@name createtree
]]
---[[
Clone a node of the dispatching tree to another position.
@class function
@name assign
@param path Virtual path destination
@param clone Virtual path source
@param title Destination node title (optional)
@param order Destination node order value (optional)
@return Dispatching tree node
]]
---[[
Create a new dispatching node and define common parameters.
@class function
@name entry
@param path Virtual path
@param target Target function to call when dispatched.
@param title Destination node title
@param order Destination node order value (optional)
@return Dispatching tree node
]]
---[[
Fetch or create a dispatching node without setting the target module or
enabling the node.
@class function
@name get
@param ... Virtual path
@return Dispatching tree node
]]
---[[
Fetch or create a new dispatching node.
@class function
@name node
@param ... Virtual path
@return Dispatching tree node
]]
---[[
Lookup node in dispatching tree.
@class function
@name lookup
@param ... Virtual path
@return Node object, canonical url or nil if the path was not found.
]]
---[[
Alias the first (lowest order) page automatically
@class function
@name firstchild
]]
---[[
Create a redirect to another dispatching node.
@class function
@name alias
@param ... Virtual path destination
]]
---[[
Rewrite the first x path values of the request.
@class function
@name rewrite
@param n Number of path values to replace
@param ... Virtual path to replace removed path values with
]]
---[[
Create a function-call dispatching target.
@class function
@name call
@param name Target function of local controller
@param ... Additional parameters passed to the function
]]
---[[
Create a template render dispatching target.
@class function
@name template
@param name Template to be rendered
]]
---[[
Create a CBI model dispatching target.
@class function
@name cbi
@param model CBI model to be rendered
]]
---[[
Create a combined dispatching target for non argv and argv requests.
@class function
@name arcombine
@param trg1 Overview Target
@param trg2 Detail Target
]]
---[[
Create a CBI form model dispatching target.
@class function
@name form
@param model CBI form model tpo be rendered
]]
---[[
Access the luci.i18n translate() api.
@class function
@name translate
@param text Text to translate
]]
---[[
No-op function used to mark translation entries for menu labels.
This function does not actually translate the given argument but
is used by build/i18n-scan.pl to find translatable entries.
@class function
@name _
]]

@ -0,0 +1,55 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Licensed to the public under the Apache License 2.0.
local tparser = require "luci.template.parser"
local util = require "luci.util"
local tostring = tostring
module "luci.i18n"
i18ndir = util.libpath() .. "/i18n/"
context = util.threadlocal()
default = "en"
function setlanguage(lang)
local code, subcode = lang:match("^([A-Za-z][A-Za-z])[%-_]([A-Za-z][A-Za-z])$")
if not (code and subcode) then
subcode = lang:match("^([A-Za-z][A-Za-z])$")
if not subcode then
return nil
end
end
context.parent = code and code:lower()
context.lang = context.parent and context.parent.."-"..subcode:lower() or subcode:lower()
if tparser.load_catalog(context.lang, i18ndir) and
tparser.change_catalog(context.lang)
then
return context.lang
elseif context.parent then
if tparser.load_catalog(context.parent, i18ndir) and
tparser.change_catalog(context.parent)
then
return context.parent
end
end
return nil
end
function translate(key)
return tparser.translate(key) or key
end
function translatef(key, ...)
return tostring(translate(key)):format(...)
end
function dump()
local rv = {}
tparser.get_translations(function(k, v) rv[k] = v end)
return rv
end

@ -0,0 +1,42 @@
---[[
LuCI translation library.
]]
module "luci.i18n"
---[[
Set the context default translation language.
@class function
@name setlanguage
@param lang An IETF/BCP 47 language tag or ISO3166 country code, e.g. "en-US" or "de"
@return The effective loaded language, e.g. "en" for "en-US" - or nil on failure
]]
---[[
Return the translated value for a specific translation key.
@class function
@name translate
@param key Default translation text
@return Translated string
]]
---[[
Return the translated value for a specific translation key and use it as sprintf pattern.
@class function
@name translatef
@param key Default translation text
@param ... Format parameters
@return Translated and formatted string
]]
---[[
Return all currently loaded translation strings as a key-value table. The key is the
hexadecimal representation of the translation key while the value is the translated
text content.
@class function
@name dump
@return Key-value translation string table.
]]

@ -0,0 +1,508 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Licensed to the public under the Apache License 2.0.
local os = require "os"
local util = require "luci.util"
local table = require "table"
local setmetatable, rawget, rawset = setmetatable, rawget, rawset
local require, getmetatable, assert = require, getmetatable, assert
local error, pairs, ipairs, select = error, pairs, ipairs, select
local type, tostring, tonumber, unpack = type, tostring, tonumber, unpack
-- The typical workflow for UCI is: Get a cursor instance from the
-- cursor factory, modify data (via Cursor.add, Cursor.delete, etc.),
-- save the changes to the staging area via Cursor.save and finally
-- Cursor.commit the data to the actual config files.
-- LuCI then needs to Cursor.apply the changes so daemons etc. are
-- reloaded.
module "luci.model.uci"
local ERRSTR = {
"Invalid command",
"Invalid argument",
"Method not found",
"Entry not found",
"No data",
"Permission denied",
"Timeout",
"Not supported",
"Unknown error",
"Connection failed"
}
local session_id = nil
local function call(cmd, args)
if type(args) == "table" and session_id then
args.ubus_rpc_session = session_id
end
return util.ubus("uci", cmd, args)
end
function cursor()
return _M
end
function cursor_state()
return _M
end
function substate(self)
return self
end
function get_confdir(self)
return "/etc/config"
end
function get_savedir(self)
return "/tmp/.uci"
end
function get_session_id(self)
return session_id
end
function set_confdir(self, directory)
return false
end
function set_savedir(self, directory)
return false
end
function set_session_id(self, id)
session_id = id
return true
end
function load(self, config)
return true
end
function save(self, config)
return true
end
function unload(self, config)
return true
end
function changes(self, config)
local rv, err = call("changes", { config = config })
if type(rv) == "table" and type(rv.changes) == "table" then
return rv.changes
elseif err then
return nil, ERRSTR[err]
else
return { }
end
end
function revert(self, config)
local _, err = call("revert", { config = config })
return (err == nil), ERRSTR[err]
end
function commit(self, config)
local _, err = call("commit", { config = config })
return (err == nil), ERRSTR[err]
end
function apply(self, rollback)
local _, err
if rollback then
local sys = require "luci.sys"
local conf = require "luci.config"
local timeout = tonumber(conf and conf.apply and conf.apply.rollback or 90) or 0
_, err = call("apply", {
timeout = (timeout > 90) and timeout or 90,
rollback = true
})
if not err then
local now = os.time()
local token = sys.uniqueid(16)
util.ubus("session", "set", {
ubus_rpc_session = "00000000000000000000000000000000",
values = {
rollback = {
token = token,
session = session_id,
timeout = now + timeout
}
}
})
return token
end
else
_, err = call("changes", {})
if not err then
if type(_) == "table" and type(_.changes) == "table" then
local k, v
for k, v in pairs(_.changes) do
_, err = call("commit", { config = k })
if err then
break
end
end
end
end
if not err then
_, err = call("apply", { rollback = false })
end
end
return (err == nil), ERRSTR[err]
end
function confirm(self, token)
local is_pending, time_remaining, rollback_sid, rollback_token = self:rollback_pending()
if is_pending then
if token ~= rollback_token then
return false, "Permission denied"
end
local _, err = util.ubus("uci", "confirm", {
ubus_rpc_session = rollback_sid
})
if not err then
util.ubus("session", "set", {
ubus_rpc_session = "00000000000000000000000000000000",
values = { rollback = {} }
})
end
return (err == nil), ERRSTR[err]
end
return false, "No data"
end
function rollback(self)
local is_pending, time_remaining, rollback_sid = self:rollback_pending()
if is_pending then
local _, err = util.ubus("uci", "rollback", {
ubus_rpc_session = rollback_sid
})
if not err then
util.ubus("session", "set", {
ubus_rpc_session = "00000000000000000000000000000000",
values = { rollback = {} }
})
end
return (err == nil), ERRSTR[err]
end
return false, "No data"
end
function rollback_pending(self)
local rv, err = util.ubus("session", "get", {
ubus_rpc_session = "00000000000000000000000000000000",
keys = { "rollback" }
})
local now = os.time()
if type(rv) == "table" and
type(rv.values) == "table" and
type(rv.values.rollback) == "table" and
type(rv.values.rollback.token) == "string" and
type(rv.values.rollback.session) == "string" and
type(rv.values.rollback.timeout) == "number" and
rv.values.rollback.timeout > now
then
return true,
rv.values.rollback.timeout - now,
rv.values.rollback.session,
rv.values.rollback.token
end
return false, ERRSTR[err]
end
function foreach(self, config, stype, callback)
if type(callback) == "function" then
local rv, err = call("get", {
config = config,
type = stype
})
if type(rv) == "table" and type(rv.values) == "table" then
local sections = { }
local res = false
local index = 1
local _, section
for _, section in pairs(rv.values) do
section[".index"] = section[".index"] or index
sections[index] = section
index = index + 1
end
table.sort(sections, function(a, b)
return a[".index"] < b[".index"]
end)
for _, section in ipairs(sections) do
local continue = callback(section)
res = true
if continue == false then
break
end
end
return res
else
return false, ERRSTR[err] or "No data"
end
else
return false, "Invalid argument"
end
end
local function _get(self, operation, config, section, option)
if section == nil then
return nil
elseif type(option) == "string" and option:byte(1) ~= 46 then
local rv, err = call(operation, {
config = config,
section = section,
option = option
})
if type(rv) == "table" then
return rv.value or nil
elseif err then
return false, ERRSTR[err]
else
return nil
end
elseif option == nil then
local values = self:get_all(config, section)
if values then
return values[".type"], values[".name"]
else
return nil
end
else
return false, "Invalid argument"
end
end
function get(self, ...)
return _get(self, "get", ...)
end
function get_state(self, ...)
return _get(self, "state", ...)
end
function get_all(self, config, section)
local rv, err = call("get", {
config = config,
section = section
})
if type(rv) == "table" and type(rv.values) == "table" then
return rv.values
elseif err then
return false, ERRSTR[err]
else
return nil
end
end
function get_bool(self, ...)
local val = self:get(...)
return (val == "1" or val == "true" or val == "yes" or val == "on")
end
function get_first(self, config, stype, option, default)
local rv = default
self:foreach(config, stype, function(s)
local val = not option and s[".name"] or s[option]
if type(default) == "number" then
val = tonumber(val)
elseif type(default) == "boolean" then
val = (val == "1" or val == "true" or
val == "yes" or val == "on")
end
if val ~= nil then
rv = val
return false
end
end)
return rv
end
function get_list(self, config, section, option)
if config and section and option then
local val = self:get(config, section, option)
return (type(val) == "table" and val or { val })
end
return { }
end
function section(self, config, stype, name, values)
local rv, err = call("add", {
config = config,
type = stype,
name = name,
values = values
})
if type(rv) == "table" then
return rv.section
elseif err then
return false, ERRSTR[err]
else
return nil
end
end
function add(self, config, stype)
return self:section(config, stype)
end
function set(self, config, section, option, ...)
if select('#', ...) == 0 then
local sname, err = self:section(config, option, section)
return (not not sname), err
else
local _, err = call("set", {
config = config,
section = section,
values = { [option] = select(1, ...) }
})
return (err == nil), ERRSTR[err]
end
end
function set_list(self, config, section, option, value)
if section == nil or option == nil then
return false
elseif value == nil or (type(value) == "table" and #value == 0) then
return self:delete(config, section, option)
elseif type(value) == "table" then
return self:set(config, section, option, value)
else
return self:set(config, section, option, { value })
end
end
function tset(self, config, section, values)
local _, err = call("set", {
config = config,
section = section,
values = values
})
return (err == nil), ERRSTR[err]
end
function reorder(self, config, section, index)
local sections
if type(section) == "string" and type(index) == "number" then
local pos = 0
sections = { }
self:foreach(config, nil, function(s)
if pos == index then
pos = pos + 1
end
if s[".name"] ~= section then
pos = pos + 1
sections[pos] = s[".name"]
else
sections[index + 1] = section
end
end)
elseif type(section) == "table" then
sections = section
else
return false, "Invalid argument"
end
local _, err = call("order", {
config = config,
sections = sections
})
return (err == nil), ERRSTR[err]
end
function delete(self, config, section, option)
local _, err = call("delete", {
config = config,
section = section,
option = option
})
return (err == nil), ERRSTR[err]
end
function delete_all(self, config, stype, comparator)
local _, err
if type(comparator) == "table" then
_, err = call("delete", {
config = config,
type = stype,
match = comparator
})
elseif type(comparator) == "function" then
local rv = call("get", {
config = config,
type = stype
})
if type(rv) == "table" and type(rv.values) == "table" then
local sname, section
for sname, section in pairs(rv.values) do
if comparator(section) then
_, err = call("delete", {
config = config,
section = sname
})
end
end
end
elseif comparator == nil then
_, err = call("delete", {
config = config,
type = stype
})
else
return false, "Invalid argument"
end
return (err == nil), ERRSTR[err]
end

@ -0,0 +1,369 @@
---[[
LuCI UCI model library.
The typical workflow for UCI is: Get a cursor instance from the
cursor factory, modify data (via Cursor.add, Cursor.delete, etc.),
save the changes to the staging area via Cursor.save and finally
Cursor.commit the data to the actual config files.
LuCI then needs to Cursor.apply the changes so daemons etc. are
reloaded.
@cstyle instance
]]
module "luci.model.uci"
---[[
Create a new UCI-Cursor.
@class function
@name cursor
@return UCI-Cursor
]]
---[[
Create a new Cursor initialized to the state directory.
@class function
@name cursor_state
@return UCI cursor
]]
---[[
Applies UCI configuration changes.
If the rollback parameter is set to true, the apply function will invoke the
rollback mechanism which causes the configuration to be automatically reverted
if no confirm() call occurs within a certain timeout.
The current default timeout is 30s and can be increased using the
"luci.apply.timeout" uci configuration key.
@class function
@name Cursor.apply
@param rollback Enable rollback mechanism
@return Boolean whether operation succeeded
]]
---[[
Confirms UCI apply process.
If a previous UCI apply with rollback has been invoked using apply(true),
this function confirms the process and cancels the pending rollback timer.
If no apply with rollback session is active, the function has no effect and
returns with a "No data" error.
@class function
@name Cursor.confirm
@return Boolean whether operation succeeded
]]
---[[
Cancels UCI apply process.
If a previous UCI apply with rollback has been invoked using apply(true),
this function cancels the process and rolls back the configuration to the
pre-apply state.
If no apply with rollback session is active, the function has no effect and
returns with a "No data" error.
@class function
@name Cursor.rollback
@return Boolean whether operation succeeded
]]
---[[
Checks whether a pending rollback is scheduled.
If a previous UCI apply with rollback has been invoked using apply(true),
and has not been confirmed or rolled back yet, this function returns true
and the remaining time until rollback in seconds. If no rollback is pending,
the function returns false. On error, the function returns false and an
additional string describing the error.
@class function
@name Cursor.rollback_pending
@return Boolean whether rollback is pending
@return Remaining time in seconds
]]
---[[
Delete all sections of a given type that match certain criteria.
@class function
@name Cursor.delete_all
@param config UCI config
@param type UCI section type
@param comparator Function that will be called for each section and returns
a boolean whether to delete the current section (optional)
]]
---[[
Create a new section and initialize it with data.
@class function
@name Cursor.section
@param config UCI config
@param type UCI section type
@param name UCI section name (optional)
@param values Table of key - value pairs to initialize the section with
@return Name of created section
]]
---[[
Updated the data of a section using data from a table.
@class function
@name Cursor.tset
@param config UCI config
@param section UCI section name (optional)
@param values Table of key - value pairs to update the section with
]]
---[[
Get a boolean option and return it's value as true or false.
@class function
@name Cursor.get_bool
@param config UCI config
@param section UCI section name
@param option UCI option
@return Boolean
]]
---[[
Get an option or list and return values as table.
@class function
@name Cursor.get_list
@param config UCI config
@param section UCI section name
@param option UCI option
@return table. If the option was not found, you will simply get an empty
table.
]]
---[[
Get the given option from the first section with the given type.
@class function
@name Cursor.get_first
@param config UCI config
@param type UCI section type
@param option UCI option (optional)
@param default Default value (optional)
@return UCI value
]]
---[[
Set given values as list. Setting a list option to an empty list
has the same effect as deleting the option.
@class function
@name Cursor.set_list
@param config UCI config
@param section UCI section name
@param option UCI option
@param value Value or table. Non-table values will be set as single
item UCI list.
@return Boolean whether operation succeeded
]]
---[[
Create a sub-state of this cursor.
The sub-state is tied to the parent cursor, means it the parent unloads or
loads configs, the sub state will do so as well.
@class function
@name Cursor.substate
@return UCI state cursor tied to the parent cursor
]]
---[[
Add an anonymous section.
@class function
@name Cursor.add
@param config UCI config
@param type UCI section type
@return Name of created section
]]
---[[
Get a table of saved but uncommitted changes.
@class function
@name Cursor.changes
@param config UCI config
@return Table of changes
@see Cursor.save
]]
---[[
Commit saved changes.
@class function
@name Cursor.commit
@param config UCI config
@return Boolean whether operation succeeded
@see Cursor.revert
@see Cursor.save
]]
---[[
Deletes a section or an option.
@class function
@name Cursor.delete
@param config UCI config
@param section UCI section name
@param option UCI option (optional)
@return Boolean whether operation succeeded
]]
---[[
Call a function for every section of a certain type.
@class function
@name Cursor.foreach
@param config UCI config
@param type UCI section type
@param callback Function to be called
@return Boolean whether operation succeeded
]]
---[[
Get a section type or an option
@class function
@name Cursor.get
@param config UCI config
@param section UCI section name
@param option UCI option (optional)
@return UCI value
]]
---[[
Get all sections of a config or all values of a section.
@class function
@name Cursor.get_all
@param config UCI config
@param section UCI section name (optional)
@return Table of UCI sections or table of UCI values
]]
---[[
Manually load a config.
@class function
@name Cursor.load
@param config UCI config
@return Boolean whether operation succeeded
@see Cursor.save
@see Cursor.unload
]]
---[[
Revert saved but uncommitted changes.
@class function
@name Cursor.revert
@param config UCI config
@return Boolean whether operation succeeded
@see Cursor.commit
@see Cursor.save
]]
---[[
Saves changes made to a config to make them committable.
@class function
@name Cursor.save
@param config UCI config
@return Boolean whether operation succeeded
@see Cursor.load
@see Cursor.unload
]]
---[[
Set a value or create a named section.
When invoked with three arguments `config`, `sectionname`, `sectiontype`,
then a named section of the given type is created.
When invoked with four arguments `config`, `sectionname`, `optionname` and
`optionvalue` then the value of the specified option is set to the given value.
@class function
@name Cursor.set
@param config UCI config
@param section UCI section name
@param option UCI option or UCI section type
@param value UCI value or nothing if you want to create a section
@return Boolean whether operation succeeded
]]
---[[
Get the configuration directory.
@class function
@name Cursor.get_confdir
@return Configuration directory
]]
---[[
Get the directory for uncomitted changes.
@class function
@name Cursor.get_savedir
@return Save directory
]]
---[[
Get the effective session ID.
@class function
@name Cursor.get_session_id
@return String containing the session ID
]]
---[[
Set the configuration directory.
@class function
@name Cursor.set_confdir
@param directory UCI configuration directory
@return Boolean whether operation succeeded
]]
---[[
Set the directory for uncommitted changes.
@class function
@name Cursor.set_savedir
@param directory UCI changes directory
@return Boolean whether operation succeeded
]]
---[[
Set the effective session ID.
@class function
@name Cursor.set_session_id
@param id String containing the session ID to set
@return Boolean whether operation succeeded
]]
---[[
Discard changes made to a config.
@class function
@name Cursor.unload
@param config UCI config
@return Boolean whether operation succeeded
@see Cursor.load
@see Cursor.save
]]

@ -0,0 +1,73 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Licensed to the public under the Apache License 2.0.
exectime = os.clock()
module("luci.sgi.cgi", package.seeall)
local ltn12 = require("luci.ltn12")
require("nixio.util")
require("luci.http")
require("luci.sys")
require("luci.dispatcher")
-- Limited source to avoid endless blocking
local function limitsource(handle, limit)
limit = limit or 0
local BLOCKSIZE = ltn12.BLOCKSIZE
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 r = luci.http.Request(
luci.sys.getenv(),
limitsource(io.stdin, tonumber(luci.sys.getenv("CONTENT_LENGTH"))),
ltn12.sink.file(io.stderr)
)
local x = coroutine.create(luci.dispatcher.httpdispatch)
local hcache = ""
local active = true
while coroutine.status(x) ~= "dead" do
local res, id, data1, data2 = coroutine.resume(x, r)
if not res then
print("Status: 500 Internal Server Error")
print("Content-Type: text/plain\n")
print(id)
break;
end
if active then
if id == 1 then
io.write("Status: " .. tostring(data1) .. " " .. data2 .. "\r\n")
elseif id == 2 then
hcache = hcache .. data1 .. ": " .. data2 .. "\r\n"
elseif id == 3 then
io.write(hcache)
io.write("\r\n")
elseif id == 4 then
io.write(tostring(data1 or ""))
elseif id == 5 then
io.flush()
io.close()
active = false
elseif id == 6 then
data1:copyz(nixio.stdout, data2)
data1:close()
end
end
end
end

@ -0,0 +1,99 @@
-- Copyright 2010 Jo-Philipp Wich <jow@openwrt.org>
-- Licensed to the public under the Apache License 2.0.
require "nixio.util"
require "luci.http"
require "luci.sys"
require "luci.dispatcher"
require "luci.ltn12"
function handle_request(env)
exectime = os.clock()
local renv = {
CONTENT_LENGTH = env.CONTENT_LENGTH,
CONTENT_TYPE = env.CONTENT_TYPE,
REQUEST_METHOD = env.REQUEST_METHOD,
REQUEST_URI = env.REQUEST_URI,
PATH_INFO = env.PATH_INFO,
SCRIPT_NAME = env.SCRIPT_NAME:gsub("/+$", ""),
SCRIPT_FILENAME = env.SCRIPT_NAME,
SERVER_PROTOCOL = env.SERVER_PROTOCOL,
QUERY_STRING = env.QUERY_STRING,
DOCUMENT_ROOT = env.DOCUMENT_ROOT,
HTTPS = env.HTTPS,
REDIRECT_STATUS = env.REDIRECT_STATUS,
REMOTE_ADDR = env.REMOTE_ADDR,
REMOTE_NAME = env.REMOTE_NAME,
REMOTE_PORT = env.REMOTE_PORT,
REMOTE_USER = env.REMOTE_USER,
SERVER_ADDR = env.SERVER_ADDR,
SERVER_NAME = env.SERVER_NAME,
SERVER_PORT = env.SERVER_PORT
}
local k, v
for k, v in pairs(env.headers) do
k = k:upper():gsub("%-", "_")
renv["HTTP_" .. k] = v
end
local len = tonumber(env.CONTENT_LENGTH) or 0
local function recv()
if len > 0 then
local rlen, rbuf = uhttpd.recv(4096)
if rlen >= 0 then
len = len - rlen
return rbuf
end
end
return nil
end
local send = uhttpd.send
local req = luci.http.Request(
renv, recv, luci.ltn12.sink.file(io.stderr)
)
local x = coroutine.create(luci.dispatcher.httpdispatch)
local hcache = { }
local active = true
while coroutine.status(x) ~= "dead" do
local res, id, data1, data2 = coroutine.resume(x, req)
if not res then
send("Status: 500 Internal Server Error\r\n")
send("Content-Type: text/plain\r\n\r\n")
send(tostring(id))
break
end
if active then
if id == 1 then
send("Status: ")
send(tostring(data1))
send(" ")
send(tostring(data2))
send("\r\n")
elseif id == 2 then
hcache[data1] = data2
elseif id == 3 then
for k, v in pairs(hcache) do
send(tostring(k))
send(": ")
send(tostring(v))
send("\r\n")
end
send("\r\n")
elseif id == 4 then
send(tostring(data1 or ""))
elseif id == 5 then
active = false
elseif id == 6 then
data1:copyz(nixio.stdout, data2)
end
end
end
end

@ -0,0 +1,6 @@
-- Copyright 2009 Steven Barth <steven@midlink.org>
-- Copyright 2009 Jo-Philipp Wich <jow@openwrt.org>
-- Licensed to the public under the Apache License 2.0.
local util = require "luci.util"
module("luci.store", util.threadlocal)

@ -0,0 +1,615 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Licensed to the public under the Apache License 2.0.
local io = require "io"
local os = require "os"
local table = require "table"
local nixio = require "nixio"
local fs = require "nixio.fs"
local uci = require "luci.model.uci"
local luci = {}
luci.util = require "luci.util"
luci.ip = require "luci.ip"
local tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack =
tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack
module "luci.sys"
function call(...)
return os.execute(...) / 256
end
exec = luci.util.exec
-- containing the whole environment is returned otherwise this function returns
-- the corresponding string value for the given name or nil if no such variable
-- exists.
getenv = nixio.getenv
function hostname(newname)
if type(newname) == "string" and #newname > 0 then
fs.writefile( "/proc/sys/kernel/hostname", newname )
return newname
else
return nixio.uname().nodename
end
end
function httpget(url, stream, target)
if not target then
local source = stream and io.popen or luci.util.exec
return source("wget -qO- %s" % luci.util.shellquote(url))
else
return os.execute("wget -qO %s %s" %
{luci.util.shellquote(target), luci.util.shellquote(url)})
end
end
function reboot()
return os.execute("reboot >/dev/null 2>&1")
end
function syslog()
return luci.util.exec("logread")
end
function dmesg()
return luci.util.exec("dmesg")
end
function uniqueid(bytes)
local rand = fs.readfile("/dev/urandom", bytes)
return rand and nixio.bin.hexlify(rand)
end
function uptime()
return nixio.sysinfo().uptime
end
net = {}
local function _nethints(what, callback)
local _, k, e, mac, ip, name, duid, iaid
local cur = uci.cursor()
local ifn = { }
local hosts = { }
local lookup = { }
local function _add(i, ...)
local k = select(i, ...)
if k then
if not hosts[k] then hosts[k] = { } end
hosts[k][1] = select(1, ...) or hosts[k][1]
hosts[k][2] = select(2, ...) or hosts[k][2]
hosts[k][3] = select(3, ...) or hosts[k][3]
hosts[k][4] = select(4, ...) or hosts[k][4]
end
end
luci.ip.neighbors(nil, function(neigh)
if neigh.mac and neigh.family == 4 then
_add(what, neigh.mac:string(), neigh.dest:string(), nil, nil)
elseif neigh.mac and neigh.family == 6 then
_add(what, neigh.mac:string(), nil, neigh.dest:string(), nil)
end
end)
if fs.access("/etc/ethers") then
for e in io.lines("/etc/ethers") do
mac, name = e:match("^([a-fA-F0-9:-]+)%s+(%S+)")
mac = luci.ip.checkmac(mac)
if mac and name then
if luci.ip.checkip4(name) then
_add(what, mac, name, nil, nil)
else
_add(what, mac, nil, nil, name)
end
end
end
end
cur:foreach("dhcp", "dnsmasq",
function(s)
if s.leasefile and fs.access(s.leasefile) then
for e in io.lines(s.leasefile) do
mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)")
mac = luci.ip.checkmac(mac)
if mac and ip then
_add(what, mac, ip, nil, name ~= "*" and name)
end
end
end
end
)
cur:foreach("dhcp", "odhcpd",
function(s)
if type(s.leasefile) == "string" and fs.access(s.leasefile) then
for e in io.lines(s.leasefile) do
duid, iaid, name, _, ip = e:match("^# %S+ (%S+) (%S+) (%S+) (-?%d+) %S+ %S+ ([0-9a-f:.]+)/[0-9]+")
mac = net.duid_to_mac(duid)
if mac then
if ip and iaid == "ipv4" then
_add(what, mac, ip, nil, name ~= "*" and name)
elseif ip then
_add(what, mac, nil, ip, name ~= "*" and name)
end
end
end
end
end
)
cur:foreach("dhcp", "host",
function(s)
for mac in luci.util.imatch(s.mac) do
mac = luci.ip.checkmac(mac)
if mac then
_add(what, mac, s.ip, nil, s.name)
end
end
end)
for _, e in ipairs(nixio.getifaddrs()) do
if e.name ~= "lo" then
ifn[e.name] = ifn[e.name] or { }
if e.family == "packet" and e.addr and #e.addr == 17 then
ifn[e.name][1] = e.addr:upper()
elseif e.family == "inet" then
ifn[e.name][2] = e.addr
elseif e.family == "inet6" then
ifn[e.name][3] = e.addr
end
end
end
for _, e in pairs(ifn) do
if e[what] and (e[2] or e[3]) then
_add(what, e[1], e[2], e[3], e[4])
end
end
for _, e in pairs(hosts) do
lookup[#lookup+1] = (what > 1) and e[what] or (e[2] or e[3])
end
if #lookup > 0 then
lookup = luci.util.ubus("network.rrdns", "lookup", {
addrs = lookup,
timeout = 250,
limit = 1000
}) or { }
end
for _, e in luci.util.kspairs(hosts) do
callback(e[1], e[2], e[3], lookup[e[2]] or lookup[e[3]] or e[4])
end
end
-- Each entry contains the values in the following order:
-- [ "mac", "name" ]
function net.mac_hints(callback)
if callback then
_nethints(1, function(mac, v4, v6, name)
name = name or v4
if name and name ~= mac then
callback(mac, name or v4)
end
end)
else
local rv = { }
_nethints(1, function(mac, v4, v6, name)
name = name or v4
if name and name ~= mac then
rv[#rv+1] = { mac, name or v4 }
end
end)
return rv
end
end
-- Each entry contains the values in the following order:
-- [ "ip", "name" ]
function net.ipv4_hints(callback)
if callback then
_nethints(2, function(mac, v4, v6, name)
name = name or mac
if name and name ~= v4 then
callback(v4, name)
end
end)
else
local rv = { }
_nethints(2, function(mac, v4, v6, name)
name = name or mac
if name and name ~= v4 then
rv[#rv+1] = { v4, name }
end
end)
return rv
end
end
-- Each entry contains the values in the following order:
-- [ "ip", "name" ]
function net.ipv6_hints(callback)
if callback then
_nethints(3, function(mac, v4, v6, name)
name = name or mac
if name and name ~= v6 then
callback(v6, name)
end
end)
else
local rv = { }
_nethints(3, function(mac, v4, v6, name)
name = name or mac
if name and name ~= v6 then
rv[#rv+1] = { v6, name }
end
end)
return rv
end
end
function net.host_hints(callback)
if callback then
_nethints(1, function(mac, v4, v6, name)
if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
callback(mac, v4, v6, name)
end
end)
else
local rv = { }
_nethints(1, function(mac, v4, v6, name)
if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
local e = { }
if v4 then e.ipv4 = v4 end
if v6 then e.ipv6 = v6 end
if name then e.name = name end
rv[mac] = e
end
end)
return rv
end
end
function net.conntrack(callback)
local ok, nfct = pcall(io.lines, "/proc/net/nf_conntrack")
if not ok or not nfct then
return nil
end
local line, connt = nil, (not callback) and { }
for line in nfct do
local fam, l3, l4, rest =
line:match("^(ipv[46]) +(%d+) +%S+ +(%d+) +(.+)$")
local timeout, tuples = rest:match("^(%d+) +(.+)$")
if not tuples then
tuples = rest
end
if fam and l3 and l4 and not tuples:match("^TIME_WAIT ") then
l4 = nixio.getprotobynumber(l4)
local entry = {
bytes = 0,
packets = 0,
layer3 = fam,
layer4 = l4 and l4.name or "unknown",
timeout = tonumber(timeout, 10)
}
local key, val
for key, val in tuples:gmatch("(%w+)=(%S+)") do
if key == "bytes" or key == "packets" then
entry[key] = entry[key] + tonumber(val, 10)
elseif key == "src" or key == "dst" then
if entry[key] == nil then
entry[key] = luci.ip.new(val):string()
end
elseif key == "sport" or key == "dport" then
if entry[key] == nil then
entry[key] = val
end
elseif val then
entry[key] = val
end
end
if callback then
callback(entry)
else
connt[#connt+1] = entry
end
end
end
return callback and true or connt
end
function net.devices()
local devs = {}
local seen = {}
for k, v in ipairs(nixio.getifaddrs()) do
if v.name and not seen[v.name] then
seen[v.name] = true
devs[#devs+1] = v.name
end
end
return devs
end
function net.duid_to_mac(duid)
local b1, b2, b3, b4, b5, b6
if type(duid) == "string" then
-- DUID-LLT / Ethernet
if #duid == 28 then
b1, b2, b3, b4, b5, b6 = duid:match("^00010001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)%x%x%x%x%x%x%x%x$")
-- DUID-LL / Ethernet
elseif #duid == 20 then
b1, b2, b3, b4, b5, b6 = duid:match("^00030001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
-- DUID-LL / Ethernet (Without Header)
elseif #duid == 12 then
b1, b2, b3, b4, b5, b6 = duid:match("^(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
end
end
return b1 and luci.ip.checkmac(table.concat({ b1, b2, b3, b4, b5, b6 }, ":"))
end
process = {}
function process.info(key)
local s = {uid = nixio.getuid(), gid = nixio.getgid()}
return not key and s or s[key]
end
function process.list()
local data = {}
local k
local ps = luci.util.execi("/bin/busybox top -bn1")
if not ps then
return
end
for line in ps do
local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
"^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][<NW ][<N ]) +(%d+m?) +(%d+%%) +(%d+%%) +(.+)"
)
local idx = tonumber(pid)
if idx and not cmd:match("top %-bn1") then
data[idx] = {
['PID'] = pid,
['PPID'] = ppid,
['USER'] = user,
['STAT'] = stat,
['VSZ'] = vsz,
['%MEM'] = mem,
['%CPU'] = cpu,
['COMMAND'] = cmd
}
end
end
return data
end
function process.setgroup(gid)
return nixio.setgid(gid)
end
function process.setuser(uid)
return nixio.setuid(uid)
end
process.signal = nixio.kill
local function xclose(fd)
if fd and fd:fileno() > 2 then
fd:close()
end
end
function process.exec(command, stdout, stderr, nowait)
local out_r, out_w, err_r, err_w
if stdout then out_r, out_w = nixio.pipe() end
if stderr then err_r, err_w = nixio.pipe() end
local pid = nixio.fork()
if pid == 0 then
nixio.chdir("/")
local null = nixio.open("/dev/null", "w+")
if null then
nixio.dup(out_w or null, nixio.stdout)
nixio.dup(err_w or null, nixio.stderr)
nixio.dup(null, nixio.stdin)
xclose(out_w)
xclose(out_r)
xclose(err_w)
xclose(err_r)
xclose(null)
end
nixio.exec(unpack(command))
os.exit(-1)
end
local _, pfds, rv = nil, {}, { code = -1, pid = pid }
xclose(out_w)
xclose(err_w)
if out_r then
pfds[#pfds+1] = {
fd = out_r,
cb = type(stdout) == "function" and stdout,
name = "stdout",
events = nixio.poll_flags("in", "err", "hup")
}
end
if err_r then
pfds[#pfds+1] = {
fd = err_r,
cb = type(stderr) == "function" and stderr,
name = "stderr",
events = nixio.poll_flags("in", "err", "hup")
}
end
while #pfds > 0 do
local nfds, err = nixio.poll(pfds, -1)
if not nfds and err ~= nixio.const.EINTR then
break
end
local i
for i = #pfds, 1, -1 do
local rfd = pfds[i]
if rfd.revents > 0 then
local chunk, err = rfd.fd:read(4096)
if chunk and #chunk > 0 then
if rfd.cb then
rfd.cb(chunk)
else
rfd.buf = rfd.buf or {}
rfd.buf[#rfd.buf + 1] = chunk
end
else
table.remove(pfds, i)
if rfd.buf then
rv[rfd.name] = table.concat(rfd.buf, "")
end
rfd.fd:close()
end
end
end
end
if not nowait then
_, _, rv.code = nixio.waitpid(pid)
end
return rv
end
user = {}
-- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
user.getuser = nixio.getpw
function user.getpasswd(username)
local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
local pwh = pwe and (pwe.pwdp or pwe.passwd)
if not pwh or #pwh < 1 then
return nil, pwe
else
return pwh, pwe
end
end
function user.checkpasswd(username, pass)
local pwh, pwe = user.getpasswd(username)
if pwe then
return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
end
return false
end
function user.setpasswd(username, password)
return os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{
luci.util.shellquote(password),
luci.util.shellquote(password),
luci.util.shellquote(username)
})
end
wifi = {}
function wifi.getiwinfo(ifname)
local ntm = require "luci.model.network"
ntm.init()
local wnet = ntm:get_wifinet(ifname)
if wnet and wnet.iwinfo then
return wnet.iwinfo
end
local wdev = ntm:get_wifidev(ifname)
if wdev and wdev.iwinfo then
return wdev.iwinfo
end
return { ifname = ifname }
end
init = {}
init.dir = "/etc/init.d/"
function init.names()
local names = { }
for name in fs.glob(init.dir.."*") do
names[#names+1] = fs.basename(name)
end
return names
end
function init.index(name)
name = fs.basename(name)
if fs.access(init.dir..name) then
return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
%{ init.dir, name })
end
end
local function init_action(action, name)
name = fs.basename(name)
if fs.access(init.dir..name) then
return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
end
end
function init.enabled(name)
return (init_action("enabled", name) == 0)
end
function init.enable(name)
return (init_action("enable", name) == 0)
end
function init.disable(name)
return (init_action("disable", name) == 0)
end
function init.start(name)
return (init_action("start", name) == 0)
end
function init.stop(name)
return (init_action("stop", name) == 0)
end
function init.restart(name)
return (init_action("restart", name) == 0)
end
function init.reload(name)
return (init_action("reload", name) == 0)
end

@ -0,0 +1,392 @@
---[[
LuCI Linux and POSIX system utilities.
]]
module "luci.sys"
---[[
Execute a given shell command and return the error code
@class function
@name call
@param ... Command to call
@return Error code of the command
]]
---[[
Execute a given shell command and capture its standard output
@class function
@name exec
@param command Command to call
@return String containing the return the output of the command
]]
---[[
Retrieve information about currently mounted file systems.
@class function
@name mounts
@return Table containing mount information
]]
---[[
Retrieve environment variables. If no variable is given then a table
containing the whole environment is returned otherwise this function returns
the corresponding string value for the given name or nil if no such variable
exists.
@class function
@name getenv
@param var Name of the environment variable to retrieve (optional)
@return String containing the value of the specified variable
@return Table containing all variables if no variable name is given
]]
---[[
Get or set the current hostname.
@class function
@name hostname
@param String containing a new hostname to set (optional)
@return String containing the system hostname
]]
---[[
Returns the contents of a documented referred by an URL.
@class function
@name httpget
@param url The URL to retrieve
@param stream Return a stream instead of a buffer
@param target Directly write to target file name
@return String containing the contents of given the URL
]]
---[[
Initiate a system reboot.
@class function
@name reboot
@return Return value of os.execute()
]]
---[[
Retrieves the output of the "logread" command.
@class function
@name syslog
@return String containing the current log buffer
]]
---[[
Retrieves the output of the "dmesg" command.
@class function
@name dmesg
@return String containing the current log buffer
]]
---[[
Generates a random id with specified length.
@class function
@name uniqueid
@param bytes Number of bytes for the unique id
@return String containing hex encoded id
]]
---[[
Returns the current system uptime stats.
@class function
@name uptime
@return String containing total uptime in seconds
]]
---[[
LuCI system utilities / network related functions.
@class module
@name luci.sys.net
]]
---[[
Returns a two-dimensional table of mac address hints.
@class function
@name net.mac_hints
@return Table of table containing known hosts from various sources.
Each entry contains the values in the following order:
[ "mac", "name" ]
]]
---[[
Returns a two-dimensional table of IPv4 address hints.
@class function
@name net.ipv4_hints
@return Table of table containing known hosts from various sources.
Each entry contains the values in the following order:
[ "ip", "name" ]
]]
---[[
Returns a two-dimensional table of IPv6 address hints.
@class function
@name net.ipv6_hints
@return Table of table containing known hosts from various sources.
Each entry contains the values in the following order:
[ "ip", "name" ]
]]
---[[
Returns a two-dimensional table of host hints.
@class function
@name net.host_hints
@return Table of table containing known hosts from various sources,
indexed by mac address. Each subtable contains at least one
of the fields "name", "ipv4" or "ipv6".
]]
---[[
Returns conntrack information
@class function
@name net.conntrack
@return Table with the currently tracked IP connections
]]
---[[
Determine the names of available network interfaces.
@class function
@name net.devices
@return Table containing all current interface names
]]
---[[
LuCI system utilities / process related functions.
@class module
@name luci.sys.process
]]
---[[
Get the current process id.
@class function
@name process.info
@return Number containing the current pid
]]
---[[
Retrieve information about currently running processes.
@class function
@name process.list
@return Table containing process information
]]
---[[
Set the gid of a process identified by given pid.
@class function
@name process.setgroup
@param gid Number containing the Unix group id
@return Boolean indicating successful operation
@return String containing the error message if failed
@return Number containing the error code if failed
]]
---[[
Set the uid of a process identified by given pid.
@class function
@name process.setuser
@param uid Number containing the Unix user id
@return Boolean indicating successful operation
@return String containing the error message if failed
@return Number containing the error code if failed
]]
---[[
Send a signal to a process identified by given pid.
@class function
@name process.signal
@param pid Number containing the process id
@param sig Signal to send (default: 15 [SIGTERM])
@return Boolean indicating successful operation
@return Number containing the error code if failed
]]
---[[
Execute a process, optionally capturing stdio.
Executes the process specified by the given argv vector, e.g.
`{ "/bin/sh", "-c", "echo 1" }` and waits for it to terminate unless a true
value has been passed for the "nowait" parameter.
When a function value is passed for the stdout or stderr arguments, the passed
function is repeatedly called for each chunk read from the corresponding stdio
stream. The read data is passed as string containing at most 4096 bytes at a
time.
When a true, non-function value is passed for the stdout or stderr arguments,
the data of the corresponding stdio stream is read into an internal string
buffer and returned as "stdout" or "stderr" field respectively in the result
table.
When a true value is passed to the nowait parameter, the function does not
await process termination but returns as soon as all captured stdio streams
have been closed or - if no streams are captured - immediately after launching
the process.
@class function
@name process.exec
@param commend Table containing the argv vector to execute
@param stdout Callback function or boolean to indicate capturing (optional)
@param stderr Callback function or boolean to indicate capturing (optional)
@param nowait Don't wait for process termination when true (optional)
@return Table containing at least the fields "code" which holds the exit
status of the invoked process or "-1" on error and "pid", which
contains the process id assigned to the spawned process. When
stdout and/or stderr capturing has been requested, it additionally
contains "stdout" and "stderr" fields respectively, holding the
captured stdio data as string.
]]
---[[
LuCI system utilities / user related functions.
@class module
@name luci.sys.user
]]
---[[
Retrieve user information for given uid.
@class function
@name getuser
@param uid Number containing the Unix user id
@return Table containing the following fields:
-- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
]]
---[[
Retrieve the current user password hash.
@class function
@name user.getpasswd
@param username String containing the username to retrieve the password for
@return String containing the hash or nil if no password is set.
@return Password database entry
]]
---[[
Test whether given string matches the password of a given system user.
@class function
@name user.checkpasswd
@param username String containing the Unix user name
@param pass String containing the password to compare
@return Boolean indicating whether the passwords are equal
]]
---[[
Change the password of given user.
@class function
@name user.setpasswd
@param username String containing the Unix user name
@param password String containing the password to compare
@return Number containing 0 on success and >= 1 on error
]]
---[[
LuCI system utilities / wifi related functions.
@class module
@name luci.sys.wifi
]]
---[[
Get wireless information for given interface.
@class function
@name wifi.getiwinfo
@param ifname String containing the interface name
@return A wrapped iwinfo object instance
]]
---[[
LuCI system utilities / init related functions.
@class module
@name luci.sys.init
]]
---[[
Get the names of all installed init scripts
@class function
@name init.names
@return Table containing the names of all inistalled init scripts
]]
---[[
Get the index of he given init script
@class function
@name init.index
@param name Name of the init script
@return Numeric index value
]]
---[[
Test whether the given init script is enabled
@class function
@name init.enabled
@param name Name of the init script
@return Boolean indicating whether init is enabled
]]
---[[
Enable the given init script
@class function
@name init.enable
@param name Name of the init script
@return Boolean indicating success
]]
---[[
Disable the given init script
@class function
@name init.disable
@param name Name of the init script
@return Boolean indicating success
]]
---[[
Start the given init script
@class function
@name init.start
@param name Name of the init script
@return Boolean indicating success
]]
---[[
Stop the given init script
@class function
@name init.stop
@param name Name of the init script
@return Boolean indicating success
]]

@ -0,0 +1,19 @@
-- Licensed to the public under the Apache License 2.0.
local setmetatable, require, rawget, rawset = setmetatable, require, rawget, rawset
module "luci.sys.zoneinfo"
setmetatable(_M, {
__index = function(t, k)
if k == "TZ" and not rawget(t, k) then
local m = require "luci.sys.zoneinfo.tzdata"
rawset(t, k, rawget(m, k))
elseif k == "OFFSET" and not rawget(t, k) then
local m = require "luci.sys.zoneinfo.tzoffset"
rawset(t, k, rawget(m, k))
end
return rawget(t, k)
end
})

@ -0,0 +1,451 @@
-- Licensed to the public under the Apache License 2.0.
module "luci.sys.zoneinfo.tzdata"
TZ = {
{ 'Africa/Abidjan', 'GMT0' },
{ 'Africa/Accra', 'GMT0' },
{ 'Africa/Addis Ababa', 'EAT-3' },
{ 'Africa/Algiers', 'CET-1' },
{ 'Africa/Asmara', 'EAT-3' },
{ 'Africa/Bamako', 'GMT0' },
{ 'Africa/Bangui', 'WAT-1' },
{ 'Africa/Banjul', 'GMT0' },
{ 'Africa/Bissau', 'GMT0' },
{ 'Africa/Blantyre', 'CAT-2' },
{ 'Africa/Brazzaville', 'WAT-1' },
{ 'Africa/Bujumbura', 'CAT-2' },
{ 'Africa/Cairo', 'EET-2EEST,M4.5.5/0,M10.5.4/24' },
{ 'Africa/Casablanca', '<+01>-1' },
{ 'Africa/Ceuta', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Africa/Conakry', 'GMT0' },
{ 'Africa/Dakar', 'GMT0' },
{ 'Africa/Dar es Salaam', 'EAT-3' },
{ 'Africa/Djibouti', 'EAT-3' },
{ 'Africa/Douala', 'WAT-1' },
{ 'Africa/El Aaiun', '<+01>-1' },
{ 'Africa/Freetown', 'GMT0' },
{ 'Africa/Gaborone', 'CAT-2' },
{ 'Africa/Harare', 'CAT-2' },
{ 'Africa/Johannesburg', 'SAST-2' },
{ 'Africa/Juba', 'CAT-2' },
{ 'Africa/Kampala', 'EAT-3' },
{ 'Africa/Khartoum', 'CAT-2' },
{ 'Africa/Kigali', 'CAT-2' },
{ 'Africa/Kinshasa', 'WAT-1' },
{ 'Africa/Lagos', 'WAT-1' },
{ 'Africa/Libreville', 'WAT-1' },
{ 'Africa/Lome', 'GMT0' },
{ 'Africa/Luanda', 'WAT-1' },
{ 'Africa/Lubumbashi', 'CAT-2' },
{ 'Africa/Lusaka', 'CAT-2' },
{ 'Africa/Malabo', 'WAT-1' },
{ 'Africa/Maputo', 'CAT-2' },
{ 'Africa/Maseru', 'SAST-2' },
{ 'Africa/Mbabane', 'SAST-2' },
{ 'Africa/Mogadishu', 'EAT-3' },
{ 'Africa/Monrovia', 'GMT0' },
{ 'Africa/Nairobi', 'EAT-3' },
{ 'Africa/Ndjamena', 'WAT-1' },
{ 'Africa/Niamey', 'WAT-1' },
{ 'Africa/Nouakchott', 'GMT0' },
{ 'Africa/Ouagadougou', 'GMT0' },
{ 'Africa/Porto-Novo', 'WAT-1' },
{ 'Africa/Sao Tome', 'GMT0' },
{ 'Africa/Tripoli', 'EET-2' },
{ 'Africa/Tunis', 'CET-1' },
{ 'Africa/Windhoek', 'CAT-2' },
{ 'America/Adak', 'HST10HDT,M3.2.0,M11.1.0' },
{ 'America/Anchorage', 'AKST9AKDT,M3.2.0,M11.1.0' },
{ 'America/Anguilla', 'AST4' },
{ 'America/Antigua', 'AST4' },
{ 'America/Araguaina', '<-03>3' },
{ 'America/Argentina/Buenos Aires', '<-03>3' },
{ 'America/Argentina/Catamarca', '<-03>3' },
{ 'America/Argentina/Cordoba', '<-03>3' },
{ 'America/Argentina/Jujuy', '<-03>3' },
{ 'America/Argentina/La Rioja', '<-03>3' },
{ 'America/Argentina/Mendoza', '<-03>3' },
{ 'America/Argentina/Rio Gallegos', '<-03>3' },
{ 'America/Argentina/Salta', '<-03>3' },
{ 'America/Argentina/San Juan', '<-03>3' },
{ 'America/Argentina/San Luis', '<-03>3' },
{ 'America/Argentina/Tucuman', '<-03>3' },
{ 'America/Argentina/Ushuaia', '<-03>3' },
{ 'America/Aruba', 'AST4' },
{ 'America/Asuncion', '<-04>4<-03>,M10.1.0/0,M3.4.0/0' },
{ 'America/Atikokan', 'EST5' },
{ 'America/Bahia', '<-03>3' },
{ 'America/Bahia Banderas', 'CST6' },
{ 'America/Barbados', 'AST4' },
{ 'America/Belem', '<-03>3' },
{ 'America/Belize', 'CST6' },
{ 'America/Blanc-Sablon', 'AST4' },
{ 'America/Boa Vista', '<-04>4' },
{ 'America/Bogota', '<-05>5' },
{ 'America/Boise', 'MST7MDT,M3.2.0,M11.1.0' },
{ 'America/Cambridge Bay', 'MST7MDT,M3.2.0,M11.1.0' },
{ 'America/Campo Grande', '<-04>4' },
{ 'America/Cancun', 'EST5' },
{ 'America/Caracas', '<-04>4' },
{ 'America/Cayenne', '<-03>3' },
{ 'America/Cayman', 'EST5' },
{ 'America/Chicago', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/Chihuahua', 'CST6' },
{ 'America/Ciudad Juarez', 'MST7MDT,M3.2.0,M11.1.0' },
{ 'America/Costa Rica', 'CST6' },
{ 'America/Creston', 'MST7' },
{ 'America/Cuiaba', '<-04>4' },
{ 'America/Curacao', 'AST4' },
{ 'America/Danmarkshavn', 'GMT0' },
{ 'America/Dawson', 'MST7' },
{ 'America/Dawson Creek', 'MST7' },
{ 'America/Denver', 'MST7MDT,M3.2.0,M11.1.0' },
{ 'America/Detroit', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Dominica', 'AST4' },
{ 'America/Edmonton', 'MST7MDT,M3.2.0,M11.1.0' },
{ 'America/Eirunepe', '<-05>5' },
{ 'America/El Salvador', 'CST6' },
{ 'America/Fort Nelson', 'MST7' },
{ 'America/Fortaleza', '<-03>3' },
{ 'America/Glace Bay', 'AST4ADT,M3.2.0,M11.1.0' },
{ 'America/Goose Bay', 'AST4ADT,M3.2.0,M11.1.0' },
{ 'America/Grand Turk', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Grenada', 'AST4' },
{ 'America/Guadeloupe', 'AST4' },
{ 'America/Guatemala', 'CST6' },
{ 'America/Guayaquil', '<-05>5' },
{ 'America/Guyana', '<-04>4' },
{ 'America/Halifax', 'AST4ADT,M3.2.0,M11.1.0' },
{ 'America/Havana', 'CST5CDT,M3.2.0/0,M11.1.0/1' },
{ 'America/Hermosillo', 'MST7' },
{ 'America/Indiana/Indianapolis', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Indiana/Knox', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/Indiana/Marengo', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Indiana/Petersburg', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Indiana/Tell City', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/Indiana/Vevay', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Indiana/Vincennes', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Indiana/Winamac', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Inuvik', 'MST7MDT,M3.2.0,M11.1.0' },
{ 'America/Iqaluit', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Jamaica', 'EST5' },
{ 'America/Juneau', 'AKST9AKDT,M3.2.0,M11.1.0' },
{ 'America/Kentucky/Louisville', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Kentucky/Monticello', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Kralendijk', 'AST4' },
{ 'America/La Paz', '<-04>4' },
{ 'America/Lima', '<-05>5' },
{ 'America/Los Angeles', 'PST8PDT,M3.2.0,M11.1.0' },
{ 'America/Lower Princes', 'AST4' },
{ 'America/Maceio', '<-03>3' },
{ 'America/Managua', 'CST6' },
{ 'America/Manaus', '<-04>4' },
{ 'America/Marigot', 'AST4' },
{ 'America/Martinique', 'AST4' },
{ 'America/Matamoros', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/Mazatlan', 'MST7' },
{ 'America/Menominee', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/Merida', 'CST6' },
{ 'America/Metlakatla', 'AKST9AKDT,M3.2.0,M11.1.0' },
{ 'America/Mexico City', 'CST6' },
{ 'America/Miquelon', '<-03>3<-02>,M3.2.0,M11.1.0' },
{ 'America/Moncton', 'AST4ADT,M3.2.0,M11.1.0' },
{ 'America/Monterrey', 'CST6' },
{ 'America/Montevideo', '<-03>3' },
{ 'America/Montserrat', 'AST4' },
{ 'America/Nassau', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/New York', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Nome', 'AKST9AKDT,M3.2.0,M11.1.0' },
{ 'America/Noronha', '<-02>2' },
{ 'America/North Dakota/Beulah', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/North Dakota/Center', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/North Dakota/New Salem', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/Nuuk', '<-02>2<-01>,M3.5.0/-1,M10.5.0/0' },
{ 'America/Ojinaga', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/Panama', 'EST5' },
{ 'America/Paramaribo', '<-03>3' },
{ 'America/Phoenix', 'MST7' },
{ 'America/Port of Spain', 'AST4' },
{ 'America/Port-au-Prince', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Porto Velho', '<-04>4' },
{ 'America/Puerto Rico', 'AST4' },
{ 'America/Punta Arenas', '<-03>3' },
{ 'America/Rankin Inlet', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/Recife', '<-03>3' },
{ 'America/Regina', 'CST6' },
{ 'America/Resolute', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/Rio Branco', '<-05>5' },
{ 'America/Santarem', '<-03>3' },
{ 'America/Santiago', '<-04>4<-03>,M9.1.6/24,M4.1.6/24' },
{ 'America/Santo Domingo', 'AST4' },
{ 'America/Sao Paulo', '<-03>3' },
{ 'America/Scoresbysund', '<-01>1<+00>,M3.5.0/0,M10.5.0/1' },
{ 'America/Sitka', 'AKST9AKDT,M3.2.0,M11.1.0' },
{ 'America/St Barthelemy', 'AST4' },
{ 'America/St Johns', 'NST3:30NDT,M3.2.0,M11.1.0' },
{ 'America/St Kitts', 'AST4' },
{ 'America/St Lucia', 'AST4' },
{ 'America/St Thomas', 'AST4' },
{ 'America/St Vincent', 'AST4' },
{ 'America/Swift Current', 'CST6' },
{ 'America/Tegucigalpa', 'CST6' },
{ 'America/Thule', 'AST4ADT,M3.2.0,M11.1.0' },
{ 'America/Tijuana', 'PST8PDT,M3.2.0,M11.1.0' },
{ 'America/Toronto', 'EST5EDT,M3.2.0,M11.1.0' },
{ 'America/Tortola', 'AST4' },
{ 'America/Vancouver', 'PST8PDT,M3.2.0,M11.1.0' },
{ 'America/Whitehorse', 'MST7' },
{ 'America/Winnipeg', 'CST6CDT,M3.2.0,M11.1.0' },
{ 'America/Yakutat', 'AKST9AKDT,M3.2.0,M11.1.0' },
{ 'Antarctica/Casey', '<+11>-11' },
{ 'Antarctica/Davis', '<+07>-7' },
{ 'Antarctica/DumontDUrville', '<+10>-10' },
{ 'Antarctica/Macquarie', 'AEST-10AEDT,M10.1.0,M4.1.0/3' },
{ 'Antarctica/Mawson', '<+05>-5' },
{ 'Antarctica/McMurdo', 'NZST-12NZDT,M9.5.0,M4.1.0/3' },
{ 'Antarctica/Palmer', '<-03>3' },
{ 'Antarctica/Rothera', '<-03>3' },
{ 'Antarctica/Syowa', '<+03>-3' },
{ 'Antarctica/Troll', '<+00>0<+02>-2,M3.5.0/1,M10.5.0/3' },
{ 'Antarctica/Vostok', '<+06>-6' },
{ 'Arctic/Longyearbyen', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Asia/Aden', '<+03>-3' },
{ 'Asia/Almaty', '<+06>-6' },
{ 'Asia/Amman', '<+03>-3' },
{ 'Asia/Anadyr', '<+12>-12' },
{ 'Asia/Aqtau', '<+05>-5' },
{ 'Asia/Aqtobe', '<+05>-5' },
{ 'Asia/Ashgabat', '<+05>-5' },
{ 'Asia/Atyrau', '<+05>-5' },
{ 'Asia/Baghdad', '<+03>-3' },
{ 'Asia/Bahrain', '<+03>-3' },
{ 'Asia/Baku', '<+04>-4' },
{ 'Asia/Bangkok', '<+07>-7' },
{ 'Asia/Barnaul', '<+07>-7' },
{ 'Asia/Beirut', 'EET-2EEST,M3.5.0/0,M10.5.0/0' },
{ 'Asia/Bishkek', '<+06>-6' },
{ 'Asia/Brunei', '<+08>-8' },
{ 'Asia/Chita', '<+09>-9' },
{ 'Asia/Choibalsan', '<+08>-8' },
{ 'Asia/Colombo', '<+0530>-5:30' },
{ 'Asia/Damascus', '<+03>-3' },
{ 'Asia/Dhaka', '<+06>-6' },
{ 'Asia/Dili', '<+09>-9' },
{ 'Asia/Dubai', '<+04>-4' },
{ 'Asia/Dushanbe', '<+05>-5' },
{ 'Asia/Famagusta', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Asia/Gaza', 'EET-2EEST,M3.4.4/50,M10.4.4/50' },
{ 'Asia/Hebron', 'EET-2EEST,M3.4.4/50,M10.4.4/50' },
{ 'Asia/Ho Chi Minh', '<+07>-7' },
{ 'Asia/Hong Kong', 'HKT-8' },
{ 'Asia/Hovd', '<+07>-7' },
{ 'Asia/Irkutsk', '<+08>-8' },
{ 'Asia/Jakarta', 'WIB-7' },
{ 'Asia/Jayapura', 'WIT-9' },
{ 'Asia/Jerusalem', 'IST-2IDT,M3.4.4/26,M10.5.0' },
{ 'Asia/Kabul', '<+0430>-4:30' },
{ 'Asia/Kamchatka', '<+12>-12' },
{ 'Asia/Karachi', 'PKT-5' },
{ 'Asia/Kathmandu', '<+0545>-5:45' },
{ 'Asia/Khandyga', '<+09>-9' },
{ 'Asia/Kolkata', 'IST-5:30' },
{ 'Asia/Krasnoyarsk', '<+07>-7' },
{ 'Asia/Kuala Lumpur', '<+08>-8' },
{ 'Asia/Kuching', '<+08>-8' },
{ 'Asia/Kuwait', '<+03>-3' },
{ 'Asia/Macau', 'CST-8' },
{ 'Asia/Magadan', '<+11>-11' },
{ 'Asia/Makassar', 'WITA-8' },
{ 'Asia/Manila', 'PST-8' },
{ 'Asia/Muscat', '<+04>-4' },
{ 'Asia/Nicosia', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Asia/Novokuznetsk', '<+07>-7' },
{ 'Asia/Novosibirsk', '<+07>-7' },
{ 'Asia/Omsk', '<+06>-6' },
{ 'Asia/Oral', '<+05>-5' },
{ 'Asia/Phnom Penh', '<+07>-7' },
{ 'Asia/Pontianak', 'WIB-7' },
{ 'Asia/Pyongyang', 'KST-9' },
{ 'Asia/Qatar', '<+03>-3' },
{ 'Asia/Qostanay', '<+06>-6' },
{ 'Asia/Qyzylorda', '<+05>-5' },
{ 'Asia/Riyadh', '<+03>-3' },
{ 'Asia/Sakhalin', '<+11>-11' },
{ 'Asia/Samarkand', '<+05>-5' },
{ 'Asia/Seoul', 'KST-9' },
{ 'Asia/Shanghai', 'CST-8' },
{ 'Asia/Singapore', '<+08>-8' },
{ 'Asia/Srednekolymsk', '<+11>-11' },
{ 'Asia/Taipei', 'CST-8' },
{ 'Asia/Tashkent', '<+05>-5' },
{ 'Asia/Tbilisi', '<+04>-4' },
{ 'Asia/Tehran', '<+0330>-3:30' },
{ 'Asia/Thimphu', '<+06>-6' },
{ 'Asia/Tokyo', 'JST-9' },
{ 'Asia/Tomsk', '<+07>-7' },
{ 'Asia/Ulaanbaatar', '<+08>-8' },
{ 'Asia/Urumqi', '<+06>-6' },
{ 'Asia/Ust-Nera', '<+10>-10' },
{ 'Asia/Vientiane', '<+07>-7' },
{ 'Asia/Vladivostok', '<+10>-10' },
{ 'Asia/Yakutsk', '<+09>-9' },
{ 'Asia/Yangon', '<+0630>-6:30' },
{ 'Asia/Yekaterinburg', '<+05>-5' },
{ 'Asia/Yerevan', '<+04>-4' },
{ 'Atlantic/Azores', '<-01>1<+00>,M3.5.0/0,M10.5.0/1' },
{ 'Atlantic/Bermuda', 'AST4ADT,M3.2.0,M11.1.0' },
{ 'Atlantic/Canary', 'WET0WEST,M3.5.0/1,M10.5.0' },
{ 'Atlantic/Cape Verde', '<-01>1' },
{ 'Atlantic/Faroe', 'WET0WEST,M3.5.0/1,M10.5.0' },
{ 'Atlantic/Madeira', 'WET0WEST,M3.5.0/1,M10.5.0' },
{ 'Atlantic/Reykjavik', 'GMT0' },
{ 'Atlantic/South Georgia', '<-02>2' },
{ 'Atlantic/St Helena', 'GMT0' },
{ 'Atlantic/Stanley', '<-03>3' },
{ 'Australia/Adelaide', 'ACST-9:30ACDT,M10.1.0,M4.1.0/3' },
{ 'Australia/Brisbane', 'AEST-10' },
{ 'Australia/Broken Hill', 'ACST-9:30ACDT,M10.1.0,M4.1.0/3' },
{ 'Australia/Darwin', 'ACST-9:30' },
{ 'Australia/Eucla', '<+0845>-8:45' },
{ 'Australia/Hobart', 'AEST-10AEDT,M10.1.0,M4.1.0/3' },
{ 'Australia/Lindeman', 'AEST-10' },
{ 'Australia/Lord Howe', '<+1030>-10:30<+11>-11,M10.1.0,M4.1.0' },
{ 'Australia/Melbourne', 'AEST-10AEDT,M10.1.0,M4.1.0/3' },
{ 'Australia/Perth', 'AWST-8' },
{ 'Australia/Sydney', 'AEST-10AEDT,M10.1.0,M4.1.0/3' },
{ 'Etc/GMT', 'GMT0' },
{ 'Etc/GMT+1', '<-01>1' },
{ 'Etc/GMT+10', '<-10>10' },
{ 'Etc/GMT+11', '<-11>11' },
{ 'Etc/GMT+12', '<-12>12' },
{ 'Etc/GMT+2', '<-02>2' },
{ 'Etc/GMT+3', '<-03>3' },
{ 'Etc/GMT+4', '<-04>4' },
{ 'Etc/GMT+5', '<-05>5' },
{ 'Etc/GMT+6', '<-06>6' },
{ 'Etc/GMT+7', '<-07>7' },
{ 'Etc/GMT+8', '<-08>8' },
{ 'Etc/GMT+9', '<-09>9' },
{ 'Etc/GMT-1', '<+01>-1' },
{ 'Etc/GMT-10', '<+10>-10' },
{ 'Etc/GMT-11', '<+11>-11' },
{ 'Etc/GMT-12', '<+12>-12' },
{ 'Etc/GMT-13', '<+13>-13' },
{ 'Etc/GMT-14', '<+14>-14' },
{ 'Etc/GMT-2', '<+02>-2' },
{ 'Etc/GMT-3', '<+03>-3' },
{ 'Etc/GMT-4', '<+04>-4' },
{ 'Etc/GMT-5', '<+05>-5' },
{ 'Etc/GMT-6', '<+06>-6' },
{ 'Etc/GMT-7', '<+07>-7' },
{ 'Etc/GMT-8', '<+08>-8' },
{ 'Etc/GMT-9', '<+09>-9' },
{ 'Europe/Amsterdam', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Andorra', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Astrakhan', '<+04>-4' },
{ 'Europe/Athens', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Europe/Belgrade', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Berlin', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Bratislava', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Brussels', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Bucharest', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Europe/Budapest', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Busingen', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Chisinau', 'EET-2EEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Copenhagen', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Dublin', 'IST-1GMT0,M10.5.0,M3.5.0/1' },
{ 'Europe/Gibraltar', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Guernsey', 'GMT0BST,M3.5.0/1,M10.5.0' },
{ 'Europe/Helsinki', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Europe/Isle of Man', 'GMT0BST,M3.5.0/1,M10.5.0' },
{ 'Europe/Istanbul', '<+03>-3' },
{ 'Europe/Jersey', 'GMT0BST,M3.5.0/1,M10.5.0' },
{ 'Europe/Kaliningrad', 'EET-2' },
{ 'Europe/Kirov', 'MSK-3' },
{ 'Europe/Kyiv', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Europe/Lisbon', 'WET0WEST,M3.5.0/1,M10.5.0' },
{ 'Europe/Ljubljana', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/London', 'GMT0BST,M3.5.0/1,M10.5.0' },
{ 'Europe/Luxembourg', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Madrid', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Malta', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Mariehamn', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Europe/Minsk', '<+03>-3' },
{ 'Europe/Monaco', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Moscow', 'MSK-3' },
{ 'Europe/Oslo', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Paris', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Podgorica', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Prague', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Riga', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Europe/Rome', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Samara', '<+04>-4' },
{ 'Europe/San Marino', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Sarajevo', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Saratov', '<+04>-4' },
{ 'Europe/Simferopol', 'MSK-3' },
{ 'Europe/Skopje', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Sofia', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Europe/Stockholm', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Tallinn', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Europe/Tirane', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Ulyanovsk', '<+04>-4' },
{ 'Europe/Vaduz', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Vatican', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Vienna', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Vilnius', 'EET-2EEST,M3.5.0/3,M10.5.0/4' },
{ 'Europe/Volgograd', 'MSK-3' },
{ 'Europe/Warsaw', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Zagreb', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Europe/Zurich', 'CET-1CEST,M3.5.0,M10.5.0/3' },
{ 'Indian/Antananarivo', 'EAT-3' },
{ 'Indian/Chagos', '<+06>-6' },
{ 'Indian/Christmas', '<+07>-7' },
{ 'Indian/Cocos', '<+0630>-6:30' },
{ 'Indian/Comoro', 'EAT-3' },
{ 'Indian/Kerguelen', '<+05>-5' },
{ 'Indian/Mahe', '<+04>-4' },
{ 'Indian/Maldives', '<+05>-5' },
{ 'Indian/Mauritius', '<+04>-4' },
{ 'Indian/Mayotte', 'EAT-3' },
{ 'Indian/Reunion', '<+04>-4' },
{ 'Pacific/Apia', '<+13>-13' },
{ 'Pacific/Auckland', 'NZST-12NZDT,M9.5.0,M4.1.0/3' },
{ 'Pacific/Bougainville', '<+11>-11' },
{ 'Pacific/Chatham', '<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45' },
{ 'Pacific/Chuuk', '<+10>-10' },
{ 'Pacific/Easter', '<-06>6<-05>,M9.1.6/22,M4.1.6/22' },
{ 'Pacific/Efate', '<+11>-11' },
{ 'Pacific/Fakaofo', '<+13>-13' },
{ 'Pacific/Fiji', '<+12>-12' },
{ 'Pacific/Funafuti', '<+12>-12' },
{ 'Pacific/Galapagos', '<-06>6' },
{ 'Pacific/Gambier', '<-09>9' },
{ 'Pacific/Guadalcanal', '<+11>-11' },
{ 'Pacific/Guam', 'ChST-10' },
{ 'Pacific/Honolulu', 'HST10' },
{ 'Pacific/Kanton', '<+13>-13' },
{ 'Pacific/Kiritimati', '<+14>-14' },
{ 'Pacific/Kosrae', '<+11>-11' },
{ 'Pacific/Kwajalein', '<+12>-12' },
{ 'Pacific/Majuro', '<+12>-12' },
{ 'Pacific/Marquesas', '<-0930>9:30' },
{ 'Pacific/Midway', 'SST11' },
{ 'Pacific/Nauru', '<+12>-12' },
{ 'Pacific/Niue', '<-11>11' },
{ 'Pacific/Norfolk', '<+11>-11<+12>,M10.1.0,M4.1.0/3' },
{ 'Pacific/Noumea', '<+11>-11' },
{ 'Pacific/Pago Pago', 'SST11' },
{ 'Pacific/Palau', '<+09>-9' },
{ 'Pacific/Pitcairn', '<-08>8' },
{ 'Pacific/Pohnpei', '<+11>-11' },
{ 'Pacific/Port Moresby', '<+10>-10' },
{ 'Pacific/Rarotonga', '<-10>10' },
{ 'Pacific/Saipan', 'ChST-10' },
{ 'Pacific/Tahiti', '<-10>10' },
{ 'Pacific/Tarawa', '<+12>-12' },
{ 'Pacific/Tongatapu', '<+13>-13' },
{ 'Pacific/Wake', '<+12>-12' },
{ 'Pacific/Wallis', '<+12>-12' },
}

@ -0,0 +1,46 @@
-- Licensed to the public under the Apache License 2.0.
module "luci.sys.zoneinfo.tzoffset"
OFFSET = {
gmt = 0, -- GMT
eat = 10800, -- EAT
cet = 3600, -- CET
wat = 3600, -- WAT
cat = 7200, -- CAT
eet = 7200, -- EET
eest = 10800, -- EEST
sast = 7200, -- SAST
hst = -36000, -- HST
hdt = -32400, -- HDT
akst = -32400, -- AKST
akdt = -28800, -- AKDT
ast = -14400, -- AST
est = -18000, -- EST
cst = -21600, -- CST
mst = -25200, -- MST
mdt = -21600, -- MDT
pst = -28800, -- PST
pdt = -25200, -- PDT
nst = -12600, -- NST
ndt = -9000, -- NDT
aest = 36000, -- AEST
aedt = 39600, -- AEDT
nzst = 43200, -- NZST
nzdt = 46800, -- NZDT
hkt = 28800, -- HKT
wib = 25200, -- WIB
wit = 32400, -- WIT
ist = 7200, -- IST
idt = 10800, -- IDT
pkt = 18000, -- PKT
wita = 28800, -- WITA
kst = 32400, -- KST
jst = 32400, -- JST
wet = 0, -- WET
acst = 34200, -- ACST
acdt = 37800, -- ACDT
awst = 28800, -- AWST
msk = 10800, -- MSK
sst = -39600, -- SST
}

@ -0,0 +1,100 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Licensed to the public under the Apache License 2.0.
local util = require "luci.util"
local config = require "luci.config"
local tparser = require "luci.template.parser"
local tostring, pairs, loadstring = tostring, pairs, loadstring
local setmetatable, loadfile = setmetatable, loadfile
local getfenv, setfenv, rawget = getfenv, setfenv, rawget
local assert, type, error = assert, type, error
--- LuCI template library.
module "luci.template"
config.template = config.template or {}
viewdir = config.template.viewdir or util.libpath() .. "/view"
-- Define the namespace for template modules
context = util.threadlocal()
--- Render a certain template.
-- @param name Template name
-- @param scope Scope to assign to template (optional)
function render(name, scope)
return Template(name):render(scope or getfenv(2))
end
--- Render a template from a string.
-- @param template Template string
-- @param scope Scope to assign to template (optional)
function render_string(template, scope)
return Template(nil, template):render(scope or getfenv(2))
end
-- Template class
Template = util.class()
-- Shared template cache to store templates in to avoid unnecessary reloading
Template.cache = setmetatable({}, {__mode = "v"})
-- Constructor - Reads and compiles the template on-demand
function Template.__init__(self, name, template)
if name then
self.template = self.cache[name]
self.name = name
else
self.name = "[string]"
end
-- Create a new namespace for this template
self.viewns = context.viewns
-- If we have a cached template, skip compiling and loading
if not self.template then
-- Compile template
local err
local sourcefile
if name then
sourcefile = viewdir .. "/" .. name .. ".htm"
self.template, _, err = tparser.parse(sourcefile)
else
sourcefile = "[string]"
self.template, _, err = tparser.parse_string(template)
end
-- If we have no valid template throw error, otherwise cache the template
if not self.template then
error("Failed to load template '" .. self.name .. "'.\n" ..
"Error while parsing template '" .. sourcefile .. "':\n" ..
(err or "Unknown syntax error"))
elseif name then
self.cache[name] = self.template
end
end
end
-- Renders a template
function Template.render(self, scope)
scope = scope or getfenv(2)
-- Put our predefined objects in the scope of the template
setfenv(self.template, setmetatable({}, {__index =
function(tbl, key)
return rawget(tbl, key) or self.viewns[key] or scope[key]
end}))
-- Now finally render the thing
local stat, err = util.copcall(self.template)
if not stat then
error("Failed to execute template '" .. self.name .. "'.\n" ..
"A runtime error occurred: " .. tostring(err or "(nil)"))
end
end

@ -0,0 +1,9 @@
-- Licensed to the public under the Apache License 2.0.
module "luci.version"
distname = "Host System"
distversion = "SDK"
luciname = "LuCI"
luciversion = "SVN"

@ -0,0 +1,24 @@
<%#
Copyright 2015 Jo-Philipp Wich <jow@openwrt.org>
Licensed to the public under the Apache License 2.0.
-%>
<%+header%>
<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. Click "Continue »" below to return to the previous page.
%></p>
<hr />
<p class="right">
<strong><a href="#" onclick="window.history.back();">Continue »</a></strong>
</p>
<%+footer%>

@ -0,0 +1,11 @@
<%#
Copyright 2010 Jo-Philipp Wich <jow@openwrt.org>
Copyright 2018 Daniel F. Dickinson <cshored@thecshore.com>
Licensed to the public under the Apache License 2.0.
-%>
<%+header%>
<p>Component not present.</p>
<%+footer%>

@ -0,0 +1,12 @@
<%#
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.
-%>
<%+header%>
<h2 name="content">404 <%:Not Found%></h2>
<p><%:Sorry, the object you requested was not found.%></p>
<p><%=message%></p>
<tt><%:Unable to dispatch%>: <%=url(unpack(luci.dispatcher.context.request))%></tt>
<%+footer%>

@ -0,0 +1,11 @@
<%#
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.
-%>
<%+header%>
<h2 name="content">500 <%:Internal Server Error%></h2>
<p><%:Sorry, the server encountered an unexpected error.%></p>
<pre class="error500"><%=message%></pre>
<%+footer%>

@ -0,0 +1,27 @@
<%#
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008-2019 Jo-Philipp Wich <jo@mein.io>
Licensed to the public under the Apache License 2.0.
-%>
<%
local is_rollback_pending, rollback_time_remaining, rollback_session, rollback_token = luci.model.uci:rollback_pending()
if is_rollback_pending or trigger_apply or trigger_revert then
%>
<script type="text/javascript">
document.addEventListener("luci-loaded", function() {
<% if trigger_apply then -%>
L.ui.changes.apply(true);
<%- elseif trigger_revert then -%>
L.ui.changes.revert();
<%- else -%>
L.ui.changes.confirm(true, Date.now() + <%=rollback_time_remaining%> * 1000, <%=luci.http.write_json(rollback_token)%>);
<%- end %>
});
</script>
<%
end
include("themes/" .. theme .. "/footer")
%>

@ -0,0 +1,38 @@
<%#
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008-2019 Jo-Philipp Wich <jo@mein.io>
Licensed to the public under the Apache License 2.0.
-%>
<%
if not luci.dispatcher.context.template_header_sent then
include("themes/" .. theme .. "/header")
luci.dispatcher.context.template_header_sent = true
end
local applyconf = luci.config and luci.config.apply
%>
<script type="text/javascript" src="<%=resource%>/promis.min.js"></script>
<script type="text/javascript" src="<%=resource%>/luci.js"></script>
<script type="text/javascript">
L = new LuCI(<%= luci.http.write_json({
token = token,
media = media,
resource = resource,
scriptname = luci.http.getenv("SCRIPT_NAME"),
pathinfo = luci.http.getenv("PATH_INFO"),
documentroot = luci.http.getenv("DOCUMENT_ROOT"),
requestpath = luci.dispatcher.context.requestpath,
dispatchpath = luci.dispatcher.context.path,
pollinterval = luci.config.main.pollinterval or 5,
ubuspath = luci.config.main.ubuspath or '/ubus/',
sessionid = luci.dispatcher.context.authsession,
nodespec = luci.dispatcher.context.dispatched,
apply_rollback = math.max(applyconf and applyconf.rollback or 90, 90),
apply_holdoff = math.max(applyconf and applyconf.holdoff or 4, 1),
apply_timeout = math.max(applyconf and applyconf.timeout or 5, 1),
apply_display = math.max(applyconf and applyconf.display or 1.5, 1),
rollback_token = rollback_token
}) %>);
</script>

@ -0,0 +1,7 @@
<%#
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.
-%>
<% include("themes/" .. theme .. "/indexer") %>

@ -0,0 +1,75 @@
<%#
Copyright 2008 Steven Barth <steven@midlink.org>
Copyright 2008-2012 Jo-Philipp Wich <jow@openwrt.org>
Licensed to the public under the Apache License 2.0.
-%>
<%+header%>
<form method="post" action="<%=pcdata(FULL_REQUEST_URI)%>">
<%- if fuser then %>
<div class="alert-message warning">
<p><%:Invalid username and/or password! Please try again.%></p>
</div>
<% end -%>
<div class="cbi-map">
<h2 name="content"><%:Authorization Required%></h2>
<div class="cbi-map-descr">
<%:Please enter your username and password.%>
</div>
<div class="cbi-section"><div class="cbi-section-node">
<div class="cbi-value">
<label class="cbi-value-title" for="luci_username"><%:Username%></label>
<div class="cbi-value-field">
<input class="cbi-input-text" type="text" name="luci_username" id="luci_username" autocomplete="username" value="<%=duser%>" />
</div>
</div>
<div class="cbi-value cbi-value-last">
<label class="cbi-value-title" for="luci_password"><%:Password%></label>
<div class="cbi-value-field">
<input class="cbi-input-text" type="password" name="luci_password" id="luci_password" autocomplete="current-password"/>
</div>
</div>
</div></div>
</div>
<div class="cbi-page-actions">
<input type="submit" value="<%:Login%>" class="btn cbi-button cbi-button-apply" />
<input type="reset" value="<%:Reset%>" class="btn cbi-button cbi-button-reset" />
</div>
</form>
<script type="text/javascript">//<![CDATA[
var input = document.getElementsByName('luci_password')[0];
if (input)
input.focus();
//]]></script>
<%
local uci = require "luci.model.uci".cursor()
local fs = require "nixio.fs"
local https_key = uci:get("uhttpd", "main", "key")
local https_port = uci:get("uhttpd", "main", "listen_https")
if type(https_port) == "table" then
https_port = https_port[1]
end
if https_port and fs.access(https_key) then
https_port = https_port:match("(%d+)$")
%>
<script type="text/javascript">//<![CDATA[
if (document.location.protocol != 'https:') {
var url = 'https://' + window.location.hostname + ':' + '<%=https_port%>' + window.location.pathname;
var img=new Image;
img.onload=function(){window.location = url};
img.src='https://' + window.location.hostname + ':' + '<%=https_port%>' + '<%=resource%>/icons/loading.gif?' + Math.random();
setTimeout(function(){
img.src=''
}, 5000);
}
//]]></script>
<% end %>
<%+footer%>

@ -0,0 +1,12 @@
<%+header%>
<div id="view">
<div class="spinning"><%:Loading view…%></div>
<script type="text/javascript">
L.require('ui').then(function(ui) {
ui.instantiateView('<%=view%>');
});
</script>
</div>
<%+footer%>

@ -0,0 +1,26 @@
-- Copyright 2008 Steven Barth <steven@midlink.org>
-- Licensed to the public under the Apache License 2.0.
local tparser = require "luci.template.parser"
local string = require "string"
local tostring = tostring
module "luci.xml"
--
-- String and data manipulation routines
--
function pcdata(value)
return value and tparser.pcdata(tostring(value))
end
function striptags(value)
return value and tparser.striptags(tostring(value))
end
-- also register functions above in the central string class for convenience
string.pcdata = pcdata
string.striptags = striptags

@ -0,0 +1,23 @@
---[[
LuCI utility functions.
]]
module "luci.xml"
---[[
Create valid XML PCDATA from given string.
@class function
@name pcdata
@param value String value containing the data to escape
@return String value containing the escaped data
]]
---[[
Strip HTML tags from given string.
@class function
@name striptags
@param value String containing the HTML text
@return String with HTML tags stripped of
]]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

Loading…
Cancel
Save