diff options
Diffstat (limited to 'modules/base')
145 files changed, 17947 insertions, 0 deletions
diff --git a/modules/base/Makefile b/modules/base/Makefile new file mode 100644 index 0000000000..ad16ec7376 --- /dev/null +++ b/modules/base/Makefile @@ -0,0 +1,45 @@ +ifneq (,$(wildcard ../../build/config.mk)) +include ../../build/config.mk +include ../../build/module.mk +include ../../build/gccconfig.mk +else +include standalone.mk +endif + +TPL_LDFLAGS = +TPL_CFLAGS = +TPL_SO = parser.so +TPL_PO2LMO = po2lmo +TPL_PO2LMO_OBJ = src/po2lmo.o +TPL_LMO_OBJ = src/template_lmo.o +TPL_COMMON_OBJ = src/template_parser.o src/template_utils.o +TPL_LUALIB_OBJ = src/template_lualib.o + +%.o: %.c + $(COMPILE) $(TPL_CFLAGS) $(LUA_CFLAGS) $(FPIC) -c -o $@ $< + +compile: build-clean $(TPL_COMMON_OBJ) $(TPL_LUALIB_OBJ) $(TPL_LMO_OBJ) $(TPL_PO2LMO_OBJ) + $(LINK) $(SHLIB_FLAGS) $(TPL_LDFLAGS) -o src/$(TPL_SO) \ + $(TPL_COMMON_OBJ) $(TPL_LMO_OBJ) $(TPL_LUALIB_OBJ) + $(LINK) -o src/$(TPL_PO2LMO) \ + $(TPL_LMO_OBJ) $(TPL_PO2LMO_OBJ) + mkdir -p dist$(LUCI_LIBRARYDIR)/template + cp src/$(TPL_SO) dist$(LUCI_LIBRARYDIR)/template/$(TPL_SO) + +install: build + cp -pR dist$(LUA_LIBRARYDIR)/* $(LUA_LIBRARYDIR) + +clean: build-clean + +build-clean: + rm -f src/*.o src/$(TPL_SO) + +host-compile: build-clean host-clean $(TPL_LMO_OBJ) $(TPL_PO2LMO_OBJ) + $(LINK) -o src/$(TPL_PO2LMO) $(TPL_LMO_OBJ) $(TPL_PO2LMO_OBJ) + +host-install: host-compile + cp src/$(TPL_PO2LMO) ../../build/$(TPL_PO2LMO) + +host-clean: + rm -f src/$(TPL_PO2LMO) + rm -f ../../build/$(TPL_PO2LMO) diff --git a/modules/base/htdocs/cgi-bin/luci b/modules/base/htdocs/cgi-bin/luci new file mode 100755 index 0000000000..529d1d0bc5 --- /dev/null +++ b/modules/base/htdocs/cgi-bin/luci @@ -0,0 +1,5 @@ +#!/usr/bin/lua +require "luci.cacheloader" +require "luci.sgi.cgi" +luci.dispatcher.indexcache = "/tmp/luci-indexcache" +luci.sgi.cgi.run()
\ No newline at end of file diff --git a/modules/base/htdocs/luci-static/resources/cbi.js b/modules/base/htdocs/luci-static/resources/cbi.js new file mode 100644 index 0000000000..02814a5428 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi.js @@ -0,0 +1,1339 @@ +/* + LuCI - Lua Configuration Interface + + Copyright 2008 Steven Barth <steven@midlink.org> + Copyright 2008-2012 Jo-Philipp Wich <xm@subsignal.org> + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +var cbi_d = []; +var cbi_t = []; +var cbi_c = []; + +var cbi_validators = { + + 'integer': function() + { + return (this.match(/^-?[0-9]+$/) != null); + }, + + 'uinteger': function() + { + return (cbi_validators.integer.apply(this) && (this >= 0)); + }, + + 'float': function() + { + return !isNaN(parseFloat(this)); + }, + + 'ufloat': function() + { + return (cbi_validators['float'].apply(this) && (this >= 0)); + }, + + 'ipaddr': function() + { + return cbi_validators.ip4addr.apply(this) || + cbi_validators.ip6addr.apply(this); + }, + + 'ip4addr': function() + { + if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/)) + { + return (RegExp.$1 >= 0) && (RegExp.$1 <= 255) && + (RegExp.$2 >= 0) && (RegExp.$2 <= 255) && + (RegExp.$3 >= 0) && (RegExp.$3 <= 255) && + (RegExp.$4 >= 0) && (RegExp.$4 <= 255) && + ((RegExp.$6.indexOf('.') < 0) + ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32)) + : (cbi_validators.ip4addr.apply(RegExp.$6))) + ; + } + + return false; + }, + + 'ip6addr': function() + { + if( this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/) ) + { + if( !RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)) ) + { + var addr = RegExp.$1; + + if( addr == '::' ) + { + return true; + } + + if( addr.indexOf('.') > 0 ) + { + var off = addr.lastIndexOf(':'); + + if( !(off && cbi_validators.ip4addr.apply(addr.substr(off+1))) ) + return false; + + addr = addr.substr(0, off) + ':0:0'; + } + + if( addr.indexOf('::') >= 0 ) + { + var colons = 0; + var fill = '0'; + + for( var i = 1; i < (addr.length-1); i++ ) + if( addr.charAt(i) == ':' ) + colons++; + + if( colons > 7 ) + return false; + + for( var i = 0; i < (7 - colons); i++ ) + fill += ':0'; + + if (addr.match(/^(.*?)::(.*?)$/)) + addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill + + (RegExp.$2 ? ':' + RegExp.$2 : ''); + } + + return (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null); + } + } + + return false; + }, + + 'port': function() + { + return cbi_validators.integer.apply(this) && + (this >= 0) && (this <= 65535); + }, + + 'portrange': function() + { + if (this.match(/^(\d+)-(\d+)$/)) + { + var p1 = RegExp.$1; + var p2 = RegExp.$2; + + return cbi_validators.port.apply(p1) && + cbi_validators.port.apply(p2) && + (parseInt(p1) <= parseInt(p2)) + ; + } + else + { + return cbi_validators.port.apply(this); + } + }, + + 'macaddr': function() + { + return (this.match(/^([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}$/) != null); + }, + + 'host': function() + { + return cbi_validators.hostname.apply(this) || + cbi_validators.ipaddr.apply(this); + }, + + 'hostname': function() + { + if (this.length <= 253) + return (this.match(/^[a-zA-Z0-9]+$/) != null || + (this.match(/^[a-zA-Z0-9_][a-zA-Z0-9_\-.]*[a-zA-Z0-9]$/) && + this.match(/[^0-9.]/))); + + return false; + }, + + 'network': function() + { + return cbi_validators.uciname.apply(this) || + cbi_validators.host.apply(this); + }, + + 'wpakey': function() + { + var v = this; + + if( v.length == 64 ) + return (v.match(/^[a-fA-F0-9]{64}$/) != null); + else + return (v.length >= 8) && (v.length <= 63); + }, + + 'wepkey': function() + { + var v = this; + + if ( v.substr(0,2) == 's:' ) + v = v.substr(2); + + if( (v.length == 10) || (v.length == 26) ) + return (v.match(/^[a-fA-F0-9]{10,26}$/) != null); + else + return (v.length == 5) || (v.length == 13); + }, + + 'uciname': function() + { + return (this.match(/^[a-zA-Z0-9_]+$/) != null); + }, + + 'range': function(min, max) + { + var val = parseFloat(this); + if (!isNaN(min) && !isNaN(max) && !isNaN(val)) + return ((val >= min) && (val <= max)); + + return false; + }, + + 'min': function(min) + { + var val = parseFloat(this); + if (!isNaN(min) && !isNaN(val)) + return (val >= min); + + return false; + }, + + 'max': function(max) + { + var val = parseFloat(this); + if (!isNaN(max) && !isNaN(val)) + return (val <= max); + + return false; + }, + + 'rangelength': function(min, max) + { + var val = '' + this; + if (!isNaN(min) && !isNaN(max)) + return ((val.length >= min) && (val.length <= max)); + + return false; + }, + + 'minlength': function(min) + { + var val = '' + this; + if (!isNaN(min)) + return (val.length >= min); + + return false; + }, + + 'maxlength': function(max) + { + var val = '' + this; + if (!isNaN(max)) + return (val.length <= max); + + return false; + }, + + 'or': function() + { + for (var i = 0; i < arguments.length; i += 2) + { + if (typeof arguments[i] != 'function') + { + if (arguments[i] == this) + return true; + i--; + } + else if (arguments[i].apply(this, arguments[i+1])) + { + return true; + } + } + return false; + }, + + 'and': function() + { + for (var i = 0; i < arguments.length; i += 2) + { + if (typeof arguments[i] != 'function') + { + if (arguments[i] != this) + return false; + i--; + } + else if (!arguments[i].apply(this, arguments[i+1])) + { + return false; + } + } + return true; + }, + + 'neg': function() + { + return cbi_validators.or.apply( + this.replace(/^[ \t]*![ \t]*/, ''), arguments); + }, + + 'list': function(subvalidator, subargs) + { + if (typeof subvalidator != 'function') + return false; + + var tokens = this.match(/[^ \t]+/g); + for (var i = 0; i < tokens.length; i++) + if (!subvalidator.apply(tokens[i], subargs)) + return false; + + return true; + }, + 'phonedigit': function() + { + return (this.match(/^[0-9\*#!\.]+$/) != null); + } +}; + + +function cbi_d_add(field, dep, next) { + var obj = document.getElementById(field); + if (obj) { + var entry + for (var i=0; i<cbi_d.length; i++) { + if (cbi_d[i].id == field) { + entry = cbi_d[i]; + break; + } + } + if (!entry) { + entry = { + "node": obj, + "id": field, + "parent": obj.parentNode.id, + "next": next, + "deps": [] + }; + cbi_d.unshift(entry); + } + entry.deps.push(dep) + } +} + +function cbi_d_checkvalue(target, ref) { + var t = document.getElementById(target); + var value; + + if (!t) { + var tl = document.getElementsByName(target); + + if( tl.length > 0 && (tl[0].type == 'radio' || tl[0].type == 'checkbox')) + for( var i = 0; i < tl.length; i++ ) + if( tl[i].checked ) { + value = tl[i].value; + break; + } + + value = value ? value : ""; + } else if (!t.value) { + value = ""; + } else { + value = t.value; + + if (t.type == "checkbox") { + value = t.checked ? value : ""; + } + } + + return (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) { + return !reverse; + } + } + return def; +} + +function cbi_d_update() { + var state = false; + for (var i=0; i<cbi_d.length; i++) { + var entry = cbi_d[i]; + var next = document.getElementById(entry.next) + 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; + if( entry.parent ) + cbi_c[entry.parent]--; + } else if ((!node || !node.parentNode) && cbi_d_check(entry.deps)) { + if (!next) { + parent.appendChild(entry.node); + } else { + next.parentNode.insertBefore(entry.node, next); + } + state = true; + if( entry.parent ) + cbi_c[entry.parent]++; + } + } + + if (entry && entry.parent) { + if (!cbi_t_update()) + cbi_tag_last(parent); + } + + if (state) { + cbi_d_update(); + } +} + +function cbi_bind(obj, type, callback, mode) { + if (!obj.addEventListener) { + obj.attachEvent('on' + type, + function(){ + var e = window.event; + + if (!e.target && e.srcElement) + e.target = e.srcElement; + + return !!callback(e); + } + ); + } else { + obj.addEventListener(type, callback, !!mode); + } + return obj; +} + +function cbi_combobox(id, values, def, man) { + var selid = "cbi.combobox." + id; + if (document.getElementById(selid)) { + return + } + + var obj = document.getElementById(id) + var sel = document.createElement("select"); + sel.id = selid; + sel.className = obj.className.replace(/cbi-input-text/, 'cbi-input-select'); + + if (obj.nextSibling) { + obj.parentNode.insertBefore(sel, obj.nextSibling); + } else { + obj.parentNode.appendChild(sel); + } + + var dt = obj.getAttribute('cbi_datatype'); + var op = obj.getAttribute('cbi_optional'); + + if (dt) + cbi_validate_field(sel, op == 'true', dt); + + if (!values[obj.value]) { + if (obj.value == "") { + var optdef = document.createElement("option"); + optdef.value = ""; + optdef.appendChild(document.createTextNode(def)); + sel.appendChild(optdef); + } else { + var opt = document.createElement("option"); + opt.value = obj.value; + opt.selected = "selected"; + opt.appendChild(document.createTextNode(obj.value)); + sel.appendChild(opt); + } + } + + for (var i in values) { + var opt = document.createElement("option"); + opt.value = i; + + if (obj.value == i) { + opt.selected = "selected"; + } + + opt.appendChild(document.createTextNode(values[i])); + sel.appendChild(opt); + } + + var optman = document.createElement("option"); + optman.value = ""; + optman.appendChild(document.createTextNode(man)); + sel.appendChild(optman); + + obj.style.display = "none"; + + cbi_bind(sel, "change", function() { + if (sel.selectedIndex == sel.options.length - 1) { + obj.style.display = "inline"; + sel.parentNode.removeChild(sel); + obj.focus(); + } else { + obj.value = sel.options[sel.selectedIndex].value; + } + + try { + cbi_d_update(); + } catch (e) { + //Do nothing + } + }) + + // Retrigger validation in select + sel.focus(); + sel.blur(); +} + +function cbi_combobox_init(id, values, def, man) { + var obj = document.getElementById(id); + cbi_bind(obj, "blur", function() { + cbi_combobox(id, values, def, man) + }); + cbi_combobox(id, values, def, man); +} + +function cbi_filebrowser(id, url, defpath) { + var field = document.getElementById(id); + var browser = window.open( + url + ( field.value || defpath || '' ) + '?field=' + id, + "luci_filebrowser", "width=300,height=400,left=100,top=200,scrollbars=yes" + ); + + browser.focus(); +} + +function cbi_browser_init(id, respath, url, defpath) +{ + function cbi_browser_btnclick(e) { + cbi_filebrowser(id, url, defpath); + return false; + } + + var field = document.getElementById(id); + + var btn = document.createElement('img'); + btn.className = 'cbi-image-button'; + btn.src = respath + '/cbi/folder.gif'; + field.parentNode.insertBefore(btn, field.nextSibling); + + cbi_bind(btn, 'click', cbi_browser_btnclick); +} + +function cbi_dynlist_init(name, respath, datatype, optional, choices) +{ + var input0 = document.getElementsByName(name)[0]; + var prefix = input0.name; + var parent = input0.parentNode; + var holder = input0.placeholder; + + var values; + + function cbi_dynlist_redraw(focus, add, del) + { + values = [ ]; + + while (parent.firstChild) + { + var n = parent.firstChild; + var i = parseInt(n.index); + + if (i != del) + { + if (n.nodeName.toLowerCase() == 'input') + values.push(n.value || ''); + else if (n.nodeName.toLowerCase() == 'select') + values[values.length-1] = n.options[n.selectedIndex].value; + } + + parent.removeChild(n); + } + + if (add >= 0) + { + focus = add+1; + values.splice(focus, 0, ''); + } + else if (values.length == 0) + { + focus = 0; + values.push(''); + } + + for (var i = 0; i < values.length; i++) + { + var t = document.createElement('input'); + t.id = prefix + '.' + (i+1); + t.name = prefix; + t.value = values[i]; + t.type = 'text'; + t.index = i; + t.className = 'cbi-input-text'; + + if (i == 0 && holder) + { + t.placeholder = holder; + } + + var b = document.createElement('img'); + b.src = respath + ((i+1) < values.length ? '/cbi/remove.gif' : '/cbi/add.gif'); + b.className = 'cbi-image-button'; + + parent.appendChild(t); + parent.appendChild(b); + parent.appendChild(document.createElement('br')); + + if (datatype) + { + cbi_validate_field(t.id, ((i+1) == values.length) || optional, datatype); + } + + if (choices) + { + cbi_combobox_init(t.id, choices[0], '', choices[1]); + t.nextSibling.index = i; + + cbi_bind(t.nextSibling, 'keydown', cbi_dynlist_keydown); + cbi_bind(t.nextSibling, 'keypress', cbi_dynlist_keypress); + + if (i == focus || -i == focus) + t.nextSibling.focus(); + } + else + { + cbi_bind(t, 'keydown', cbi_dynlist_keydown); + cbi_bind(t, 'keypress', cbi_dynlist_keypress); + + if (i == focus) + { + t.focus(); + } + else if (-i == focus) + { + t.focus(); + + /* force cursor to end */ + var v = t.value; + t.value = ' ' + t.value = v; + } + } + + cbi_bind(b, 'click', cbi_dynlist_btnclick); + } + } + + function cbi_dynlist_keypress(ev) + { + ev = ev ? ev : window.event; + + var se = ev.target ? ev.target : ev.srcElement; + + if (se.nodeType == 3) + se = se.parentNode; + + switch (ev.keyCode) + { + /* backspace, delete */ + case 8: + case 46: + if (se.value.length == 0) + { + if (ev.preventDefault) + ev.preventDefault(); + + return false; + } + + return true; + + /* enter, arrow up, arrow down */ + case 13: + case 38: + case 40: + if (ev.preventDefault) + ev.preventDefault(); + + return false; + } + + return true; + } + + function cbi_dynlist_keydown(ev) + { + ev = ev ? ev : window.event; + + var se = ev.target ? ev.target : ev.srcElement; + + if (se.nodeType == 3) + se = se.parentNode; + + var prev = se.previousSibling; + while (prev && prev.name != name) + prev = prev.previousSibling; + + var next = se.nextSibling; + while (next && next.name != name) + next = next.nextSibling; + + /* advance one further in combobox case */ + if (next && next.nextSibling.name == name) + next = next.nextSibling; + + switch (ev.keyCode) + { + /* backspace, delete */ + case 8: + case 46: + var del = (se.nodeName.toLowerCase() == 'select') + ? true : (se.value.length == 0); + + if (del) + { + if (ev.preventDefault) + ev.preventDefault(); + + var focus = se.index; + if (ev.keyCode == 8) + focus = -focus+1; + + cbi_dynlist_redraw(focus, -1, se.index); + + return false; + } + + break; + + /* enter */ + case 13: + cbi_dynlist_redraw(-1, se.index, -1); + break; + + /* arrow up */ + case 38: + if (prev) + prev.focus(); + + break; + + /* arrow down */ + case 40: + if (next) + next.focus(); + + break; + } + + return true; + } + + function cbi_dynlist_btnclick(ev) + { + ev = ev ? ev : window.event; + + var se = ev.target ? ev.target : ev.srcElement; + + if (se.src.indexOf('remove') > -1) + { + se.previousSibling.value = ''; + + cbi_dynlist_keydown({ + target: se.previousSibling, + keyCode: 8 + }); + } + else + { + cbi_dynlist_keydown({ + target: se.previousSibling, + keyCode: 13 + }); + } + + return false; + } + + cbi_dynlist_redraw(NaN, -1, -1); +} + +//Hijacks the CBI form to send via XHR (requires Prototype) +function cbi_hijack_forms(layer, win, fail, load) { + var forms = layer.getElementsByTagName('form'); + for (var i=0; i<forms.length; i++) { + $(forms[i]).observe('submit', function(event) { + // Prevent the form from also submitting the regular way + event.stop(); + + // Submit via XHR + event.element().request({ + onSuccess: win, + onFailure: fail + }); + + if (load) { + load(); + } + }); + } +} + + +function cbi_t_add(section, tab) { + var t = document.getElementById('tab.' + section + '.' + tab); + var c = document.getElementById('container.' + section + '.' + tab); + + if( t && c ) { + cbi_t[section] = (cbi_t[section] || [ ]); + cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id }; + } +} + +function cbi_t_switch(section, tab) { + if( cbi_t[section] && cbi_t[section][tab] ) { + var o = cbi_t[section][tab]; + var h = document.getElementById('tab.' + section); + for( var tid in cbi_t[section] ) { + var o2 = cbi_t[section][tid]; + if( o.tab.id != o2.tab.id ) { + o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled "); + o2.container.style.display = 'none'; + } + else { + if(h) h.value = tab; + o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab "); + o2.container.style.display = 'block'; + } + } + } + return false +} + +function cbi_t_update() { + var hl_tabs = [ ]; + var updated = false; + + for( var sid in cbi_t ) + for( var tid in cbi_t[sid] ) + { + if( cbi_c[cbi_t[sid][tid].cid] == 0 ) { + cbi_t[sid][tid].tab.style.display = 'none'; + } + else if( cbi_t[sid][tid].tab && cbi_t[sid][tid].tab.style.display == 'none' ) { + cbi_t[sid][tid].tab.style.display = ''; + + var t = cbi_t[sid][tid].tab; + t.className += ' cbi-tab-highlighted'; + hl_tabs.push(t); + } + + cbi_tag_last(cbi_t[sid][tid].container); + updated = true; + } + + if( hl_tabs.length > 0 ) + window.setTimeout(function() { + for( var i = 0; i < hl_tabs.length; i++ ) + hl_tabs[i].className = hl_tabs[i].className.replace(/ cbi-tab-highlighted/g, ''); + }, 750); + + return updated; +} + + +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_reset(form) +{ + window.setTimeout( + function() { cbi_validate_form(form, null) }, 100 + ); + + return true; +} + +function cbi_validate_compile(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 cbi_validators[label] == 'function') + { + stack.push(cbi_validators[label]); + stack.push(null); + } + else + { + throw "Syntax error, unhandled token '"+label+"'"; + } + } + pos = i+1; + } + depth += (code.charCodeAt(i) == 40); + break; + + case 41: + if (--depth <= 0) + { + if (typeof stack[stack.length-2] != 'function') + throw "Syntax error, argument list follows non-function"; + + stack[stack.length-1] = + arguments.callee(code.substring(pos, i)); + + pos = i+1; + } + break; + } + } + + return stack; +} + +function cbi_validate_field(cbid, optional, type) +{ + var field = (typeof cbid == "string") ? document.getElementById(cbid) : cbid; + var vstack; try { vstack = cbi_validate_compile(type); } catch(e) { }; + + if (field && vstack && typeof vstack[0] == "function") + { + var validator = function() + { + // is not detached + if( field.form ) + { + field.className = field.className.replace(/ cbi-input-invalid/g, ''); + + // validate value + var value = (field.options && field.options.selectedIndex > -1) + ? field.options[field.options.selectedIndex].value : field.value; + + if (!(((value.length == 0) && optional) || vstack[0].apply(value, vstack[1]))) + { + // invalid + field.className += ' cbi-input-invalid'; + return false; + } + } + + return true; + }; + + if( ! field.form.cbi_validators ) + field.form.cbi_validators = [ ]; + + field.form.cbi_validators.push(validator); + + cbi_bind(field, "blur", validator); + cbi_bind(field, "keyup", validator); + + if (field.nodeName == 'SELECT') + { + cbi_bind(field, "change", validator); + cbi_bind(field, "click", validator); + } + + field.setAttribute("cbi_validate", validator); + field.setAttribute("cbi_datatype", type); + field.setAttribute("cbi_optional", (!!optional).toString()); + + validator(); + + var fcbox = document.getElementById('cbi.combobox.' + field.id); + if (fcbox) + cbi_validate_field(fcbox, optional, type); + } +} + +function cbi_row_swap(elem, up, store) +{ + var tr = elem.parentNode; + while (tr && tr.nodeName.toLowerCase() != 'tr') + tr = tr.parentNode; + + if (!tr) + return false; + + var table = tr.parentNode; + while (table && table.nodeName.toLowerCase() != 'table') + table = table.parentNode; + + if (!table) + return false; + + var s = up ? 3 : 2; + var e = up ? table.rows.length : table.rows.length - 1; + + for (var idx = s; idx < e; idx++) + { + if (table.rows[idx] == tr) + { + if (up) + tr.parentNode.insertBefore(table.rows[idx], table.rows[idx-1]); + else + tr.parentNode.insertBefore(table.rows[idx+1], table.rows[idx]); + + break; + } + } + + var ids = [ ]; + for (idx = 2; idx < table.rows.length; idx++) + { + table.rows[idx].className = table.rows[idx].className.replace( + /cbi-rowstyle-[12]/, 'cbi-rowstyle-' + (1 + (idx % 2)) + ); + + if (table.rows[idx].id && table.rows[idx].id.match(/-([^\-]+)$/) ) + ids.push(RegExp.$1); + } + + var input = document.getElementById(store); + if (input) + input.value = ids.join(' '); + + return false; +} + +function cbi_tag_last(container) +{ + var last; + + for (var i = 0; i < container.childNodes.length; i++) + { + var c = container.childNodes[i]; + if (c.nodeType == 1 && c.nodeName.toLowerCase() == 'div') + { + c.className = c.className.replace(/ cbi-value-last$/, ''); + last = c; + } + } + + if (last) + { + last.className += ' cbi-value-last'; + } +} + +String.prototype.serialize = function() +{ + var o = this; + switch(typeof(o)) + { + case 'object': + // null + if( o == null ) + { + return 'null'; + } + + // array + else if( o.length ) + { + var i, s = ''; + + for( var i = 0; i < o.length; i++ ) + s += (s ? ', ' : '') + String.serialize(o[i]); + + return '[ ' + s + ' ]'; + } + + // object + else + { + var k, s = ''; + + for( k in o ) + s += (s ? ', ' : '') + k + ': ' + String.serialize(o[k]); + + return '{ ' + s + ' }'; + } + + break; + + case 'string': + // complex string + if( o.match(/[^a-zA-Z0-9_,.: -]/) ) + return 'decodeURIComponent("' + encodeURIComponent(o) + '")'; + + // simple string + else + return '"' + o + '"'; + + break; + + default: + return o.toString(); + } +} + +String.prototype.format = function() +{ + if (!RegExp) + return; + + var html_esc = [/&/g, '&', /"/g, '"', /'/g, ''', /</g, '<', />/g, '>']; + var quot_esc = [/"/g, '"', /'/g, ''']; + + function esc(s, r) { + 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; + + var justifyRight = true; + if (pJustify && pJustify === "-") + justifyRight = false; + + var minLength = -1; + if (pMinLength) + minLength = parseInt(pMinLength); + + var precision = -1; + if (pPrecision && pType == 'f') + precision = parseInt(pPrecision.substring(1)); + + var subst = param; + + switch(pType) + { + case 'b': + subst = (parseInt(param) || 0).toString(2); + break; + + case 'c': + subst = String.fromCharCode(parseInt(param) || 0); + break; + + case 'd': + subst = (parseInt(param) || 0); + break; + + case 'u': + subst = Math.abs(parseInt(param) || 0); + break; + + case 'f': + subst = (precision > -1) + ? ((parseFloat(param) || 0.0)).toFixed(precision) + : (parseFloat(param) || 0.0); + break; + + case 'o': + subst = (parseInt(param) || 0).toString(8); + break; + + case 's': + subst = param; + break; + + case 'x': + subst = ('' + (parseInt(param) || 0).toString(16)).toLowerCase(); + break; + + case 'X': + subst = ('' + (parseInt(param) || 0).toString(16)).toUpperCase(); + break; + + case 'h': + subst = esc(param, html_esc); + break; + + case 'q': + subst = esc(param, quot_esc); + break; + + case 'j': + subst = String.serialize(param); + break; + + case 't': + var td = 0; + var th = 0; + var tm = 0; + var ts = (param || 0); + + if (ts > 60) { + tm = Math.floor(ts / 60); + ts = (ts % 60); + } + + if (tm > 60) { + th = Math.floor(tm / 60); + tm = (tm % 60); + } + + if (th > 24) { + 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 ? parseInt(pMinLength) : 1000; + var pr = pPrecision ? Math.floor(10*parseFloat('0'+pPrecision)) : 2; + + var i = 0; + var val = parseFloat(param || 0); + var units = [ '', 'K', 'M', 'G', 'T', 'P', 'E' ]; + + for (i = 0; (i < units.length) && (val > mf); i++) + val /= mf; + + subst = val.toFixed(pr) + ' ' + units[i]; + break; + } + } + } + + out += leftpart + subst; + str = str.substr(m.length); + } + + return out + str; +} + +String.prototype.nobr = function() +{ + return this.replace(/[\s\n]+/g, ' '); +} + +String.serialize = function() +{ + var a = [ ]; + for (var i = 1; i < arguments.length; i++) + a.push(arguments[i]); + return ''.serialize.apply(arguments[0], a); +} + +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); +} diff --git a/modules/base/htdocs/luci-static/resources/cbi/add.gif b/modules/base/htdocs/luci-static/resources/cbi/add.gif Binary files differnew file mode 100644 index 0000000000..0888abf85e --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/add.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/apply.gif b/modules/base/htdocs/luci-static/resources/cbi/apply.gif Binary files differnew file mode 100644 index 0000000000..82ae7ed821 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/apply.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/arrow.gif b/modules/base/htdocs/luci-static/resources/cbi/arrow.gif Binary files differnew file mode 100644 index 0000000000..10d797e9b0 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/arrow.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/down.gif b/modules/base/htdocs/luci-static/resources/cbi/down.gif Binary files differnew file mode 100644 index 0000000000..f0bb6a4ea6 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/down.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/download.gif b/modules/base/htdocs/luci-static/resources/cbi/download.gif Binary files differnew file mode 100644 index 0000000000..f99a5383b2 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/download.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/edit.gif b/modules/base/htdocs/luci-static/resources/cbi/edit.gif Binary files differnew file mode 100644 index 0000000000..7b02b6e72a --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/edit.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/fieldadd.gif b/modules/base/htdocs/luci-static/resources/cbi/fieldadd.gif Binary files differnew file mode 100644 index 0000000000..431ff64d1f --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/fieldadd.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/file.gif b/modules/base/htdocs/luci-static/resources/cbi/file.gif Binary files differnew file mode 100644 index 0000000000..3b1217dd65 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/file.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/find.gif b/modules/base/htdocs/luci-static/resources/cbi/find.gif Binary files differnew file mode 100644 index 0000000000..9ae5e3489b --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/find.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/folder.gif b/modules/base/htdocs/luci-static/resources/cbi/folder.gif Binary files differnew file mode 100644 index 0000000000..22b583bb59 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/folder.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/help.gif b/modules/base/htdocs/luci-static/resources/cbi/help.gif Binary files differnew file mode 100644 index 0000000000..9dfa0e196a --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/help.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/key.gif b/modules/base/htdocs/luci-static/resources/cbi/key.gif Binary files differnew file mode 100644 index 0000000000..e3853e5afd --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/key.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/link.gif b/modules/base/htdocs/luci-static/resources/cbi/link.gif Binary files differnew file mode 100644 index 0000000000..f0bb78da6b --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/link.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/reload.gif b/modules/base/htdocs/luci-static/resources/cbi/reload.gif Binary files differnew file mode 100644 index 0000000000..8268958a19 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/reload.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/remove.gif b/modules/base/htdocs/luci-static/resources/cbi/remove.gif Binary files differnew file mode 100644 index 0000000000..bf43a0a0bc --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/remove.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/reset.gif b/modules/base/htdocs/luci-static/resources/cbi/reset.gif Binary files differnew file mode 100644 index 0000000000..c941c19028 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/reset.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/save.gif b/modules/base/htdocs/luci-static/resources/cbi/save.gif Binary files differnew file mode 100644 index 0000000000..35e949963e --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/save.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/up.gif b/modules/base/htdocs/luci-static/resources/cbi/up.gif Binary files differnew file mode 100644 index 0000000000..e8234178ef --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/up.gif diff --git a/modules/base/htdocs/luci-static/resources/cbi/user.gif b/modules/base/htdocs/luci-static/resources/cbi/user.gif Binary files differnew file mode 100644 index 0000000000..dcb5c2a899 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/cbi/user.gif diff --git a/modules/base/htdocs/luci-static/resources/icons/bridge.png b/modules/base/htdocs/luci-static/resources/icons/bridge.png Binary files differnew file mode 100644 index 0000000000..4c163bf692 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/bridge.png diff --git a/modules/base/htdocs/luci-static/resources/icons/bridge_disabled.png b/modules/base/htdocs/luci-static/resources/icons/bridge_disabled.png Binary files differnew file mode 100644 index 0000000000..0f367c5369 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/bridge_disabled.png diff --git a/modules/base/htdocs/luci-static/resources/icons/encryption.png b/modules/base/htdocs/luci-static/resources/icons/encryption.png Binary files differnew file mode 100644 index 0000000000..41d2ba9ace --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/encryption.png diff --git a/modules/base/htdocs/luci-static/resources/icons/encryption_disabled.png b/modules/base/htdocs/luci-static/resources/icons/encryption_disabled.png Binary files differnew file mode 100644 index 0000000000..f2e05a4251 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/encryption_disabled.png diff --git a/modules/base/htdocs/luci-static/resources/icons/ethernet.png b/modules/base/htdocs/luci-static/resources/icons/ethernet.png Binary files differnew file mode 100644 index 0000000000..a02538124c --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/ethernet.png diff --git a/modules/base/htdocs/luci-static/resources/icons/ethernet_disabled.png b/modules/base/htdocs/luci-static/resources/icons/ethernet_disabled.png Binary files differnew file mode 100644 index 0000000000..2bb02e455a --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/ethernet_disabled.png diff --git a/modules/base/htdocs/luci-static/resources/icons/loading.gif b/modules/base/htdocs/luci-static/resources/icons/loading.gif Binary files differnew file mode 100644 index 0000000000..5bb90fd6a4 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/loading.gif diff --git a/modules/base/htdocs/luci-static/resources/icons/port_down.png b/modules/base/htdocs/luci-static/resources/icons/port_down.png Binary files differnew file mode 100644 index 0000000000..25ea172324 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/port_down.png diff --git a/modules/base/htdocs/luci-static/resources/icons/port_up.png b/modules/base/htdocs/luci-static/resources/icons/port_up.png Binary files differnew file mode 100644 index 0000000000..e063037910 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/port_up.png diff --git a/modules/base/htdocs/luci-static/resources/icons/signal-0-25.png b/modules/base/htdocs/luci-static/resources/icons/signal-0-25.png Binary files differnew file mode 100644 index 0000000000..2e5dff4663 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/signal-0-25.png diff --git a/modules/base/htdocs/luci-static/resources/icons/signal-0.png b/modules/base/htdocs/luci-static/resources/icons/signal-0.png Binary files differnew file mode 100644 index 0000000000..114583a676 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/signal-0.png diff --git a/modules/base/htdocs/luci-static/resources/icons/signal-25-50.png b/modules/base/htdocs/luci-static/resources/icons/signal-25-50.png Binary files differnew file mode 100644 index 0000000000..ee8cc4f1ce --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/signal-25-50.png diff --git a/modules/base/htdocs/luci-static/resources/icons/signal-50-75.png b/modules/base/htdocs/luci-static/resources/icons/signal-50-75.png Binary files differnew file mode 100644 index 0000000000..26bcbf715d --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/signal-50-75.png diff --git a/modules/base/htdocs/luci-static/resources/icons/signal-75-100.png b/modules/base/htdocs/luci-static/resources/icons/signal-75-100.png Binary files differnew file mode 100644 index 0000000000..5cffaa1b84 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/signal-75-100.png diff --git a/modules/base/htdocs/luci-static/resources/icons/signal-none.png b/modules/base/htdocs/luci-static/resources/icons/signal-none.png Binary files differnew file mode 100644 index 0000000000..b77585c0f0 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/signal-none.png diff --git a/modules/base/htdocs/luci-static/resources/icons/switch.png b/modules/base/htdocs/luci-static/resources/icons/switch.png Binary files differnew file mode 100644 index 0000000000..5c99ba5684 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/switch.png diff --git a/modules/base/htdocs/luci-static/resources/icons/switch_disabled.png b/modules/base/htdocs/luci-static/resources/icons/switch_disabled.png Binary files differnew file mode 100644 index 0000000000..b8c84c8dc4 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/switch_disabled.png diff --git a/modules/base/htdocs/luci-static/resources/icons/tunnel.png b/modules/base/htdocs/luci-static/resources/icons/tunnel.png Binary files differnew file mode 100644 index 0000000000..c5a09dd685 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/tunnel.png diff --git a/modules/base/htdocs/luci-static/resources/icons/tunnel_disabled.png b/modules/base/htdocs/luci-static/resources/icons/tunnel_disabled.png Binary files differnew file mode 100644 index 0000000000..ad9856cfec --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/tunnel_disabled.png diff --git a/modules/base/htdocs/luci-static/resources/icons/vlan.png b/modules/base/htdocs/luci-static/resources/icons/vlan.png Binary files differnew file mode 100644 index 0000000000..5c99ba5684 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/vlan.png diff --git a/modules/base/htdocs/luci-static/resources/icons/vlan_disabled.png b/modules/base/htdocs/luci-static/resources/icons/vlan_disabled.png Binary files differnew file mode 100644 index 0000000000..b8c84c8dc4 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/vlan_disabled.png diff --git a/modules/base/htdocs/luci-static/resources/icons/wifi.png b/modules/base/htdocs/luci-static/resources/icons/wifi.png Binary files differnew file mode 100644 index 0000000000..528ce2b4e9 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/wifi.png diff --git a/modules/base/htdocs/luci-static/resources/icons/wifi_big.png b/modules/base/htdocs/luci-static/resources/icons/wifi_big.png Binary files differnew file mode 100644 index 0000000000..d73a5e7401 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/wifi_big.png diff --git a/modules/base/htdocs/luci-static/resources/icons/wifi_big_disabled.png b/modules/base/htdocs/luci-static/resources/icons/wifi_big_disabled.png Binary files differnew file mode 100644 index 0000000000..af93b37b7a --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/wifi_big_disabled.png diff --git a/modules/base/htdocs/luci-static/resources/icons/wifi_disabled.png b/modules/base/htdocs/luci-static/resources/icons/wifi_disabled.png Binary files differnew file mode 100644 index 0000000000..338a34f78c --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/icons/wifi_disabled.png diff --git a/modules/base/htdocs/luci-static/resources/xhr.js b/modules/base/htdocs/luci-static/resources/xhr.js new file mode 100644 index 0000000000..701c12ac19 --- /dev/null +++ b/modules/base/htdocs/luci-static/resources/xhr.js @@ -0,0 +1,241 @@ +/* + * xhr.js - XMLHttpRequest helper class + * (c) 2008-2010 Jo-Philipp Wich + */ + +XHR = function() +{ + this.reinit = function() + { + if (window.XMLHttpRequest) { + this._xmlHttp = new XMLHttpRequest(); + } + else if (window.ActiveXObject) { + this._xmlHttp = new ActiveXObject("Microsoft.XMLHTTP"); + } + else { + alert("xhr.js: XMLHttpRequest is not supported by this browser!"); + } + } + + this.busy = function() { + if (!this._xmlHttp) + return false; + + switch (this._xmlHttp.readyState) + { + case 1: + case 2: + case 3: + return true; + + default: + return false; + } + } + + this.abort = function() { + if (this.busy()) + this._xmlHttp.abort(); + } + + this.get = function(url,data,callback) + { + this.reinit(); + + var xhr = this._xmlHttp; + var code = this._encode(data); + + url = location.protocol + '//' + location.host + url; + + if (code) + if (url.substr(url.length-1,1) == '&') + url += code; + else + url += '?' + code; + + xhr.open('GET', url, true); + + xhr.onreadystatechange = function() + { + if (xhr.readyState == 4) { + var json = null; + if (xhr.getResponseHeader("Content-Type") == "application/json") { + try { + json = eval('(' + xhr.responseText + ')'); + } + catch(e) { + json = null; + } + } + + callback(xhr, json); + } + } + + xhr.send(null); + } + + this.post = function(url,data,callback) + { + this.reinit(); + + var xhr = this._xmlHttp; + var code = this._encode(data); + + xhr.onreadystatechange = function() + { + if (xhr.readyState == 4) + callback(xhr); + } + + xhr.open('POST', url, true); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.setRequestHeader('Content-length', code.length); + xhr.setRequestHeader('Connection', 'close'); + xhr.send(code); + } + + this.cancel = function() + { + this._xmlHttp.onreadystatechange = function(){}; + this._xmlHttp.abort(); + } + + this.send_form = function(form,callback,extra_values) + { + var code = ''; + + for (var i = 0; i < form.elements.length; i++) + { + var e = form.elements[i]; + + if (e.options) + { + code += (code ? '&' : '') + + form.elements[i].name + '=' + encodeURIComponent( + e.options[e.selectedIndex].value + ); + } + else if (e.length) + { + for (var j = 0; j < e.length; j++) + if (e[j].name) { + code += (code ? '&' : '') + + e[j].name + '=' + encodeURIComponent(e[j].value); + } + } + else + { + code += (code ? '&' : '') + + e.name + '=' + encodeURIComponent(e.value); + } + } + + if (typeof extra_values == 'object') + for (var key in extra_values) + code += (code ? '&' : '') + + key + '=' + encodeURIComponent(extra_values[key]); + + return( + (form.method == 'get') + ? this.get(form.getAttribute('action'), code, callback) + : this.post(form.getAttribute('action'), code, callback) + ); + } + + this._encode = function(obj) + { + obj = obj ? obj : { }; + obj['_'] = Math.random(); + + if (typeof obj == 'object') + { + var code = ''; + var self = this; + + for (var k in obj) + code += (code ? '&' : '') + + k + '=' + encodeURIComponent(obj[k]); + + return code; + } + + return obj; + } +} + +XHR.get = function(url, data, callback) +{ + (new XHR()).get(url, data, callback); +} + +XHR.poll = function(interval, url, data, callback) +{ + if (isNaN(interval) || interval < 1) + interval = 5; + + if (!XHR._q) + { + XHR._t = 0; + XHR._q = [ ]; + XHR._r = function() { + for (var i = 0, e = XHR._q[0]; i < XHR._q.length; e = XHR._q[++i]) + { + if (!(XHR._t % e.interval) && !e.xhr.busy()) + e.xhr.get(e.url, e.data, e.callback); + } + + XHR._t++; + }; + } + + XHR._q.push({ + interval: interval, + callback: callback, + url: url, + data: data, + xhr: new XHR() + }); + + XHR.run(); +} + +XHR.halt = function() +{ + if (XHR._i) + { + /* show & set poll indicator */ + try { + document.getElementById('xhr_poll_status').style.display = ''; + document.getElementById('xhr_poll_status_on').style.display = 'none'; + document.getElementById('xhr_poll_status_off').style.display = ''; + } catch(e) { } + + window.clearInterval(XHR._i); + XHR._i = null; + } +} + +XHR.run = function() +{ + if (XHR._r && !XHR._i) + { + /* show & set poll indicator */ + try { + document.getElementById('xhr_poll_status').style.display = ''; + document.getElementById('xhr_poll_status_on').style.display = ''; + document.getElementById('xhr_poll_status_off').style.display = 'none'; + } catch(e) { } + + /* kick first round manually to prevent one second lag when setting up + * the poll interval */ + XHR._r(); + XHR._i = window.setInterval(XHR._r, 1000); + } +} + +XHR.running = function() +{ + return !!(XHR._r && XHR._i); +} diff --git a/modules/base/luasrc/ccache.lua b/modules/base/luasrc/ccache.lua new file mode 100644 index 0000000000..56ccbc3efe --- /dev/null +++ b/modules/base/luasrc/ccache.lua @@ -0,0 +1,87 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2008 Steven Barth <steven@midlink.org> +Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +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 diff --git a/modules/base/luasrc/controller/admin/servicectl.lua b/modules/base/luasrc/controller/admin/servicectl.lua new file mode 100644 index 0000000000..753d2c77f1 --- /dev/null +++ b/modules/base/luasrc/controller/admin/servicectl.lua @@ -0,0 +1,60 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2010 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +module("luci.controller.admin.servicectl", package.seeall) + +function index() + entry({"servicectl"}, alias("servicectl", "status")).sysauth = "root" + entry({"servicectl", "status"}, call("action_status")).leaf = true + entry({"servicectl", "restart"}, call("action_restart")).leaf = true +end + +function action_status() + local data = nixio.fs.readfile("/var/run/luci-reload-status") + if data then + luci.http.write("/etc/config/") + luci.http.write(data) + else + luci.http.write("finish") + end +end + +function action_restart(args) + local uci = require "luci.model.uci".cursor() + if args then + local service + local services = { } + + for service in args:gmatch("[%w_-]+") do + services[#services+1] = service + end + + local command = uci:apply(services, true) + if nixio.fork() == 0 then + local i = nixio.open("/dev/null", "r") + local o = nixio.open("/dev/null", "w") + + nixio.dup(i, nixio.stdin) + nixio.dup(o, nixio.stdout) + + i:close() + o:close() + + nixio.exec("/bin/sh", unpack(command)) + else + luci.http.write("OK") + os.exit(0) + end + end +end diff --git a/modules/base/luasrc/debug.lua b/modules/base/luasrc/debug.lua new file mode 100644 index 0000000000..8ff1bb6981 --- /dev/null +++ b/modules/base/luasrc/debug.lua @@ -0,0 +1,37 @@ +local debug = require "debug" +local io = require "io" +local collectgarbage, floor = collectgarbage, math.floor + +module "luci.debug" +__file__ = debug.getinfo(1, 'S').source:sub(2) + +-- Enables the memory tracer with given flags and returns a function to disable the tracer again +function trap_memtrace(flags, dest) + flags = flags or "clr" + local tracefile = io.open(dest or "/tmp/memtrace", "w") + local peak = 0 + + local function trap(what, line) + local info = debug.getinfo(2, "Sn") + local size = floor(collectgarbage("count")) + if size > peak then + peak = size + end + if tracefile then + tracefile:write( + "[", what, "] ", info.source, ":", (line or "?"), "\t", + (info.namewhat or ""), "\t", + (info.name or ""), "\t", + size, " (", peak, ")\n" + ) + end + end + + debug.sethook(trap, flags) + + return function() + debug.sethook() + tracefile:close() + end +end + diff --git a/modules/base/luasrc/fs.lua b/modules/base/luasrc/fs.lua new file mode 100644 index 0000000000..a81ff675d4 --- /dev/null +++ b/modules/base/luasrc/fs.lua @@ -0,0 +1,244 @@ +--[[ +LuCI - Filesystem tools + +Description: +A module offering often needed filesystem manipulation functions + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +local io = require "io" +local os = require "os" +local ltn12 = require "luci.ltn12" +local fs = require "nixio.fs" +local nutil = require "nixio.util" + +local type = type + +--- LuCI filesystem library. +module "luci.fs" + +--- Test for file access permission on given path. +-- @class function +-- @name access +-- @param str String value containing the path +-- @return Number containing the return code, 0 on sucess or nil on error +-- @return String containing the error description (if any) +-- @return Number containing the os specific errno (if any) +access = fs.access + +--- Evaluate given shell glob pattern and return a table containing all matching +-- file and directory entries. +-- @class function +-- @name glob +-- @param filename String containing the path of the file to read +-- @return Table containing file and directory entries or nil if no matches +-- @return String containing the error description (if no matches) +-- @return Number containing the os specific errno (if no matches) +function glob(...) + local iter, code, msg = fs.glob(...) + if iter then + return nutil.consume(iter) + else + return nil, code, msg + end +end + +--- Checks wheather the given path exists and points to a regular file. +-- @param filename String containing the path of the file to test +-- @return Boolean indicating wheather given path points to regular file +function isfile(filename) + return fs.stat(filename, "type") == "reg" +end + +--- Checks wheather the given path exists and points to a directory. +-- @param dirname String containing the path of the directory to test +-- @return Boolean indicating wheather given path points to directory +function isdirectory(dirname) + return fs.stat(dirname, "type") == "dir" +end + +--- Read the whole content of the given file into memory. +-- @param filename String containing the path of the file to read +-- @return String containing the file contents or nil on error +-- @return String containing the error message on error +readfile = fs.readfile + +--- Write the contents of given string to given file. +-- @param filename String containing the path of the file to read +-- @param data String containing the data to write +-- @return Boolean containing true on success or nil on error +-- @return String containing the error message on error +writefile = fs.writefile + +--- Copies a file. +-- @param source Source file +-- @param dest Destination +-- @return Boolean containing true on success or nil on error +copy = fs.datacopy + +--- Renames a file. +-- @param source Source file +-- @param dest Destination +-- @return Boolean containing true on success or nil on error +rename = fs.move + +--- Get the last modification time of given file path in Unix epoch format. +-- @param path String containing the path of the file or directory to read +-- @return Number containing the epoch time or nil on error +-- @return String containing the error description (if any) +-- @return Number containing the os specific errno (if any) +function mtime(path) + return fs.stat(path, "mtime") +end + +--- Set the last modification time of given file path in Unix epoch format. +-- @param path String containing the path of the file or directory to read +-- @param mtime Last modification timestamp +-- @param atime Last accessed timestamp +-- @return 0 in case of success nil on error +-- @return String containing the error description (if any) +-- @return Number containing the os specific errno (if any) +function utime(path, mtime, atime) + return fs.utimes(path, atime, mtime) +end + +--- Return the last element - usually the filename - from the given path with +-- the directory component stripped. +-- @class function +-- @name basename +-- @param path String containing the path to strip +-- @return String containing the base name of given path +-- @see dirname +basename = fs.basename + +--- Return the directory component of the given path with the last element +-- stripped of. +-- @class function +-- @name dirname +-- @param path String containing the path to strip +-- @return String containing the directory component of given path +-- @see basename +dirname = fs.dirname + +--- Return a table containing all entries of the specified directory. +-- @class function +-- @name dir +-- @param path String containing the path of the directory to scan +-- @return Table containing file and directory entries or nil on error +-- @return String containing the error description on error +-- @return Number containing the os specific errno on error +function dir(...) + local iter, code, msg = fs.dir(...) + if iter then + local t = nutil.consume(iter) + t[#t+1] = "." + t[#t+1] = ".." + return t + else + return nil, code, msg + end +end + +--- Create a new directory, recursively on demand. +-- @param path String with the name or path of the directory to create +-- @param recursive Create multiple directory levels (optional, default is true) +-- @return Number with the return code, 0 on sucess or nil on error +-- @return String containing the error description on error +-- @return Number containing the os specific errno on error +function mkdir(path, recursive) + return recursive and fs.mkdirr(path) or fs.mkdir(path) +end + +--- Remove the given empty directory. +-- @class function +-- @name rmdir +-- @param path String containing the path of the directory to remove +-- @return Number with the return code, 0 on sucess or nil on error +-- @return String containing the error description on error +-- @return Number containing the os specific errno on error +rmdir = fs.rmdir + +local stat_tr = { + reg = "regular", + dir = "directory", + lnk = "link", + chr = "character device", + blk = "block device", + fifo = "fifo", + sock = "socket" +} +--- Get information about given file or directory. +-- @class function +-- @name stat +-- @param path String containing the path of the directory to query +-- @return Table containing file or directory properties or nil on error +-- @return String containing the error description on error +-- @return Number containing the os specific errno on error +function stat(path, key) + local data, code, msg = fs.stat(path) + if data then + data.mode = data.modestr + data.type = stat_tr[data.type] or "?" + end + return key and data and data[key] or data, code, msg +end + +--- Set permissions on given file or directory. +-- @class function +-- @name chmod +-- @param path String containing the path of the directory +-- @param perm String containing the permissions to set ([ugoa][+-][rwx]) +-- @return Number with the return code, 0 on sucess or nil on error +-- @return String containing the error description on error +-- @return Number containing the os specific errno on error +chmod = fs.chmod + +--- Create a hard- or symlink from given file (or directory) to specified target +-- file (or directory) path. +-- @class function +-- @name link +-- @param path1 String containing the source path to link +-- @param path2 String containing the destination path for the link +-- @param symlink Boolean indicating wheather to create a symlink (optional) +-- @return Number with the return code, 0 on sucess or nil on error +-- @return String containing the error description on error +-- @return Number containing the os specific errno on error +function link(src, dest, sym) + return sym and fs.symlink(src, dest) or fs.link(src, dest) +end + +--- Remove the given file. +-- @class function +-- @name unlink +-- @param path String containing the path of the file to remove +-- @return Number with the return code, 0 on sucess or nil on error +-- @return String containing the error description on error +-- @return Number containing the os specific errno on error +unlink = fs.unlink + +--- Retrieve target of given symlink. +-- @class function +-- @name readlink +-- @param path String containing the path of the symlink to read +-- @return String containing the link target or nil on error +-- @return String containing the error description on error +-- @return Number containing the os specific errno on error +readlink = fs.readlink diff --git a/modules/base/luasrc/init.lua b/modules/base/luasrc/init.lua new file mode 100644 index 0000000000..4d66e86734 --- /dev/null +++ b/modules/base/luasrc/init.lua @@ -0,0 +1,39 @@ +--[[ +LuCI - Lua Configuration Interface + +Description: +Main class + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +local require = require + +-- Make sure that bitlib is loaded +if not _G.bit then + _G.bit = require "bit" +end + +module "luci" + +local v = require "luci.version" + +__version__ = v.luciversion or "trunk" +__appname__ = v.luciname or "LuCI" diff --git a/modules/base/luasrc/ip.lua b/modules/base/luasrc/ip.lua new file mode 100644 index 0000000000..60ca090135 --- /dev/null +++ b/modules/base/luasrc/ip.lua @@ -0,0 +1,673 @@ +--[[ + +LuCI ip calculation libarary +(c) 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net> +(c) 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +--- LuCI IP calculation library. +module( "luci.ip", package.seeall ) + +require "nixio" +local bit = nixio.bit +local util = require "luci.util" + +--- Boolean; true if system is little endian +LITTLE_ENDIAN = not util.bigendian() + +--- Boolean; true if system is big endian +BIG_ENDIAN = not LITTLE_ENDIAN + +--- Specifier for IPv4 address family +FAMILY_INET4 = 0x04 + +--- Specifier for IPv6 address family +FAMILY_INET6 = 0x06 + + +local function __bless(x) + return setmetatable( x, { + __index = luci.ip.cidr, + __add = luci.ip.cidr.add, + __sub = luci.ip.cidr.sub, + __lt = luci.ip.cidr.lower, + __eq = luci.ip.cidr.equal, + __le = + function(...) + return luci.ip.cidr.equal(...) or luci.ip.cidr.lower(...) + end + } ) +end + +local function __array16( x, family ) + local list + + if type(x) == "number" then + list = { bit.rshift(x, 16), bit.band(x, 0xFFFF) } + + elseif type(x) == "string" then + if x:find(":") then x = IPv6(x) else x = IPv4(x) end + if x then + assert( x[1] == family, "Can't mix IPv4 and IPv6 addresses" ) + list = { unpack(x[2]) } + end + + elseif type(x) == "table" and type(x[2]) == "table" then + assert( x[1] == family, "Can't mix IPv4 and IPv6 addresses" ) + list = { unpack(x[2]) } + + elseif type(x) == "table" then + list = { unpack(x) } + end + + assert( list, "Invalid operand" ) + + return list +end + +local function __mask16(bits) + return bit.lshift( bit.rshift( 0xFFFF, 16 - bits % 16 ), 16 - bits % 16 ) +end + +local function __not16(bits) + return bit.band( bit.bnot( __mask16(bits) ), 0xFFFF ) +end + +local function __maxlen(family) + return ( family == FAMILY_INET4 ) and 32 or 128 +end + +local function __sublen(family) + return ( family == FAMILY_INET4 ) and 30 or 127 +end + + +--- Convert given short value to network byte order on little endian hosts +-- @param x Unsigned integer value between 0x0000 and 0xFFFF +-- @return Byte-swapped value +-- @see htonl +-- @see ntohs +function htons(x) + if LITTLE_ENDIAN then + return bit.bor( + bit.rshift( x, 8 ), + bit.band( bit.lshift( x, 8 ), 0xFF00 ) + ) + else + return x + end +end + +--- Convert given long value to network byte order on little endian hosts +-- @param x Unsigned integer value between 0x00000000 and 0xFFFFFFFF +-- @return Byte-swapped value +-- @see htons +-- @see ntohl +function htonl(x) + if LITTLE_ENDIAN then + return bit.bor( + bit.lshift( htons( bit.band( x, 0xFFFF ) ), 16 ), + htons( bit.rshift( x, 16 ) ) + ) + else + return x + end +end + +--- Convert given short value to host byte order on little endian hosts +-- @class function +-- @name ntohs +-- @param x Unsigned integer value between 0x0000 and 0xFFFF +-- @return Byte-swapped value +-- @see htonl +-- @see ntohs +ntohs = htons + +--- Convert given short value to host byte order on little endian hosts +-- @class function +-- @name ntohl +-- @param x Unsigned integer value between 0x00000000 and 0xFFFFFFFF +-- @return Byte-swapped value +-- @see htons +-- @see ntohl +ntohl = htonl + + +--- Parse given IPv4 address in dotted quad or CIDR notation. If an optional +-- netmask is given as second argument and the IP address is encoded in CIDR +-- notation then the netmask parameter takes precedence. If neither a CIDR +-- encoded prefix nor a netmask parameter is given, then a prefix length of +-- 32 bit is assumed. +-- @param address IPv4 address in dotted quad or CIDR notation +-- @param netmask IPv4 netmask in dotted quad notation (optional) +-- @return luci.ip.cidr instance or nil if given address was invalid +-- @see IPv6 +-- @see Hex +function IPv4(address, netmask) + address = address or "0.0.0.0/0" + + local obj = __bless({ FAMILY_INET4 }) + + local data = {} + local prefix = address:match("/(.+)") + address = address:gsub("/.+","") + address = address:gsub("^%[(.*)%]$", "%1"):upper():gsub("^::FFFF:", "") + + if netmask then + prefix = obj:prefix(netmask) + elseif prefix then + prefix = tonumber(prefix) + if not prefix or prefix < 0 or prefix > 32 then return nil end + else + prefix = 32 + end + + local b1, b2, b3, b4 = address:match("^(%d+)%.(%d+)%.(%d+)%.(%d+)$") + + b1 = tonumber(b1) + b2 = tonumber(b2) + b3 = tonumber(b3) + b4 = tonumber(b4) + + if b1 and b1 <= 255 and + b2 and b2 <= 255 and + b3 and b3 <= 255 and + b4 and b4 <= 255 and + prefix + then + table.insert(obj, { b1 * 256 + b2, b3 * 256 + b4 }) + table.insert(obj, prefix) + return obj + end +end + +--- Parse given IPv6 address in full, compressed, mixed or CIDR notation. +-- If an optional netmask is given as second argument and the IP address is +-- encoded in CIDR notation then the netmask parameter takes precedence. +-- If neither a CIDR encoded prefix nor a netmask parameter is given, then a +-- prefix length of 128 bit is assumed. +-- @param address IPv6 address in full/compressed/mixed or CIDR notation +-- @param netmask IPv6 netmask in full/compressed/mixed notation (optional) +-- @return luci.ip.cidr instance or nil if given address was invalid +-- @see IPv4 +-- @see Hex +function IPv6(address, netmask) + address = address or "::/0" + + local obj = __bless({ FAMILY_INET6 }) + + local data = {} + local prefix = address:match("/(.+)") + address = address:gsub("/.+","") + address = address:gsub("^%[(.*)%]$", "%1") + + if netmask then + prefix = obj:prefix(netmask) + elseif prefix then + prefix = tonumber(prefix) + if not prefix or prefix < 0 or prefix > 128 then return nil end + else + prefix = 128 + end + + local borderl = address:sub(1, 1) == ":" and 2 or 1 + local borderh, zeroh, chunk, block, i + + if #address > 45 then return nil end + + repeat + borderh = address:find(":", borderl, true) + if not borderh then break end + + block = tonumber(address:sub(borderl, borderh - 1), 16) + if block and block <= 0xFFFF then + data[#data+1] = block + else + if zeroh or borderh - borderl > 1 then return nil end + zeroh = #data + 1 + end + + borderl = borderh + 1 + until #data == 7 + + chunk = address:sub(borderl) + if #chunk > 0 and #chunk <= 4 then + block = tonumber(chunk, 16) + if not block or block > 0xFFFF then return nil end + + data[#data+1] = block + elseif #chunk > 4 then + if #data == 7 or #chunk > 15 then return nil end + borderl = 1 + for i=1, 4 do + borderh = chunk:find(".", borderl, true) + if not borderh and i < 4 then return nil end + borderh = borderh and borderh - 1 + + block = tonumber(chunk:sub(borderl, borderh)) + if not block or block > 255 then return nil end + + if i == 1 or i == 3 then + data[#data+1] = block * 256 + else + data[#data] = data[#data] + block + end + + borderl = borderh and borderh + 2 + end + end + + if zeroh then + if #data == 8 then return nil end + while #data < 8 do + table.insert(data, zeroh, 0) + end + end + + if #data == 8 and prefix then + table.insert(obj, data) + table.insert(obj, prefix) + return obj + end +end + +--- Transform given hex-encoded value to luci.ip.cidr instance of specified +-- address family. +-- @param hex String containing hex encoded value +-- @param prefix Prefix length of CIDR instance (optional, default is 32/128) +-- @param family Address family, either luci.ip.FAMILY_INET4 or FAMILY_INET6 +-- @param swap Bool indicating whether to swap byteorder on low endian host +-- @return luci.ip.cidr instance or nil if given value was invalid +-- @see IPv4 +-- @see IPv6 +function Hex( hex, prefix, family, swap ) + family = ( family ~= nil ) and family or FAMILY_INET4 + swap = ( swap == nil ) and true or swap + prefix = prefix or __maxlen(family) + + local len = __maxlen(family) + local tmp = "" + local data = { } + local i + + for i = 1, (len/4) - #hex do tmp = tmp .. '0' end + + if swap and LITTLE_ENDIAN then + for i = #hex, 1, -2 do tmp = tmp .. hex:sub( i - 1, i ) end + else + tmp = tmp .. hex + end + + hex = tmp + + for i = 1, ( len / 4 ), 4 do + local n = tonumber( hex:sub( i, i+3 ), 16 ) + if n then + data[#data+1] = n + else + return nil + end + end + + return __bless({ family, data, prefix }) +end + + +--- LuCI IP Library / CIDR instances +-- @class module +-- @cstyle instance +-- @name luci.ip.cidr +cidr = util.class() + +--- Test whether the instance is a IPv4 address. +-- @return Boolean indicating a IPv4 address type +-- @see cidr.is6 +function cidr.is4( self ) + return self[1] == FAMILY_INET4 +end + +--- Test whether this instance is an IPv4 RFC1918 private address +-- @return Boolean indicating whether this instance is an RFC1918 address +function cidr.is4rfc1918( self ) + if self[1] == FAMILY_INET4 then + return ((self[2][1] >= 0x0A00) and (self[2][1] <= 0x0AFF)) or + ((self[2][1] >= 0xAC10) and (self[2][1] <= 0xAC1F)) or + (self[2][1] == 0xC0A8) + end + return false +end + +--- Test whether this instance is an IPv4 link-local address (Zeroconf) +-- @return Boolean indicating whether this instance is IPv4 link-local +function cidr.is4linklocal( self ) + if self[1] == FAMILY_INET4 then + return (self[2][1] == 0xA9FE) + end + return false +end + +--- Test whether the instance is a IPv6 address. +-- @return Boolean indicating a IPv6 address type +-- @see cidr.is4 +function cidr.is6( self ) + return self[1] == FAMILY_INET6 +end + +--- Test whether this instance is an IPv6 link-local address +-- @return Boolean indicating whether this instance is IPv6 link-local +function cidr.is6linklocal( self ) + if self[1] == FAMILY_INET6 then + return (self[2][1] >= 0xFE80) and (self[2][1] <= 0xFEBF) + end + return false +end + +--- Return a corresponding string representation of the instance. +-- If the prefix length is lower then the maximum possible prefix length for the +-- corresponding address type then the address is returned in CIDR notation, +-- otherwise the prefix will be left out. +function cidr.string( self ) + local str + if self:is4() then + str = string.format( + "%d.%d.%d.%d", + bit.rshift(self[2][1], 8), bit.band(self[2][1], 0xFF), + bit.rshift(self[2][2], 8), bit.band(self[2][2], 0xFF) + ) + if self[3] < 32 then + str = str .. "/" .. self[3] + end + elseif self:is6() then + str = string.format( "%X:%X:%X:%X:%X:%X:%X:%X", unpack(self[2]) ) + if self[3] < 128 then + str = str .. "/" .. self[3] + end + end + return str +end + +--- Test whether the value of the instance is lower then the given address. +-- This function will throw an exception if the given address has a different +-- family than this instance. +-- @param addr A luci.ip.cidr instance to compare against +-- @return Boolean indicating whether this instance is lower +-- @see cidr.higher +-- @see cidr.equal +function cidr.lower( self, addr ) + assert( self[1] == addr[1], "Can't compare IPv4 and IPv6 addresses" ) + local i + for i = 1, #self[2] do + if self[2][i] ~= addr[2][i] then + return self[2][i] < addr[2][i] + end + end + return false +end + +--- Test whether the value of the instance is higher then the given address. +-- This function will throw an exception if the given address has a different +-- family than this instance. +-- @param addr A luci.ip.cidr instance to compare against +-- @return Boolean indicating whether this instance is higher +-- @see cidr.lower +-- @see cidr.equal +function cidr.higher( self, addr ) + assert( self[1] == addr[1], "Can't compare IPv4 and IPv6 addresses" ) + local i + for i = 1, #self[2] do + if self[2][i] ~= addr[2][i] then + return self[2][i] > addr[2][i] + end + end + return false +end + +--- Test whether the value of the instance is equal to the given address. +-- This function will throw an exception if the given address is a different +-- family than this instance. +-- @param addr A luci.ip.cidr instance to compare against +-- @return Boolean indicating whether this instance is equal +-- @see cidr.lower +-- @see cidr.higher +function cidr.equal( self, addr ) + assert( self[1] == addr[1], "Can't compare IPv4 and IPv6 addresses" ) + local i + for i = 1, #self[2] do + if self[2][i] ~= addr[2][i] then + return false + end + end + return true +end + +--- Return the prefix length of this CIDR instance. +-- @param mask Override instance prefix with given netmask (optional) +-- @return Prefix length in bit +function cidr.prefix( self, mask ) + local prefix = self[3] + + if mask then + prefix = 0 + + local stop = false + local obj = type(mask) ~= "table" + and ( self:is4() and IPv4(mask) or IPv6(mask) ) or mask + + if not obj then return nil end + + local _, word + for _, word in ipairs(obj[2]) do + if word == 0xFFFF then + prefix = prefix + 16 + else + local bitmask = bit.lshift(1, 15) + while bit.band(word, bitmask) == bitmask do + prefix = prefix + 1 + bitmask = bit.lshift(1, 15 - (prefix % 16)) + end + + break + end + end + end + + return prefix +end + +--- Return a corresponding CIDR representing the network address of this +-- instance. +-- @param bits Override prefix length of this instance (optional) +-- @return CIDR instance containing the network address +-- @see cidr.host +-- @see cidr.broadcast +-- @see cidr.mask +function cidr.network( self, bits ) + local data = { } + bits = bits or self[3] + + local i + for i = 1, math.floor( bits / 16 ) do + data[#data+1] = self[2][i] + end + + if #data < #self[2] then + data[#data+1] = bit.band( self[2][1+#data], __mask16(bits) ) + + for i = #data + 1, #self[2] do + data[#data+1] = 0 + end + end + + return __bless({ self[1], data, __maxlen(self[1]) }) +end + +--- Return a corresponding CIDR representing the host address of this +-- instance. This is intended to extract the host address from larger subnet. +-- @return CIDR instance containing the network address +-- @see cidr.network +-- @see cidr.broadcast +-- @see cidr.mask +function cidr.host( self ) + return __bless({ self[1], self[2], __maxlen(self[1]) }) +end + +--- Return a corresponding CIDR representing the netmask of this instance. +-- @param bits Override prefix length of this instance (optional) +-- @return CIDR instance containing the netmask +-- @see cidr.network +-- @see cidr.host +-- @see cidr.broadcast +function cidr.mask( self, bits ) + local data = { } + bits = bits or self[3] + + for i = 1, math.floor( bits / 16 ) do + data[#data+1] = 0xFFFF + end + + if #data < #self[2] then + data[#data+1] = __mask16(bits) + + for i = #data + 1, #self[2] do + data[#data+1] = 0 + end + end + + return __bless({ self[1], data, __maxlen(self[1]) }) +end + +--- Return CIDR containing the broadcast address of this instance. +-- @return CIDR instance containing the netmask, always nil for IPv6 +-- @see cidr.network +-- @see cidr.host +-- @see cidr.mask +function cidr.broadcast( self ) + -- IPv6 has no broadcast addresses (XXX: assert() instead?) + if self[1] == FAMILY_INET4 then + local data = { unpack(self[2]) } + local offset = math.floor( self[3] / 16 ) + 1 + + if offset <= #data then + data[offset] = bit.bor( data[offset], __not16(self[3]) ) + for i = offset + 1, #data do data[i] = 0xFFFF end + + return __bless({ self[1], data, __maxlen(self[1]) }) + end + end +end + +--- Test whether this instance fully contains the given CIDR instance. +-- @param addr CIDR instance to test against +-- @return Boolean indicating whether this instance contains the given CIDR +function cidr.contains( self, addr ) + assert( self[1] == addr[1], "Can't compare IPv4 and IPv6 addresses" ) + + if self:prefix() <= addr:prefix() then + return self:network() == addr:network(self:prefix()) + end + + return false +end + +--- Add specified amount of hosts to this instance. +-- @param amount Number of hosts to add to this instance +-- @param inplace Boolen indicating whether to alter values inplace (optional) +-- @return CIDR representing the new address or nil on overflow error +-- @see cidr.sub +function cidr.add( self, amount, inplace ) + local pos + local data = { unpack(self[2]) } + local shorts = __array16( amount, self[1] ) + + for pos = #data, 1, -1 do + local add = ( #shorts > 0 ) and table.remove( shorts, #shorts ) or 0 + if ( data[pos] + add ) > 0xFFFF then + data[pos] = ( data[pos] + add ) % 0xFFFF + if pos > 1 then + data[pos-1] = data[pos-1] + ( add - data[pos] ) + else + return nil + end + else + data[pos] = data[pos] + add + end + end + + if inplace then + self[2] = data + return self + else + return __bless({ self[1], data, self[3] }) + end +end + +--- Substract specified amount of hosts from this instance. +-- @param amount Number of hosts to substract from this instance +-- @param inplace Boolen indicating whether to alter values inplace (optional) +-- @return CIDR representing the new address or nil on underflow error +-- @see cidr.add +function cidr.sub( self, amount, inplace ) + local pos + local data = { unpack(self[2]) } + local shorts = __array16( amount, self[1] ) + + for pos = #data, 1, -1 do + local sub = ( #shorts > 0 ) and table.remove( shorts, #shorts ) or 0 + if ( data[pos] - sub ) < 0 then + data[pos] = ( sub - data[pos] ) % 0xFFFF + if pos > 1 then + data[pos-1] = data[pos-1] - ( sub + data[pos] ) + else + return nil + end + else + data[pos] = data[pos] - sub + end + end + + if inplace then + self[2] = data + return self + else + return __bless({ self[1], data, self[3] }) + end +end + +--- Return CIDR containing the lowest available host address within this subnet. +-- @return CIDR containing the host address, nil if subnet is too small +-- @see cidr.maxhost +function cidr.minhost( self ) + if self[3] <= __sublen(self[1]) then + -- 1st is Network Address in IPv4 and Subnet-Router Anycast Adresse in IPv6 + return self:network():add(1, true) + end +end + +--- Return CIDR containing the highest available host address within the subnet. +-- @return CIDR containing the host address, nil if subnet is too small +-- @see cidr.minhost +function cidr.maxhost( self ) + if self[3] <= __sublen(self[1]) then + local i + local data = { unpack(self[2]) } + local offset = math.floor( self[3] / 16 ) + 1 + + data[offset] = bit.bor( data[offset], __not16(self[3]) ) + for i = offset + 1, #data do data[i] = 0xFFFF end + data = __bless({ self[1], data, __maxlen(self[1]) }) + + -- Last address in reserved for Broadcast Address in IPv4 + if data[1] == FAMILY_INET4 then data:sub(1, true) end + + return data + end +end diff --git a/modules/base/luasrc/ltn12.lua b/modules/base/luasrc/ltn12.lua new file mode 100644 index 0000000000..9371290c61 --- /dev/null +++ b/modules/base/luasrc/ltn12.lua @@ -0,0 +1,391 @@ +--[[ +LuaSocket 2.0.2 license +Copyright � 2004-2007 Diego Nehab + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +]]-- +--[[ + Changes made by LuCI project: + * Renamed to luci.ltn12 to avoid collisions with luasocket + * Added inline documentation +]]-- +----------------------------------------------------------------------------- +-- LTN12 - Filters, sources, sinks and pumps. +-- LuaSocket toolkit. +-- Author: Diego Nehab +-- RCS ID: $Id$ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module +----------------------------------------------------------------------------- +local string = require("string") +local table = require("table") +local base = _G + +--- Diego Nehab's LTN12 - Filters, sources, sinks and pumps. +-- See http://lua-users.org/wiki/FiltersSourcesAndSinks for design concepts +module("luci.ltn12") + +filter = {} +source = {} +sink = {} +pump = {} + +-- 2048 seems to be better in windows... +BLOCKSIZE = 2048 +_VERSION = "LTN12 1.0.1" + +----------------------------------------------------------------------------- +-- Filter stuff +----------------------------------------------------------------------------- + +--- LTN12 Filter constructors +-- @class module +-- @name luci.ltn12.filter + +--- Return a high level filter that cycles a low-level filter +-- by passing it each chunk and updating a context between calls. +-- @param low Low-level filter +-- @param ctx Context +-- @param extra Extra argument passed to the low-level filter +-- @return LTN12 filter +function filter.cycle(low, ctx, extra) + base.assert(low) + return function(chunk) + local ret + ret, ctx = low(ctx, chunk, extra) + return ret + end +end + +--- Chain a bunch of filters together. +-- (thanks to Wim Couwenberg) +-- @param ... filters to be chained +-- @return LTN12 filter +function filter.chain(...) + local n = table.getn(arg) + local top, index = 1, 1 + local retry = "" + return function(chunk) + retry = chunk and retry + while true do + if index == top then + chunk = arg[index](chunk) + if chunk == "" or top == n then return chunk + elseif chunk then index = index + 1 + else + top = top+1 + index = top + end + else + chunk = arg[index](chunk or "") + if chunk == "" then + index = index - 1 + chunk = retry + elseif chunk then + if index == n then return chunk + else index = index + 1 end + else base.error("filter returned inappropriate nil") end + end + end + end +end + +----------------------------------------------------------------------------- +-- Source stuff +----------------------------------------------------------------------------- + +--- LTN12 Source constructors +-- @class module +-- @name luci.ltn12.source + +-- create an empty source +local function empty() + return nil +end + +--- Create an empty source. +-- @return LTN12 source +function source.empty() + return empty +end + +--- Return a source that just outputs an error. +-- @param err Error object +-- @return LTN12 source +function source.error(err) + return function() + return nil, err + end +end + +--- Create a file source. +-- @param handle File handle ready for reading +-- @param io_err IO error object +-- @return LTN12 source +function source.file(handle, io_err) + if handle then + return function() + local chunk = handle:read(BLOCKSIZE) + if not chunk then handle:close() end + return chunk + end + else return source.error(io_err or "unable to open file") end +end + +--- Turn a fancy source into a simple source. +-- @param src fancy source +-- @return LTN12 source +function source.simplify(src) + base.assert(src) + return function() + local chunk, err_or_new = src() + src = err_or_new or src + if not chunk then return nil, err_or_new + else return chunk end + end +end + +--- Create a string source. +-- @param s Data +-- @return LTN12 source +function source.string(s) + if s then + local i = 1 + return function() + local chunk = string.sub(s, i, i+BLOCKSIZE-1) + i = i + BLOCKSIZE + if chunk ~= "" then return chunk + else return nil end + end + else return source.empty() end +end + +--- Creates rewindable source. +-- @param src LTN12 source to be made rewindable +-- @return LTN12 source +function source.rewind(src) + base.assert(src) + local t = {} + return function(chunk) + if not chunk then + chunk = table.remove(t) + if not chunk then return src() + else return chunk end + else + t[#t+1] = chunk + end + end +end + +--- Chain a source and a filter together. +-- @param src LTN12 source +-- @param f LTN12 filter +-- @return LTN12 source +function source.chain(src, f) + base.assert(src and f) + local last_in, last_out = "", "" + local state = "feeding" + local err + return function() + if not last_out then + base.error('source is empty!', 2) + end + while true do + if state == "feeding" then + last_in, err = src() + if err then return nil, err end + last_out = f(last_in) + if not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + elseif last_out ~= "" then + state = "eating" + if last_in then last_in = "" end + return last_out + end + else + last_out = f(last_in) + if last_out == "" then + if last_in == "" then + state = "feeding" + else + base.error('filter returned ""') + end + elseif not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + else + return last_out + end + end + end + end +end + +--- Create a source that produces contents of several sources. +-- Sources will be used one after the other, as if they were concatenated +-- (thanks to Wim Couwenberg) +-- @param ... LTN12 sources +-- @return LTN12 source +function source.cat(...) + local src = table.remove(arg, 1) + return function() + while src do + local chunk, err = src() + if chunk then return chunk end + if err then return nil, err end + src = table.remove(arg, 1) + end + end +end + +----------------------------------------------------------------------------- +-- Sink stuff +----------------------------------------------------------------------------- + +--- LTN12 sink constructors +-- @class module +-- @name luci.ltn12.sink + +--- Create a sink that stores into a table. +-- @param t output table to store into +-- @return LTN12 sink +function sink.table(t) + t = t or {} + local f = function(chunk, err) + if chunk then t[#t+1] = chunk end + return 1 + end + return f, t +end + +--- Turn a fancy sink into a simple sink. +-- @param snk fancy sink +-- @return LTN12 sink +function sink.simplify(snk) + base.assert(snk) + return function(chunk, err) + local ret, err_or_new = snk(chunk, err) + if not ret then return nil, err_or_new end + snk = err_or_new or snk + return 1 + end +end + +--- Create a file sink. +-- @param handle file handle to write to +-- @param io_err IO error +-- @return LTN12 sink +function sink.file(handle, io_err) + if handle then + return function(chunk, err) + if not chunk then + handle:close() + return 1 + else return handle:write(chunk) end + end + else return sink.error(io_err or "unable to open file") end +end + +-- creates a sink that discards data +local function null() + return 1 +end + +--- Create a sink that discards data. +-- @return LTN12 sink +function sink.null() + return null +end + +--- Create a sink that just returns an error. +-- @param err Error object +-- @return LTN12 sink +function sink.error(err) + return function() + return nil, err + end +end + +--- Chain a sink with a filter. +-- @param f LTN12 filter +-- @param snk LTN12 sink +-- @return LTN12 sink +function sink.chain(f, snk) + base.assert(f and snk) + return function(chunk, err) + if chunk ~= "" then + local filtered = f(chunk) + local done = chunk and "" + while true do + local ret, snkerr = snk(filtered, err) + if not ret then return nil, snkerr end + if filtered == done then return 1 end + filtered = f(done) + end + else return 1 end + end +end + +----------------------------------------------------------------------------- +-- Pump stuff +----------------------------------------------------------------------------- + +--- LTN12 pump functions +-- @class module +-- @name luci.ltn12.pump + +--- Pump one chunk from the source to the sink. +-- @param src LTN12 source +-- @param snk LTN12 sink +-- @return Chunk of data or nil if an error occured +-- @return Error object +function pump.step(src, snk) + local chunk, src_err = src() + local ret, snk_err = snk(chunk, src_err) + if chunk and ret then return 1 + else return nil, src_err or snk_err end +end + +--- Pump all data from a source to a sink, using a step function. +-- @param src LTN12 source +-- @param snk LTN12 sink +-- @param step step function (optional) +-- @return 1 if the operation succeeded otherwise nil +-- @return Error object +function pump.all(src, snk, step) + base.assert(src and snk) + step = step or pump.step + while true do + local ret, err = step(src, snk) + if not ret then + if err then return nil, err + else return 1 end + end + end +end + diff --git a/modules/base/luasrc/luasrc/cacheloader.lua b/modules/base/luasrc/luasrc/cacheloader.lua new file mode 100644 index 0000000000..942c4b7b48 --- /dev/null +++ b/modules/base/luasrc/luasrc/cacheloader.lua @@ -0,0 +1,23 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2008 Steven Barth <steven@midlink.org> +Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +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
\ No newline at end of file diff --git a/modules/base/luasrc/luasrc/cbi.lua b/modules/base/luasrc/luasrc/cbi.lua new file mode 100644 index 0000000000..ae570b1556 --- /dev/null +++ b/modules/base/luasrc/luasrc/cbi.lua @@ -0,0 +1,1850 @@ +--[[ +LuCI - Configuration Bind Interface + +Description: +Offers an interface for binding configuration values to certain +data types. Supports value and range validation and basic dependencies. + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- +module("luci.cbi", package.seeall) + +require("luci.template") +local util = require("luci.util") +require("luci.http") + + +--local event = require "luci.sys.event" +local fs = require("nixio.fs") +local uci = require("luci.model.uci") +local datatypes = require("luci.cbi.datatypes") +local class = util.class +local instanceof = util.instanceof + +FORM_NODATA = 0 +FORM_PROCEED = 0 +FORM_VALID = 1 +FORM_DONE = 1 +FORM_INVALID = -1 +FORM_CHANGED = 2 +FORM_SKIP = 4 + +AUTO = true + +CREATE_PREFIX = "cbi.cts." +REMOVE_PREFIX = "cbi.rts." +RESORT_PREFIX = "cbi.sts." +FEXIST_PREFIX = "cbi.cbe." + +-- Loads a CBI map from given file, creating an environment and returns it +function load(cbimap, ...) + local fs = require "nixio.fs" + local i18n = require "luci.i18n" + require("luci.config") + require("luci.util") + + local upldir = "/lib/uci/upload/" + local cbidir = luci.util.libpath() .. "/model/cbi/" + local func, err + + if fs.access(cbidir..cbimap..".lua") then + func, err = loadfile(cbidir..cbimap..".lua") + elseif fs.access(cbimap) then + func, err = loadfile(cbimap) + else + func, err = nil, "Model '" .. cbimap .. "' not found!" + end + + assert(func, err) + + local env = { + translate=i18n.translate, + translatef=i18n.translatef, + arg={...} + } + + setfenv(func, setmetatable(env, {__index = + function(tbl, key) + return rawget(tbl, key) or _M[key] or _G[key] + end})) + + local maps = { func() } + local uploads = { } + local has_upload = false + + for i, map in ipairs(maps) do + if not instanceof(map, Node) then + error("CBI map returns no valid map object!") + return nil + else + map:prepare() + if map.upload_fields then + has_upload = true + for _, field in ipairs(map.upload_fields) do + uploads[ + field.config .. '.' .. + (field.section.sectiontype or '1') .. '.' .. + field.option + ] = true + end + end + end + end + + if has_upload then + local uci = luci.model.uci.cursor() + local prm = luci.http.context.request.message.params + local fd, cbid + + luci.http.setfilehandler( + function( field, chunk, eof ) + if not field then return end + if field.name and not cbid then + local c, s, o = field.name:gmatch( + "cbid%.([^%.]+)%.([^%.]+)%.([^%.]+)" + )() + + if c and s and o then + local t = uci:get( c, s ) or s + if uploads[c.."."..t.."."..o] then + local path = upldir .. field.name + fd = io.open(path, "w") + if fd then + cbid = field.name + prm[cbid] = path + end + end + end + end + + if field.name == cbid and fd then + fd:write(chunk) + end + + if eof and fd then + fd:close() + fd = nil + cbid = nil + end + end + ) + end + + return maps +end + +-- +-- Compile a datatype specification into a parse tree for evaluation later on +-- +local cdt_cache = { } + +function compile_datatype(code) + local i + local pos = 0 + local esc = false + local depth = 0 + local stack = { } + + for i = 1, #code+1 do + local byte = code:byte(i) or 44 + if esc then + esc = false + elseif byte == 92 then + esc = true + elseif byte == 40 or byte == 44 then + if depth <= 0 then + if pos < i then + local label = code:sub(pos, i-1) + :gsub("\\(.)", "%1") + :gsub("^%s+", "") + :gsub("%s+$", "") + + if #label > 0 and tonumber(label) then + stack[#stack+1] = tonumber(label) + elseif label:match("^'.*'$") or label:match('^".*"$') then + stack[#stack+1] = label:gsub("[\"'](.*)[\"']", "%1") + elseif type(datatypes[label]) == "function" then + stack[#stack+1] = datatypes[label] + stack[#stack+1] = { } + else + error("Datatype error, bad token %q" % label) + end + end + pos = i + 1 + end + depth = depth + (byte == 40 and 1 or 0) + elseif byte == 41 then + depth = depth - 1 + if depth <= 0 then + if type(stack[#stack-1]) ~= "function" then + error("Datatype error, argument list follows non-function") + end + stack[#stack] = compile_datatype(code:sub(pos, i-1)) + pos = i + 1 + end + end + end + + return stack +end + +function verify_datatype(dt, value) + if dt and #dt > 0 then + if not cdt_cache[dt] then + local c = compile_datatype(dt) + if c and type(c[1]) == "function" then + cdt_cache[dt] = c + else + error("Datatype error, not a function expression") + end + end + if cdt_cache[dt] then + return cdt_cache[dt][1](value, unpack(cdt_cache[dt][2])) + end + end + return true +end + + +-- Node pseudo abstract class +Node = class() + +function Node.__init__(self, title, description) + self.children = {} + self.title = title or "" + self.description = description or "" + self.template = "cbi/node" +end + +-- hook helper +function Node._run_hook(self, hook) + if type(self[hook]) == "function" then + return self[hook](self) + end +end + +function Node._run_hooks(self, ...) + local f + local r = false + for _, f in ipairs(arg) do + if type(self[f]) == "function" then + self[f](self) + r = true + end + end + return r +end + +-- Prepare nodes +function Node.prepare(self, ...) + for k, child in ipairs(self.children) do + child:prepare(...) + end +end + +-- Append child nodes +function Node.append(self, obj) + table.insert(self.children, obj) +end + +-- Parse this node and its children +function Node.parse(self, ...) + for k, child in ipairs(self.children) do + child:parse(...) + end +end + +-- Render this node +function Node.render(self, scope) + scope = scope or {} + scope.self = self + + luci.template.render(self.template, scope) +end + +-- Render the children +function Node.render_children(self, ...) + local k, node + for k, node in ipairs(self.children) do + node.last_child = (k == #self.children) + node:render(...) + end +end + + +--[[ +A simple template element +]]-- +Template = class(Node) + +function Template.__init__(self, template) + Node.__init__(self) + self.template = template +end + +function Template.render(self) + luci.template.render(self.template, {self=self}) +end + +function Template.parse(self, readinput) + self.readinput = (readinput ~= false) + return Map.formvalue(self, "cbi.submit") and FORM_DONE or FORM_NODATA +end + + +--[[ +Map - A map describing a configuration file +]]-- +Map = class(Node) + +function Map.__init__(self, config, ...) + Node.__init__(self, ...) + + self.config = config + self.parsechain = {self.config} + self.template = "cbi/map" + self.apply_on_parse = nil + self.readinput = true + self.proceed = false + self.flow = {} + + self.uci = uci.cursor() + self.save = true + + self.changed = false + + if not self.uci:load(self.config) then + error("Unable to read UCI data: " .. self.config) + end +end + +function Map.formvalue(self, key) + return self.readinput and luci.http.formvalue(key) +end + +function Map.formvaluetable(self, key) + return self.readinput and luci.http.formvaluetable(key) or {} +end + +function Map.get_scheme(self, sectiontype, option) + if not option then + return self.scheme and self.scheme.sections[sectiontype] + else + return self.scheme and self.scheme.variables[sectiontype] + and self.scheme.variables[sectiontype][option] + end +end + +function Map.submitstate(self) + return self:formvalue("cbi.submit") +end + +-- Chain foreign config +function Map.chain(self, config) + table.insert(self.parsechain, config) +end + +function Map.state_handler(self, state) + return state +end + +-- Use optimized UCI writing +function Map.parse(self, readinput, ...) + self.readinput = (readinput ~= false) + self:_run_hooks("on_parse") + + if self:formvalue("cbi.skip") then + self.state = FORM_SKIP + return self:state_handler(self.state) + end + + Node.parse(self, ...) + + if self.save then + self:_run_hooks("on_save", "on_before_save") + for i, config in ipairs(self.parsechain) do + self.uci:save(config) + end + self:_run_hooks("on_after_save") + if self:submitstate() and ((not self.proceed and self.flow.autoapply) or luci.http.formvalue("cbi.apply")) then + self:_run_hooks("on_before_commit") + for i, config in ipairs(self.parsechain) do + self.uci:commit(config) + + -- Refresh data because commit changes section names + self.uci:load(config) + end + self:_run_hooks("on_commit", "on_after_commit", "on_before_apply") + if self.apply_on_parse then + self.uci:apply(self.parsechain) + self:_run_hooks("on_apply", "on_after_apply") + else + -- This is evaluated by the dispatcher and delegated to the + -- template which in turn fires XHR to perform the actual + -- apply actions. + self.apply_needed = true + end + + -- Reparse sections + Node.parse(self, true) + + end + for i, config in ipairs(self.parsechain) do + self.uci:unload(config) + end + if type(self.commit_handler) == "function" then + self:commit_handler(self:submitstate()) + end + end + + if self:submitstate() then + if not self.save then + self.state = FORM_INVALID + elseif self.proceed then + self.state = FORM_PROCEED + else + self.state = self.changed and FORM_CHANGED or FORM_VALID + end + else + self.state = FORM_NODATA + end + + return self:state_handler(self.state) +end + +function Map.render(self, ...) + self:_run_hooks("on_init") + Node.render(self, ...) +end + +-- Creates a child section +function Map.section(self, class, ...) + if instanceof(class, AbstractSection) then + local obj = class(self, ...) + self:append(obj) + return obj + else + error("class must be a descendent of AbstractSection") + end +end + +-- UCI add +function Map.add(self, sectiontype) + return self.uci:add(self.config, sectiontype) +end + +-- UCI set +function Map.set(self, section, option, value) + if type(value) ~= "table" or #value > 0 then + if option then + return self.uci:set(self.config, section, option, value) + else + return self.uci:set(self.config, section, value) + end + else + return Map.del(self, section, option) + end +end + +-- UCI del +function Map.del(self, section, option) + if option then + return self.uci:delete(self.config, section, option) + else + return self.uci:delete(self.config, section) + end +end + +-- UCI get +function Map.get(self, section, option) + if not section then + return self.uci:get_all(self.config) + elseif option then + return self.uci:get(self.config, section, option) + else + return self.uci:get_all(self.config, section) + end +end + +--[[ +Compound - Container +]]-- +Compound = class(Node) + +function Compound.__init__(self, ...) + Node.__init__(self) + self.template = "cbi/compound" + self.children = {...} +end + +function Compound.populate_delegator(self, delegator) + for _, v in ipairs(self.children) do + v.delegator = delegator + end +end + +function Compound.parse(self, ...) + local cstate, state = 0 + + for k, child in ipairs(self.children) do + cstate = child:parse(...) + state = (not state or cstate < state) and cstate or state + end + + return state +end + + +--[[ +Delegator - Node controller +]]-- +Delegator = class(Node) +function Delegator.__init__(self, ...) + Node.__init__(self, ...) + self.nodes = {} + self.defaultpath = {} + self.pageaction = false + self.readinput = true + self.allow_reset = false + self.allow_cancel = false + self.allow_back = false + self.allow_finish = false + self.template = "cbi/delegator" +end + +function Delegator.set(self, name, node) + assert(not self.nodes[name], "Duplicate entry") + + self.nodes[name] = node +end + +function Delegator.add(self, name, node) + node = self:set(name, node) + self.defaultpath[#self.defaultpath+1] = name +end + +function Delegator.insert_after(self, name, after) + local n = #self.chain + 1 + for k, v in ipairs(self.chain) do + if v == after then + n = k + 1 + break + end + end + table.insert(self.chain, n, name) +end + +function Delegator.set_route(self, ...) + local n, chain, route = 0, self.chain, {...} + for i = 1, #chain do + if chain[i] == self.current then + n = i + break + end + end + for i = 1, #route do + n = n + 1 + chain[n] = route[i] + end + for i = n + 1, #chain do + chain[i] = nil + end +end + +function Delegator.get(self, name) + local node = self.nodes[name] + + if type(node) == "string" then + node = load(node, name) + end + + if type(node) == "table" and getmetatable(node) == nil then + node = Compound(unpack(node)) + end + + return node +end + +function Delegator.parse(self, ...) + if self.allow_cancel and Map.formvalue(self, "cbi.cancel") then + if self:_run_hooks("on_cancel") then + return FORM_DONE + end + end + + if not Map.formvalue(self, "cbi.delg.current") then + self:_run_hooks("on_init") + end + + local newcurrent + self.chain = self.chain or self:get_chain() + self.current = self.current or self:get_active() + self.active = self.active or self:get(self.current) + assert(self.active, "Invalid state") + + local stat = FORM_DONE + if type(self.active) ~= "function" then + self.active:populate_delegator(self) + stat = self.active:parse() + else + self:active() + end + + if stat > FORM_PROCEED then + if Map.formvalue(self, "cbi.delg.back") then + newcurrent = self:get_prev(self.current) + else + newcurrent = self:get_next(self.current) + end + elseif stat < FORM_PROCEED then + return stat + end + + + if not Map.formvalue(self, "cbi.submit") then + return FORM_NODATA + elseif stat > FORM_PROCEED + and (not newcurrent or not self:get(newcurrent)) then + return self:_run_hook("on_done") or FORM_DONE + else + self.current = newcurrent or self.current + self.active = self:get(self.current) + if type(self.active) ~= "function" then + self.active:populate_delegator(self) + local stat = self.active:parse(false) + if stat == FORM_SKIP then + return self:parse(...) + else + return FORM_PROCEED + end + else + return self:parse(...) + end + end +end + +function Delegator.get_next(self, state) + for k, v in ipairs(self.chain) do + if v == state then + return self.chain[k+1] + end + end +end + +function Delegator.get_prev(self, state) + for k, v in ipairs(self.chain) do + if v == state then + return self.chain[k-1] + end + end +end + +function Delegator.get_chain(self) + local x = Map.formvalue(self, "cbi.delg.path") or self.defaultpath + return type(x) == "table" and x or {x} +end + +function Delegator.get_active(self) + return Map.formvalue(self, "cbi.delg.current") or self.chain[1] +end + +--[[ +Page - A simple node +]]-- + +Page = class(Node) +Page.__init__ = Node.__init__ +Page.parse = function() end + + +--[[ +SimpleForm - A Simple non-UCI form +]]-- +SimpleForm = class(Node) + +function SimpleForm.__init__(self, config, title, description, data) + Node.__init__(self, title, description) + self.config = config + self.data = data or {} + self.template = "cbi/simpleform" + self.dorender = true + self.pageaction = false + self.readinput = true +end + +SimpleForm.formvalue = Map.formvalue +SimpleForm.formvaluetable = Map.formvaluetable + +function SimpleForm.parse(self, readinput, ...) + self.readinput = (readinput ~= false) + + if self:formvalue("cbi.skip") then + return FORM_SKIP + end + + if self:formvalue("cbi.cancel") and self:_run_hooks("on_cancel") then + return FORM_DONE + end + + if self:submitstate() then + Node.parse(self, 1, ...) + end + + local valid = true + for k, j in ipairs(self.children) do + for i, v in ipairs(j.children) do + valid = valid + and (not v.tag_missing or not v.tag_missing[1]) + and (not v.tag_invalid or not v.tag_invalid[1]) + and (not v.error) + end + end + + local state = + not self:submitstate() and FORM_NODATA + or valid and FORM_VALID + or FORM_INVALID + + self.dorender = not self.handle + if self.handle then + local nrender, nstate = self:handle(state, self.data) + self.dorender = self.dorender or (nrender ~= false) + state = nstate or state + end + return state +end + +function SimpleForm.render(self, ...) + if self.dorender then + Node.render(self, ...) + end +end + +function SimpleForm.submitstate(self) + return self:formvalue("cbi.submit") +end + +function SimpleForm.section(self, class, ...) + if instanceof(class, AbstractSection) then + local obj = class(self, ...) + self:append(obj) + return obj + else + error("class must be a descendent of AbstractSection") + end +end + +-- Creates a child field +function SimpleForm.field(self, class, ...) + local section + for k, v in ipairs(self.children) do + if instanceof(v, SimpleSection) then + section = v + break + end + end + if not section then + section = self:section(SimpleSection) + end + + if instanceof(class, AbstractValue) then + local obj = class(self, section, ...) + obj.track_missing = true + section:append(obj) + return obj + else + error("class must be a descendent of AbstractValue") + end +end + +function SimpleForm.set(self, section, option, value) + self.data[option] = value +end + + +function SimpleForm.del(self, section, option) + self.data[option] = nil +end + + +function SimpleForm.get(self, section, option) + return self.data[option] +end + + +function SimpleForm.get_scheme() + return nil +end + + +Form = class(SimpleForm) + +function Form.__init__(self, ...) + SimpleForm.__init__(self, ...) + self.embedded = true +end + + +--[[ +AbstractSection +]]-- +AbstractSection = class(Node) + +function AbstractSection.__init__(self, map, sectiontype, ...) + Node.__init__(self, ...) + self.sectiontype = sectiontype + self.map = map + self.config = map.config + self.optionals = {} + self.defaults = {} + self.fields = {} + self.tag_error = {} + self.tag_invalid = {} + self.tag_deperror = {} + self.changed = false + + self.optional = true + self.addremove = false + self.dynamic = false +end + +-- Define a tab for the section +function AbstractSection.tab(self, tab, title, desc) + self.tabs = self.tabs or { } + self.tab_names = self.tab_names or { } + + self.tab_names[#self.tab_names+1] = tab + self.tabs[tab] = { + title = title, + description = desc, + childs = { } + } +end + +-- Check whether the section has tabs +function AbstractSection.has_tabs(self) + return (self.tabs ~= nil) and (next(self.tabs) ~= nil) +end + +-- Appends a new option +function AbstractSection.option(self, class, option, ...) + if instanceof(class, AbstractValue) then + local obj = class(self.map, self, option, ...) + self:append(obj) + self.fields[option] = obj + return obj + elseif class == true then + error("No valid class was given and autodetection failed.") + else + error("class must be a descendant of AbstractValue") + end +end + +-- Appends a new tabbed option +function AbstractSection.taboption(self, tab, ...) + + assert(tab and self.tabs and self.tabs[tab], + "Cannot assign option to not existing tab %q" % tostring(tab)) + + local l = self.tabs[tab].childs + local o = AbstractSection.option(self, ...) + + if o then l[#l+1] = o end + + return o +end + +-- Render a single tab +function AbstractSection.render_tab(self, tab, ...) + + assert(tab and self.tabs and self.tabs[tab], + "Cannot render not existing tab %q" % tostring(tab)) + + local k, node + for k, node in ipairs(self.tabs[tab].childs) do + node.last_child = (k == #self.tabs[tab].childs) + node:render(...) + end +end + +-- Parse optional options +function AbstractSection.parse_optionals(self, section) + if not self.optional then + return + end + + self.optionals[section] = {} + + local field = self.map:formvalue("cbi.opt."..self.config.."."..section) + for k,v in ipairs(self.children) do + if v.optional and not v:cfgvalue(section) and not self:has_tabs() then + if field == v.option then + field = nil + self.map.proceed = true + else + table.insert(self.optionals[section], v) + end + end + end + + if field and #field > 0 and self.dynamic then + self:add_dynamic(field) + end +end + +-- Add a dynamic option +function AbstractSection.add_dynamic(self, field, optional) + local o = self:option(Value, field, field) + o.optional = optional +end + +-- Parse all dynamic options +function AbstractSection.parse_dynamic(self, section) + if not self.dynamic then + return + end + + local arr = luci.util.clone(self:cfgvalue(section)) + local form = self.map:formvaluetable("cbid."..self.config.."."..section) + for k, v in pairs(form) do + arr[k] = v + end + + for key,val in pairs(arr) do + local create = true + + for i,c in ipairs(self.children) do + if c.option == key then + create = false + end + end + + if create and key:sub(1, 1) ~= "." then + self.map.proceed = true + self:add_dynamic(key, true) + end + end +end + +-- Returns the section's UCI table +function AbstractSection.cfgvalue(self, section) + return self.map:get(section) +end + +-- Push events +function AbstractSection.push_events(self) + --luci.util.append(self.map.events, self.events) + self.map.changed = true +end + +-- Removes the section +function AbstractSection.remove(self, section) + self.map.proceed = true + return self.map:del(section) +end + +-- Creates the section +function AbstractSection.create(self, section) + local stat + + if section then + stat = section:match("^[%w_]+$") and self.map:set(section, nil, self.sectiontype) + else + section = self.map:add(self.sectiontype) + stat = section + end + + if stat then + for k,v in pairs(self.children) do + if v.default then + self.map:set(section, v.option, v.default) + end + end + + for k,v in pairs(self.defaults) do + self.map:set(section, k, v) + end + end + + self.map.proceed = true + + return stat +end + + +SimpleSection = class(AbstractSection) + +function SimpleSection.__init__(self, form, ...) + AbstractSection.__init__(self, form, nil, ...) + self.template = "cbi/nullsection" +end + + +Table = class(AbstractSection) + +function Table.__init__(self, form, data, ...) + local datasource = {} + local tself = self + datasource.config = "table" + self.data = data or {} + + datasource.formvalue = Map.formvalue + datasource.formvaluetable = Map.formvaluetable + datasource.readinput = true + + function datasource.get(self, section, option) + return tself.data[section] and tself.data[section][option] + end + + function datasource.submitstate(self) + return Map.formvalue(self, "cbi.submit") + end + + function datasource.del(...) + return true + end + + function datasource.get_scheme() + return nil + end + + AbstractSection.__init__(self, datasource, "table", ...) + self.template = "cbi/tblsection" + self.rowcolors = true + self.anonymous = true +end + +function Table.parse(self, readinput) + self.map.readinput = (readinput ~= false) + for i, k in ipairs(self:cfgsections()) do + if self.map:submitstate() then + Node.parse(self, k) + end + end +end + +function Table.cfgsections(self) + local sections = {} + + for i, v in luci.util.kspairs(self.data) do + table.insert(sections, i) + end + + return sections +end + +function Table.update(self, data) + self.data = data +end + + + +--[[ +NamedSection - A fixed configuration section defined by its name +]]-- +NamedSection = class(AbstractSection) + +function NamedSection.__init__(self, map, section, stype, ...) + AbstractSection.__init__(self, map, stype, ...) + + -- Defaults + self.addremove = false + self.template = "cbi/nsection" + self.section = section +end + +function NamedSection.parse(self, novld) + local s = self.section + local active = self:cfgvalue(s) + + if self.addremove then + local path = self.config.."."..s + if active then -- Remove the section + if self.map:formvalue("cbi.rns."..path) and self:remove(s) then + self:push_events() + return + end + else -- Create and apply default values + if self.map:formvalue("cbi.cns."..path) then + self:create(s) + return + end + end + end + + if active then + AbstractSection.parse_dynamic(self, s) + if self.map:submitstate() then + Node.parse(self, s) + end + AbstractSection.parse_optionals(self, s) + + if self.changed then + self:push_events() + end + end +end + + +--[[ +TypedSection - A (set of) configuration section(s) defined by the type + addremove: Defines whether the user can add/remove sections of this type + anonymous: Allow creating anonymous sections + validate: a validation function returning nil if the section is invalid +]]-- +TypedSection = class(AbstractSection) + +function TypedSection.__init__(self, map, type, ...) + AbstractSection.__init__(self, map, type, ...) + + self.template = "cbi/tsection" + self.deps = {} + self.anonymous = false +end + +-- Return all matching UCI sections for this TypedSection +function TypedSection.cfgsections(self) + local sections = {} + self.map.uci:foreach(self.map.config, self.sectiontype, + function (section) + if self:checkscope(section[".name"]) then + table.insert(sections, section[".name"]) + end + end) + + return sections +end + +-- Limits scope to sections that have certain option => value pairs +function TypedSection.depends(self, option, value) + table.insert(self.deps, {option=option, value=value}) +end + +function TypedSection.parse(self, novld) + if self.addremove then + -- Remove + local crval = REMOVE_PREFIX .. self.config + local name = self.map:formvaluetable(crval) + for k,v in pairs(name) do + if k:sub(-2) == ".x" then + k = k:sub(1, #k - 2) + end + if self:cfgvalue(k) and self:checkscope(k) then + self:remove(k) + end + end + end + + local co + for i, k in ipairs(self:cfgsections()) do + AbstractSection.parse_dynamic(self, k) + if self.map:submitstate() then + Node.parse(self, k, novld) + end + AbstractSection.parse_optionals(self, k) + end + + if self.addremove then + -- Create + local created + local crval = CREATE_PREFIX .. self.config .. "." .. self.sectiontype + local origin, name = next(self.map:formvaluetable(crval)) + if self.anonymous then + if name then + created = self:create(nil, origin) + end + else + if name then + -- Ignore if it already exists + if self:cfgvalue(name) then + name = nil; + end + + name = self:checkscope(name) + + if not name then + self.err_invalid = true + end + + if name and #name > 0 then + created = self:create(name, origin) and name + if not created then + self.invalid_cts = true + end + end + end + end + + if created then + AbstractSection.parse_optionals(self, created) + end + end + + if self.sortable then + local stval = RESORT_PREFIX .. self.config .. "." .. self.sectiontype + local order = self.map:formvalue(stval) + if order and #order > 0 then + local sid + local num = 0 + for sid in util.imatch(order) do + self.map.uci:reorder(self.config, sid, num) + num = num + 1 + end + self.changed = (num > 0) + end + end + + if created or self.changed then + self:push_events() + end +end + +-- Verifies scope of sections +function TypedSection.checkscope(self, section) + -- Check if we are not excluded + if self.filter and not self:filter(section) then + return nil + end + + -- Check if at least one dependency is met + if #self.deps > 0 and self:cfgvalue(section) then + local stat = false + + for k, v in ipairs(self.deps) do + if self:cfgvalue(section)[v.option] == v.value then + stat = true + end + end + + if not stat then + return nil + end + end + + return self:validate(section) +end + + +-- Dummy validate function +function TypedSection.validate(self, section) + return section +end + + +--[[ +AbstractValue - An abstract Value Type + null: Value can be empty + valid: A function returning the value if it is valid otherwise nil + depends: A table of option => value pairs of which one must be true + default: The default value + size: The size of the input fields + rmempty: Unset value if empty + optional: This value is optional (see AbstractSection.optionals) +]]-- +AbstractValue = class(Node) + +function AbstractValue.__init__(self, map, section, option, ...) + Node.__init__(self, ...) + self.section = section + self.option = option + self.map = map + self.config = map.config + self.tag_invalid = {} + self.tag_missing = {} + self.tag_reqerror = {} + self.tag_error = {} + self.deps = {} + self.subdeps = {} + --self.cast = "string" + + self.track_missing = false + self.rmempty = true + self.default = nil + self.size = nil + self.optional = false +end + +function AbstractValue.prepare(self) + self.cast = self.cast or "string" +end + +-- Add a dependencie to another section field +function AbstractValue.depends(self, field, value) + local deps + if type(field) == "string" then + deps = {} + deps[field] = value + else + deps = field + end + + table.insert(self.deps, {deps=deps, add=""}) +end + +-- Generates the unique CBID +function AbstractValue.cbid(self, section) + return "cbid."..self.map.config.."."..section.."."..self.option +end + +-- Return whether this object should be created +function AbstractValue.formcreated(self, section) + local key = "cbi.opt."..self.config.."."..section + return (self.map:formvalue(key) == self.option) +end + +-- Returns the formvalue for this object +function AbstractValue.formvalue(self, section) + return self.map:formvalue(self:cbid(section)) +end + +function AbstractValue.additional(self, value) + self.optional = value +end + +function AbstractValue.mandatory(self, value) + self.rmempty = not value +end + +function AbstractValue.add_error(self, section, type, msg) + self.error = self.error or { } + self.error[section] = msg or type + + self.section.error = self.section.error or { } + self.section.error[section] = self.section.error[section] or { } + table.insert(self.section.error[section], msg or type) + + if type == "invalid" then + self.tag_invalid[section] = true + elseif type == "missing" then + self.tag_missing[section] = true + end + + self.tag_error[section] = true + self.map.save = false +end + +function AbstractValue.parse(self, section, novld) + local fvalue = self:formvalue(section) + local cvalue = self:cfgvalue(section) + + -- If favlue and cvalue are both tables and have the same content + -- make them identical + if type(fvalue) == "table" and type(cvalue) == "table" then + local equal = #fvalue == #cvalue + if equal then + for i=1, #fvalue do + if cvalue[i] ~= fvalue[i] then + equal = false + end + end + end + if equal then + fvalue = cvalue + end + end + + if fvalue and #fvalue > 0 then -- If we have a form value, write it to UCI + local val_err + fvalue, val_err = self:validate(fvalue, section) + fvalue = self:transform(fvalue) + + if not fvalue and not novld then + self:add_error(section, "invalid", val_err) + end + + if fvalue and (self.forcewrite or not (fvalue == cvalue)) then + if self:write(section, fvalue) then + -- Push events + self.section.changed = true + --luci.util.append(self.map.events, self.events) + end + end + else -- Unset the UCI or error + if self.rmempty or self.optional then + if self:remove(section) then + -- Push events + self.section.changed = true + --luci.util.append(self.map.events, self.events) + end + elseif cvalue ~= fvalue and not novld then + -- trigger validator with nil value to get custom user error msg. + local _, val_err = self:validate(nil, section) + self:add_error(section, "missing", val_err) + end + end +end + +-- Render if this value exists or if it is mandatory +function AbstractValue.render(self, s, scope) + if not self.optional or self.section:has_tabs() or self:cfgvalue(s) or self:formcreated(s) then + scope = scope or {} + scope.section = s + scope.cbid = self:cbid(s) + Node.render(self, scope) + end +end + +-- Return the UCI value of this object +function AbstractValue.cfgvalue(self, section) + local value + if self.tag_error[section] then + value = self:formvalue(section) + else + value = self.map:get(section, self.option) + end + + if not value then + return nil + elseif not self.cast or self.cast == type(value) then + return value + elseif self.cast == "string" then + if type(value) == "table" then + return value[1] + end + elseif self.cast == "table" then + return { value } + end +end + +-- Validate the form value +function AbstractValue.validate(self, value) + if self.datatype and value then + if type(value) == "table" then + local v + for _, v in ipairs(value) do + if v and #v > 0 and not verify_datatype(self.datatype, v) then + return nil + end + end + else + if not verify_datatype(self.datatype, value) then + return nil + end + end + end + + return value +end + +AbstractValue.transform = AbstractValue.validate + + +-- Write to UCI +function AbstractValue.write(self, section, value) + return self.map:set(section, self.option, value) +end + +-- Remove from UCI +function AbstractValue.remove(self, section) + return self.map:del(section, self.option) +end + + + + +--[[ +Value - A one-line value + maxlength: The maximum length +]]-- +Value = class(AbstractValue) + +function Value.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/value" + self.keylist = {} + self.vallist = {} +end + +function Value.reset_values(self) + self.keylist = {} + self.vallist = {} +end + +function Value.value(self, key, val) + val = val or key + table.insert(self.keylist, tostring(key)) + table.insert(self.vallist, tostring(val)) +end + + +-- DummyValue - This does nothing except being there +DummyValue = class(AbstractValue) + +function DummyValue.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/dvalue" + self.value = nil +end + +function DummyValue.cfgvalue(self, section) + local value + if self.value then + if type(self.value) == "function" then + value = self:value(section) + else + value = self.value + end + else + value = AbstractValue.cfgvalue(self, section) + end + return value +end + +function DummyValue.parse(self) + +end + + +--[[ +Flag - A flag being enabled or disabled +]]-- +Flag = class(AbstractValue) + +function Flag.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/fvalue" + + self.enabled = "1" + self.disabled = "0" + self.default = self.disabled +end + +-- A flag can only have two states: set or unset +function Flag.parse(self, section) + local fexists = self.map:formvalue( + FEXIST_PREFIX .. self.config .. "." .. section .. "." .. self.option) + + if fexists then + local fvalue = self:formvalue(section) and self.enabled or self.disabled + if fvalue ~= self.default or (not self.optional and not self.rmempty) then + self:write(section, fvalue) + else + self:remove(section) + end + else + self:remove(section) + end +end + +function Flag.cfgvalue(self, section) + return AbstractValue.cfgvalue(self, section) or self.default +end + + +--[[ +ListValue - A one-line value predefined in a list + widget: The widget that will be used (select, radio) +]]-- +ListValue = class(AbstractValue) + +function ListValue.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/lvalue" + + self.keylist = {} + self.vallist = {} + self.size = 1 + self.widget = "select" +end + +function ListValue.reset_values(self) + self.keylist = {} + self.vallist = {} +end + +function ListValue.value(self, key, val, ...) + if luci.util.contains(self.keylist, key) then + return + end + + val = val or key + table.insert(self.keylist, tostring(key)) + table.insert(self.vallist, tostring(val)) + + for i, deps in ipairs({...}) do + self.subdeps[#self.subdeps + 1] = {add = "-"..key, deps=deps} + end +end + +function ListValue.validate(self, val) + if luci.util.contains(self.keylist, val) then + return val + else + return nil + end +end + + + +--[[ +MultiValue - Multiple delimited values + widget: The widget that will be used (select, checkbox) + delimiter: The delimiter that will separate the values (default: " ") +]]-- +MultiValue = class(AbstractValue) + +function MultiValue.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/mvalue" + + self.keylist = {} + self.vallist = {} + + self.widget = "checkbox" + self.delimiter = " " +end + +function MultiValue.render(self, ...) + if self.widget == "select" and not self.size then + self.size = #self.vallist + end + + AbstractValue.render(self, ...) +end + +function MultiValue.reset_values(self) + self.keylist = {} + self.vallist = {} +end + +function MultiValue.value(self, key, val) + if luci.util.contains(self.keylist, key) then + return + end + + val = val or key + table.insert(self.keylist, tostring(key)) + table.insert(self.vallist, tostring(val)) +end + +function MultiValue.valuelist(self, section) + local val = self:cfgvalue(section) + + if not(type(val) == "string") then + return {} + end + + return luci.util.split(val, self.delimiter) +end + +function MultiValue.validate(self, val) + val = (type(val) == "table") and val or {val} + + local result + + for i, value in ipairs(val) do + if luci.util.contains(self.keylist, value) then + result = result and (result .. self.delimiter .. value) or value + end + end + + return result +end + + +StaticList = class(MultiValue) + +function StaticList.__init__(self, ...) + MultiValue.__init__(self, ...) + self.cast = "table" + self.valuelist = self.cfgvalue + + if not self.override_scheme + and self.map:get_scheme(self.section.sectiontype, self.option) then + local vs = self.map:get_scheme(self.section.sectiontype, self.option) + if self.value and vs.values and not self.override_values then + for k, v in pairs(vs.values) do + self:value(k, v) + end + end + end +end + +function StaticList.validate(self, value) + value = (type(value) == "table") and value or {value} + + local valid = {} + for i, v in ipairs(value) do + if luci.util.contains(self.keylist, v) then + table.insert(valid, v) + end + end + return valid +end + + +DynamicList = class(AbstractValue) + +function DynamicList.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/dynlist" + self.cast = "table" + self.keylist = {} + self.vallist = {} +end + +function DynamicList.reset_values(self) + self.keylist = {} + self.vallist = {} +end + +function DynamicList.value(self, key, val) + val = val or key + table.insert(self.keylist, tostring(key)) + table.insert(self.vallist, tostring(val)) +end + +function DynamicList.write(self, section, value) + local t = { } + + if type(value) == "table" then + local x + for _, x in ipairs(value) do + if x and #x > 0 then + t[#t+1] = x + end + end + else + t = { value } + end + + if self.cast == "string" then + value = table.concat(t, " ") + else + value = t + end + + return AbstractValue.write(self, section, value) +end + +function DynamicList.cfgvalue(self, section) + local value = AbstractValue.cfgvalue(self, section) + + if type(value) == "string" then + local x + local t = { } + for x in value:gmatch("%S+") do + if #x > 0 then + t[#t+1] = x + end + end + value = t + end + + return value +end + +function DynamicList.formvalue(self, section) + local value = AbstractValue.formvalue(self, section) + + if type(value) == "string" then + if self.cast == "string" then + local x + local t = { } + for x in value:gmatch("%S+") do + t[#t+1] = x + end + value = t + else + value = { value } + end + end + + return value +end + + +--[[ +TextValue - A multi-line value + rows: Rows +]]-- +TextValue = class(AbstractValue) + +function TextValue.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/tvalue" +end + +--[[ +Button +]]-- +Button = class(AbstractValue) + +function Button.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/button" + self.inputstyle = nil + self.rmempty = true +end + + +FileUpload = class(AbstractValue) + +function FileUpload.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/upload" + if not self.map.upload_fields then + self.map.upload_fields = { self } + else + self.map.upload_fields[#self.map.upload_fields+1] = self + end +end + +function FileUpload.formcreated(self, section) + return AbstractValue.formcreated(self, section) or + self.map:formvalue("cbi.rlf."..section.."."..self.option) or + self.map:formvalue("cbi.rlf."..section.."."..self.option..".x") +end + +function FileUpload.cfgvalue(self, section) + local val = AbstractValue.cfgvalue(self, section) + if val and fs.access(val) then + return val + end + return nil +end + +function FileUpload.formvalue(self, section) + local val = AbstractValue.formvalue(self, section) + if val then + if not self.map:formvalue("cbi.rlf."..section.."."..self.option) and + not self.map:formvalue("cbi.rlf."..section.."."..self.option..".x") + then + return val + end + fs.unlink(val) + self.value = nil + end + return nil +end + +function FileUpload.remove(self, section) + local val = AbstractValue.formvalue(self, section) + if val and fs.access(val) then fs.unlink(val) end + return AbstractValue.remove(self, section) +end + + +FileBrowser = class(AbstractValue) + +function FileBrowser.__init__(self, ...) + AbstractValue.__init__(self, ...) + self.template = "cbi/browser" +end diff --git a/modules/base/luasrc/luasrc/cbi/datatypes.lua b/modules/base/luasrc/luasrc/cbi/datatypes.lua new file mode 100644 index 0000000000..c5f4ec0f0d --- /dev/null +++ b/modules/base/luasrc/luasrc/cbi/datatypes.lua @@ -0,0 +1,345 @@ +--[[ + +LuCI - Configuration Bind Interface - Datatype Tests +(c) 2010 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +local fs = require "nixio.fs" +local ip = require "luci.ip" +local math = require "math" +local util = require "luci.util" +local tonumber, tostring, type, unpack, select = tonumber, tostring, type, unpack, select + + +module "luci.cbi.datatypes" + + +_M['or'] = function(v, ...) + local i + for i = 1, select('#', ...), 2 do + local f = select(i, ...) + local a = select(i+1, ...) + if type(f) ~= "function" then + if f == v then + return true + end + i = i - 1 + elseif f(v, unpack(a)) then + return true + end + end + return false +end + +_M['and'] = function(v, ...) + local i + for i = 1, select('#', ...), 2 do + local f = select(i, ...) + local a = select(i+1, ...) + if type(f) ~= "function" then + if f ~= v then + return false + end + i = i - 1 + elseif not f(v, unpack(a)) then + return false + end + end + return true +end + +function neg(v, ...) + return _M['or'](v:gsub("^%s*!%s*", ""), ...) +end + +function list(v, subvalidator, subargs) + if type(subvalidator) ~= "function" then + return false + end + local token + for token in v:gmatch("%S+") do + if not subvalidator(token, unpack(subargs)) then + return false + end + end + return true +end + +function bool(val) + if val == "1" or val == "yes" or val == "on" or val == "true" then + return true + elseif val == "0" or val == "no" or val == "off" or val == "false" then + return true + elseif val == "" or val == nil then + return true + end + + return false +end + +function uinteger(val) + local n = tonumber(val) + if n ~= nil and math.floor(n) == n and n >= 0 then + return true + end + + return false +end + +function integer(val) + local n = tonumber(val) + if n ~= nil and math.floor(n) == n then + return true + end + + return false +end + +function ufloat(val) + local n = tonumber(val) + return ( n ~= nil and n >= 0 ) +end + +function float(val) + return ( tonumber(val) ~= nil ) +end + +function ipaddr(val) + return ip4addr(val) or ip6addr(val) +end + +function ip4addr(val) + if val then + return ip.IPv4(val) and true or false + end + + return false +end + +function ip4prefix(val) + val = tonumber(val) + return ( val and val >= 0 and val <= 32 ) +end + +function ip6addr(val) + if val then + return ip.IPv6(val) and true or false + end + + return false +end + +function ip6prefix(val) + val = tonumber(val) + return ( val and val >= 0 and val <= 128 ) +end + +function port(val) + val = tonumber(val) + return ( val and val >= 0 and val <= 65535 ) +end + +function portrange(val) + local p1, p2 = val:match("^(%d+)%-(%d+)$") + if p1 and p2 and port(p1) and port(p2) then + return true + else + return port(val) + end +end + +function macaddr(val) + if val and val:match( + "^[a-fA-F0-9]+:[a-fA-F0-9]+:[a-fA-F0-9]+:" .. + "[a-fA-F0-9]+:[a-fA-F0-9]+:[a-fA-F0-9]+$" + ) then + local parts = util.split( val, ":" ) + + for i = 1,6 do + parts[i] = tonumber( parts[i], 16 ) + if parts[i] < 0 or parts[i] > 255 then + return false + end + end + + return true + end + + return false +end + +function hostname(val) + if val and (#val < 254) and ( + val:match("^[a-zA-Z_]+$") or + (val:match("^[a-zA-Z0-9_][a-zA-Z0-9_%-%.]*[a-zA-Z0-9]$") and + val:match("[^0-9%.]")) + ) then + return true + end + return false +end + +function host(val) + return hostname(val) or ipaddr(val) +end + +function network(val) + return uciname(val) or host(val) +end + +function wpakey(val) + if #val == 64 then + return (val:match("^[a-fA-F0-9]+$") ~= nil) + else + return (#val >= 8) and (#val <= 63) + end +end + +function wepkey(val) + if val:sub(1, 2) == "s:" then + val = val:sub(3) + end + + if (#val == 10) or (#val == 26) then + return (val:match("^[a-fA-F0-9]+$") ~= nil) + else + return (#val == 5) or (#val == 13) + end +end + +function string(val) + return true -- Everything qualifies as valid string +end + +function directory( val, seen ) + local s = fs.stat(val) + seen = seen or { } + + if s and not seen[s.ino] then + seen[s.ino] = true + if s.type == "dir" then + return true + elseif s.type == "lnk" then + return directory( fs.readlink(val), seen ) + end + end + + return false +end + +function file( val, seen ) + local s = fs.stat(val) + seen = seen or { } + + if s and not seen[s.ino] then + seen[s.ino] = true + if s.type == "reg" then + return true + elseif s.type == "lnk" then + return file( fs.readlink(val), seen ) + end + end + + return false +end + +function device( val, seen ) + local s = fs.stat(val) + seen = seen or { } + + if s and not seen[s.ino] then + seen[s.ino] = true + if s.type == "chr" or s.type == "blk" then + return true + elseif s.type == "lnk" then + return device( fs.readlink(val), seen ) + end + end + + return false +end + +function uciname(val) + return (val:match("^[a-zA-Z0-9_]+$") ~= nil) +end + +function range(val, min, max) + val = tonumber(val) + min = tonumber(min) + max = tonumber(max) + + if val ~= nil and min ~= nil and max ~= nil then + return ((val >= min) and (val <= max)) + end + + return false +end + +function min(val, min) + val = tonumber(val) + min = tonumber(min) + + if val ~= nil and min ~= nil then + return (val >= min) + end + + return false +end + +function max(val, max) + val = tonumber(val) + max = tonumber(max) + + if val ~= nil and max ~= nil then + return (val <= max) + end + + return false +end + +function rangelength(val, min, max) + val = tostring(val) + min = tonumber(min) + max = tonumber(max) + + if val ~= nil and min ~= nil and max ~= nil then + return ((#val >= min) and (#val <= max)) + end + + return false +end + +function minlength(val, min) + val = tostring(val) + min = tonumber(min) + + if val ~= nil and min ~= nil then + return (#val >= min) + end + + return false +end + +function maxlength(val, max) + val = tostring(val) + max = tonumber(max) + + if val ~= nil and max ~= nil then + return (#val <= max) + end + + return false +end + +function phonedigit(val) + return (val:match("^[0-9\*#!%.]+$") ~= nil) +end diff --git a/modules/base/luasrc/luasrc/config.lua b/modules/base/luasrc/luasrc/config.lua new file mode 100644 index 0000000000..53db82b322 --- /dev/null +++ b/modules/base/luasrc/luasrc/config.lua @@ -0,0 +1,42 @@ +--[[ +LuCI - Configuration + +Description: +Some LuCI configuration values read from uci file "luci" + + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +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) diff --git a/modules/base/luasrc/luasrc/dispatcher.lua b/modules/base/luasrc/luasrc/dispatcher.lua new file mode 100644 index 0000000000..9e5b78d5e9 --- /dev/null +++ b/modules/base/luasrc/luasrc/dispatcher.lua @@ -0,0 +1,959 @@ +--[[ +LuCI - Dispatcher + +Description: +The request dispatcher and module dispatcher generators + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +--- LuCI web dispatcher. +local fs = require "nixio.fs" +local sys = require "luci.sys" +local init = require "luci.init" +local util = require "luci.util" +local http = require "luci.http" +local nixio = require "nixio", require "nixio.util" + +module("luci.dispatcher", package.seeall) +context = util.threadlocal() +uci = require "luci.model.uci" +i18n = require "luci.i18n" +_M.fs = fs + +authenticator = {} + +-- Index table +local index = nil + +-- Fastindex +local fi + + +--- Build the URL relative to the server webroot from given virtual path. +-- @param ... Virtual path +-- @return Relative URL +function build_url(...) + local path = {...} + local url = { http.getenv("SCRIPT_NAME") or "" } + + local k, v + for k, v in pairs(context.urltoken) do + url[#url+1] = "/;" + url[#url+1] = http.urlencode(k) + url[#url+1] = "=" + url[#url+1] = http.urlencode(v) + end + + local p + for _, p in ipairs(path) do + if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then + url[#url+1] = "/" + url[#url+1] = p + end + end + + return table.concat(url, "") +end + +--- Check whether a dispatch node shall be visible +-- @param node Dispatch node +-- @return Boolean indicating whether the node should be visible +function node_visible(node) + if node then + return not ( + (not node.title or #node.title == 0) or + (not node.target or node.hidden == true) or + (type(node.target) == "table" and node.target.type == "firstchild" and + (type(node.nodes) ~= "table" or not next(node.nodes))) + ) + end + return false +end + +--- Return a sorted table of visible childs within a given node +-- @param node Dispatch node +-- @return Ordered table of child node names +function node_childs(node) + local rv = { } + if node then + local k, v + for k, v in util.spairs(node.nodes, + function(a, b) + return (node.nodes[a].order or 100) + < (node.nodes[b].order or 100) + end) + do + if node_visible(v) then + rv[#rv+1] = k + end + end + end + return rv +end + + +--- Send a 404 error code and render the "error404" template if available. +-- @param message Custom error message (optional) +-- @return false +function error404(message) + luci.http.status(404, "Not Found") + message = message or "Not Found" + + require("luci.template") + if not luci.util.copcall(luci.template.render, "error404") then + luci.http.prepare_content("text/plain") + luci.http.write(message) + end + return false +end + +--- Send a 500 error code and render the "error500" template if available. +-- @param message Custom error message (optional)# +-- @return false +function error500(message) + luci.util.perror(message) + if not context.template_header_sent then + luci.http.status(500, "Internal Server Error") + luci.http.prepare_content("text/plain") + luci.http.write(message) + else + require("luci.template") + if not luci.util.copcall(luci.template.render, "error500", {message=message}) then + luci.http.prepare_content("text/plain") + luci.http.write(message) + end + end + return false +end + +function authenticator.htmlauth(validator, accs, default) + local user = luci.http.formvalue("username") + local pass = luci.http.formvalue("password") + + if user and validator(user, pass) then + return user + end + + require("luci.i18n") + require("luci.template") + context.path = {} + luci.template.render("sysauth", {duser=default, fuser=user}) + return false + +end + +--- Dispatch an HTTP request. +-- @param request LuCI HTTP Request object +function httpdispatch(request, prefix) + luci.http.context.request = request + + local r = {} + context.request = r + context.urltoken = {} + + local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true) + + if prefix then + for _, node in ipairs(prefix) do + r[#r+1] = node + end + end + + local tokensok = true + for node in pathinfo:gmatch("[^/]+") do + local tkey, tval + if tokensok then + tkey, tval = node:match(";(%w+)=([a-fA-F0-9]*)") + end + if tkey then + context.urltoken[tkey] = tval + else + tokensok = false + r[#r+1] = node + end + end + + local stat, err = util.coxpcall(function() + dispatch(context.request) + end, error500) + + luci.http.close() + + --context._disable_memtrace() +end + +--- Dispatches a LuCI virtual path. +-- @param request Virtual path +function dispatch(request) + --context._disable_memtrace = require "luci.debug".trap_memtrace("l") + local ctx = context + ctx.path = request + + local conf = require "luci.config" + assert(conf.main, + "/etc/config/luci seems to be corrupt, unable to find section 'main'") + + local lang = conf.main.lang or "auto" + if lang == "auto" then + local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or "" + for lpat in aclang:gmatch("[%w-]+") do + lpat = lpat and lpat:gsub("-", "_") + if conf.languages[lpat] then + lang = lpat + break + end + end + end + require "luci.i18n".setlanguage(lang) + + local c = ctx.tree + local stat + if not c then + c = createtree() + end + + local track = {} + local args = {} + ctx.args = args + ctx.requestargs = ctx.requestargs or args + local n + local token = ctx.urltoken + local preq = {} + local freq = {} + + for i, s in ipairs(request) do + preq[#preq+1] = s + freq[#freq+1] = s + c = c.nodes[s] + n = i + if not c then + break + end + + util.update(track, c) + + if c.leaf then + break + end + end + + if c and c.leaf then + for j=n+1, #request do + args[#args+1] = request[j] + freq[#freq+1] = request[j] + end + end + + ctx.requestpath = ctx.requestpath or freq + ctx.path = preq + + if track.i18n then + i18n.loadc(track.i18n) + end + + -- Init template engine + if (c and c.index) or not track.notemplate then + local tpl = require("luci.template") + local media = track.mediaurlbase or luci.config.main.mediaurlbase + if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then + media = nil + for name, theme in pairs(luci.config.themes) do + if name:sub(1,1) ~= "." and pcall(tpl.Template, + "themes/%s/header" % fs.basename(theme)) then + media = theme + end + end + assert(media, "No valid theme found") + end + + local function _ifattr(cond, key, val) + if cond then + local env = getfenv(3) + local scope = (type(env.self) == "table") and env.self + return string.format( + ' %s="%s"', tostring(key), + luci.util.pcdata(tostring( val + or (type(env[key]) ~= "function" and env[key]) + or (scope and type(scope[key]) ~= "function" and scope[key]) + or "" )) + ) + else + return '' + end + end + + tpl.context.viewns = setmetatable({ + write = luci.http.write; + include = function(name) tpl.Template(name):render(getfenv(2)) end; + translate = i18n.translate; + translatef = i18n.translatef; + export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end; + striptags = util.striptags; + pcdata = util.pcdata; + media = media; + theme = fs.basename(media); + resource = luci.config.main.resourcebase; + ifattr = function(...) return _ifattr(...) end; + attr = function(...) return _ifattr(true, ...) end; + }, {__index=function(table, key) + if key == "controller" then + return build_url() + elseif key == "REQUEST_URI" then + return build_url(unpack(ctx.requestpath)) + else + return rawget(table, key) or _G[key] + end + end}) + end + + track.dependent = (track.dependent ~= false) + assert(not track.dependent or not track.auto, + "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " .. + "has no parent node so the access to this location has been denied.\n" .. + "This is a software bug, please report this message at " .. + "http://luci.subsignal.org/trac/newticket" + ) + + if track.sysauth then + local sauth = require "luci.sauth" + + local authen = type(track.sysauth_authenticator) == "function" + and track.sysauth_authenticator + or authenticator[track.sysauth_authenticator] + + local def = (type(track.sysauth) == "string") and track.sysauth + local accs = def and {track.sysauth} or track.sysauth + local sess = ctx.authsession + local verifytoken = false + if not sess then + sess = luci.http.getcookie("sysauth") + sess = sess and sess:match("^[a-f0-9]*$") + verifytoken = true + end + + local sdat = sauth.read(sess) + local user + + if sdat then + if not verifytoken or ctx.urltoken.stok == sdat.token then + user = sdat.user + end + else + local eu = http.getenv("HTTP_AUTH_USER") + local ep = http.getenv("HTTP_AUTH_PASS") + if eu and ep and luci.sys.user.checkpasswd(eu, ep) then + authen = function() return eu end + end + end + + if not util.contains(accs, user) then + if authen then + ctx.urltoken.stok = nil + local user, sess = authen(luci.sys.user.checkpasswd, accs, def) + if not user or not util.contains(accs, user) then + return + else + local sid = sess or luci.sys.uniqueid(16) + if not sess then + local token = luci.sys.uniqueid(16) + sauth.reap() + sauth.write(sid, { + user=user, + token=token, + secret=luci.sys.uniqueid(16) + }) + ctx.urltoken.stok = token + end + luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url()) + ctx.authsession = sid + ctx.authuser = user + end + else + luci.http.status(403, "Forbidden") + return + end + else + ctx.authsession = sess + ctx.authuser = user + end + end + + if track.setgroup then + luci.sys.process.setgroup(track.setgroup) + end + + if track.setuser then + luci.sys.process.setuser(track.setuser) + end + + local target = nil + if c then + if type(c.target) == "function" then + target = c.target + elseif type(c.target) == "table" then + target = c.target.target + end + end + + if c and (c.index or type(target) == "function") then + ctx.dispatched = c + ctx.requested = ctx.requested or ctx.dispatched + end + + if c and c.index then + local tpl = require "luci.template" + + if util.copcall(tpl.render, "indexer", {}) then + return true + end + end + + if type(target) == "function" then + util.copcall(function() + local oldenv = getfenv(target) + local module = require(c.module) + local env = setmetatable({}, {__index= + + function(tbl, key) + return rawget(tbl, key) or module[key] or oldenv[key] + end}) + + setfenv(target, env) + end) + + local ok, err + if type(c.target) == "table" then + ok, err = util.copcall(target, c.target, unpack(args)) + else + ok, err = util.copcall(target, unpack(args)) + end + assert(ok, + "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") .. + " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" .. + "The called action terminated with an exception:\n" .. tostring(err or "(unknown)")) + else + local root = node() + if not root or not root.target then + error404("No root node was registered, this usually happens if no module was installed.\n" .. + "Install luci-mod-admin-full and retry. " .. + "If the module is already installed, try removing the /tmp/luci-indexcache file.") + else + error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" .. + "If this url belongs to an extension, make sure it is properly installed.\n" .. + "If the extension was recently installed, try removing the /tmp/luci-indexcache file.") + end + end +end + +--- Generate the dispatching index using the best possible strategy. +function createindex() + local path = luci.util.libpath() .. "/controller/" + local suff = { ".lua", ".lua.gz" } + + if luci.util.copcall(require, "luci.fastindex") then + createindex_fastindex(path, suff) + else + createindex_plain(path, suff) + end +end + +--- Generate the dispatching index using the fastindex C-indexer. +-- @param path Controller base directory +-- @param suffixes Controller file suffixes +function createindex_fastindex(path, suffixes) + index = {} + + if not fi then + fi = luci.fastindex.new("index") + for _, suffix in ipairs(suffixes) do + fi.add(path .. "*" .. suffix) + fi.add(path .. "*/*" .. suffix) + end + end + fi.scan() + + for k, v in pairs(fi.indexes) do + index[v[2]] = v[1] + end +end + +--- Generate the dispatching index using the native file-cache based strategy. +-- @param path Controller base directory +-- @param suffixes Controller file suffixes +function createindex_plain(path, suffixes) + local controllers = { } + for _, suffix in ipairs(suffixes) do + nixio.util.consume((fs.glob(path .. "*" .. suffix)), controllers) + nixio.util.consume((fs.glob(path .. "*/*" .. suffix)), controllers) + end + + if indexcache then + local cachedate = fs.stat(indexcache, "mtime") + if cachedate then + local realdate = 0 + for _, obj in ipairs(controllers) do + local omtime = fs.stat(obj, "mtime") + realdate = (omtime and omtime > realdate) and omtime or realdate + end + + if cachedate > realdate then + assert( + sys.process.info("uid") == fs.stat(indexcache, "uid") + and fs.stat(indexcache, "modestr") == "rw-------", + "Fatal: Indexcache is not sane!" + ) + + index = loadfile(indexcache)() + return index + end + end + end + + index = {} + + for i,c in ipairs(controllers) do + local modname = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".") + for _, suffix in ipairs(suffixes) do + modname = modname:gsub(suffix.."$", "") + end + + local mod = require(modname) + assert(mod ~= true, + "Invalid controller file found\n" .. + "The file '" .. c .. "' contains an invalid module line.\n" .. + "Please verify whether the module name is set to '" .. modname .. + "' - It must correspond to the file path!") + + local idx = mod.index + assert(type(idx) == "function", + "Invalid controller file found\n" .. + "The file '" .. c .. "' contains no index() function.\n" .. + "Please make sure that the controller contains a valid " .. + "index function and verify the spelling!") + + index[modname] = idx + end + + if indexcache then + local f = nixio.open(indexcache, "w", 600) + f:writeall(util.get_bytecode(index)) + f:close() + end +end + +--- Create the dispatching tree from the index. +-- Build the index before if it does not exist yet. +function createtree() + if not index then + createindex() + end + + local ctx = context + local tree = {nodes={}, inreq=true} + local modi = {} + + ctx.treecache = setmetatable({}, {__mode="v"}) + ctx.tree = tree + ctx.modifiers = modi + + -- Load default translation + require "luci.i18n".loadc("base") + + local scope = setmetatable({}, {__index = luci.dispatcher}) + + for k, v in pairs(index) do + scope._NAME = k + setfenv(v, scope) + v() + end + + local function modisort(a,b) + return modi[a].order < modi[b].order + end + + for _, v in util.spairs(modi, modisort) do + scope._NAME = v.module + setfenv(v.func, scope) + v.func() + end + + return tree +end + +--- Register a tree modifier. +-- @param func Modifier function +-- @param order Modifier order value (optional) +function modifier(func, order) + context.modifiers[#context.modifiers+1] = { + func = func, + order = order or 0, + module + = getfenv(2)._NAME + } +end + +--- Clone a node of the dispatching tree to another position. +-- @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 +function assign(path, clone, title, order) + local obj = node(unpack(path)) + obj.nodes = nil + obj.module = nil + + obj.title = title + obj.order = order + + setmetatable(obj, {__index = _create_node(clone)}) + + return obj +end + +--- Create a new dispatching node and define common parameters. +-- @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 +function entry(path, target, title, order) + local c = node(unpack(path)) + + c.target = target + c.title = title + c.order = order + c.module = getfenv(2)._NAME + + return c +end + +--- Fetch or create a dispatching node without setting the target module or +-- enabling the node. +-- @param ... Virtual path +-- @return Dispatching tree node +function get(...) + return _create_node({...}) +end + +--- Fetch or create a new dispatching node. +-- @param ... Virtual path +-- @return Dispatching tree node +function node(...) + local c = _create_node({...}) + + c.module = getfenv(2)._NAME + c.auto = nil + + return c +end + +function _create_node(path) + if #path == 0 then + return context.tree + end + + local name = table.concat(path, ".") + local c = context.treecache[name] + + if not c then + local last = table.remove(path) + local parent = _create_node(path) + + c = {nodes={}, auto=true} + -- the node is "in request" if the request path matches + -- at least up to the length of the node path + if parent.inreq and context.path[#path+1] == last then + c.inreq = true + end + parent.nodes[last] = c + context.treecache[name] = c + end + return c +end + +-- Subdispatchers -- + +function _firstchild() + local path = { unpack(context.path) } + local name = table.concat(path, ".") + local node = context.treecache[name] + + local lowest + if node and node.nodes and next(node.nodes) then + local k, v + for k, v in pairs(node.nodes) do + if not lowest or + (v.order or 100) < (node.nodes[lowest].order or 100) + then + lowest = k + end + end + end + + assert(lowest ~= nil, + "The requested node contains no childs, unable to redispatch") + + path[#path+1] = lowest + dispatch(path) +end + +--- Alias the first (lowest order) page automatically +function firstchild() + return { type = "firstchild", target = _firstchild } +end + +--- Create a redirect to another dispatching node. +-- @param ... Virtual path destination +function alias(...) + local req = {...} + return function(...) + for _, r in ipairs({...}) do + req[#req+1] = r + end + + dispatch(req) + end +end + +--- Rewrite the first x path values of the request. +-- @param n Number of path values to replace +-- @param ... Virtual path to replace removed path values with +function rewrite(n, ...) + local req = {...} + return function(...) + local dispatched = util.clone(context.dispatched) + + for i=1,n do + table.remove(dispatched, 1) + end + + for i, r in ipairs(req) do + table.insert(dispatched, i, r) + end + + for _, r in ipairs({...}) do + dispatched[#dispatched+1] = r + end + + dispatch(dispatched) + end +end + + +local function _call(self, ...) + local func = getfenv()[self.name] + assert(func ~= nil, + 'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?') + + assert(type(func) == "function", + 'The symbol "' .. self.name .. '" does not refer to a function but data ' .. + 'of type "' .. type(func) .. '".') + + if #self.argv > 0 then + return func(unpack(self.argv), ...) + else + return func(...) + end +end + +--- Create a function-call dispatching target. +-- @param name Target function of local controller +-- @param ... Additional parameters passed to the function +function call(name, ...) + return {type = "call", argv = {...}, name = name, target = _call} +end + + +local _template = function(self, ...) + require "luci.template".render(self.view) +end + +--- Create a template render dispatching target. +-- @param name Template to be rendered +function template(name) + return {type = "template", view = name, target = _template} +end + + +local function _cbi(self, ...) + local cbi = require "luci.cbi" + local tpl = require "luci.template" + local http = require "luci.http" + + local config = self.config or {} + local maps = cbi.load(self.model, ...) + + local state = nil + + for i, res in ipairs(maps) do + res.flow = config + local cstate = res:parse() + if cstate and (not state or cstate < state) then + state = cstate + end + end + + local function _resolve_path(path) + return type(path) == "table" and build_url(unpack(path)) or path + end + + if config.on_valid_to and state and state > 0 and state < 2 then + http.redirect(_resolve_path(config.on_valid_to)) + return + end + + if config.on_changed_to and state and state > 1 then + http.redirect(_resolve_path(config.on_changed_to)) + return + end + + if config.on_success_to and state and state > 0 then + http.redirect(_resolve_path(config.on_success_to)) + return + end + + if config.state_handler then + if not config.state_handler(state, maps) then + return + end + end + + http.header("X-CBI-State", state or 0) + + if not config.noheader then + tpl.render("cbi/header", {state = state}) + end + + local redirect + local messages + local applymap = false + local pageaction = true + local parsechain = { } + + for i, res in ipairs(maps) do + if res.apply_needed and res.parsechain then + local c + for _, c in ipairs(res.parsechain) do + parsechain[#parsechain+1] = c + end + applymap = true + end + + if res.redirect then + redirect = redirect or res.redirect + end + + if res.pageaction == false then + pageaction = false + end + + if res.message then + messages = messages or { } + messages[#messages+1] = res.message + end + end + + for i, res in ipairs(maps) do + res:render({ + firstmap = (i == 1), + applymap = applymap, + redirect = redirect, + messages = messages, + pageaction = pageaction, + parsechain = parsechain + }) + end + + if not config.nofooter then + tpl.render("cbi/footer", { + flow = config, + pageaction = pageaction, + redirect = redirect, + state = state, + autoapply = config.autoapply + }) + end +end + +--- Create a CBI model dispatching target. +-- @param model CBI model to be rendered +function cbi(model, config) + return {type = "cbi", config = config, model = model, target = _cbi} +end + + +local function _arcombine(self, ...) + local argv = {...} + local target = #argv > 0 and self.targets[2] or self.targets[1] + setfenv(target.target, self.env) + target:target(unpack(argv)) +end + +--- Create a combined dispatching target for non argv and argv requests. +-- @param trg1 Overview Target +-- @param trg2 Detail Target +function arcombine(trg1, trg2) + return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}} +end + + +local function _form(self, ...) + local cbi = require "luci.cbi" + local tpl = require "luci.template" + local http = require "luci.http" + + local maps = luci.cbi.load(self.model, ...) + local state = nil + + for i, res in ipairs(maps) do + local cstate = res:parse() + if cstate and (not state or cstate < state) then + state = cstate + end + end + + http.header("X-CBI-State", state or 0) + tpl.render("header") + for i, res in ipairs(maps) do + res:render() + end + tpl.render("footer") +end + +--- Create a CBI form model dispatching target. +-- @param model CBI form model tpo be rendered +function form(model) + return {type = "cbi", model = model, target = _form} +end + +--- Access the luci.i18n translate() api. +-- @class function +-- @name translate +-- @param text Text to translate +translate = i18n.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. +function _(text) + return text +end diff --git a/modules/base/luasrc/luasrc/http.lua b/modules/base/luasrc/luasrc/http.lua new file mode 100644 index 0000000000..c53307a5a1 --- /dev/null +++ b/modules/base/luasrc/luasrc/http.lua @@ -0,0 +1,344 @@ +--[[ +LuCI - HTTP-Interaction + +Description: +HTTP-Header manipulator and form variable preprocessor + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +local ltn12 = require "luci.ltn12" +local protocol = require "luci.http.protocol" +local util = require "luci.util" +local string = require "string" +local coroutine = require "coroutine" +local table = require "table" + +local ipairs, pairs, next, type, tostring, error = + ipairs, pairs, next, type, tostring, error + +--- LuCI Web Framework high-level HTTP functions. +module "luci.http" + +context = util.threadlocal() + +Request = util.class() +function Request.__init__(self, env, sourcein, sinkerr) + self.input = sourcein + self.error = sinkerr + + + -- File handler + self.filehandler = function() end + + -- HTTP-Message table + self.message = { + env = env, + headers = {}, + params = protocol.urldecode_params(env.QUERY_STRING or ""), + } + + self.parsed_input = false +end + +function Request.formvalue(self, name, noparse) + if not noparse and not self.parsed_input then + self:_parse_input() + end + + if name then + return self.message.params[name] + else + return self.message.params + end +end + +function Request.formvaluetable(self, prefix) + local vals = {} + prefix = prefix and prefix .. "." or "." + + if not self.parsed_input then + self:_parse_input() + end + + local void = self.message.params[nil] + for k, v in pairs(self.message.params) do + if k:find(prefix, 1, true) == 1 then + vals[k:sub(#prefix + 1)] = tostring(v) + end + end + + return vals +end + +function Request.content(self) + if not self.parsed_input then + self:_parse_input() + end + + return self.message.content, self.message.content_length +end + +function Request.getcookie(self, name) + local c = string.gsub(";" .. (self:getenv("HTTP_COOKIE") or "") .. ";", "%s*;%s*", ";") + local p = ";" .. name .. "=(.-);" + local i, j, value = c:find(p) + return value and urldecode(value) +end + +function Request.getenv(self, name) + if name then + return self.message.env[name] + else + return self.message.env + end +end + +function Request.setfilehandler(self, callback) + self.filehandler = callback +end + +function Request._parse_input(self) + protocol.parse_message_body( + self.input, + self.message, + self.filehandler + ) + self.parsed_input = true +end + +--- Close the HTTP-Connection. +function close() + if not context.eoh then + context.eoh = true + coroutine.yield(3) + end + + if not context.closed then + context.closed = true + coroutine.yield(5) + end +end + +--- Return the request content if the request was of unknown type. +-- @return HTTP request body +-- @return HTTP request body length +function content() + return context.request:content() +end + +--- Get a certain HTTP input value or a table of all input values. +-- @param name Name of the GET or POST variable to fetch +-- @param noparse Don't parse POST data before getting the value +-- @return HTTP input value or table of all input value +function formvalue(name, noparse) + return context.request:formvalue(name, noparse) +end + +--- Get a table of all HTTP input values with a certain prefix. +-- @param prefix Prefix +-- @return Table of all HTTP input values with given prefix +function formvaluetable(prefix) + return context.request:formvaluetable(prefix) +end + +--- Get the value of a certain HTTP-Cookie. +-- @param name Cookie Name +-- @return String containing cookie data +function getcookie(name) + return context.request:getcookie(name) +end + +--- Get the value of a certain HTTP environment variable +-- or the environment table itself. +-- @param name Environment variable +-- @return HTTP environment value or environment table +function getenv(name) + return context.request:getenv(name) +end + +--- Set a handler function for incoming user file uploads. +-- @param callback Handler function +function setfilehandler(callback) + return context.request:setfilehandler(callback) +end + +--- Send a HTTP-Header. +-- @param key Header key +-- @param value Header value +function header(key, value) + if not context.headers then + context.headers = {} + end + context.headers[key:lower()] = value + coroutine.yield(2, key, value) +end + +--- Set the mime type of following content data. +-- @param mime Mimetype of following content +function prepare_content(mime) + if not context.headers or not context.headers["content-type"] then + if mime == "application/xhtml+xml" then + if not getenv("HTTP_ACCEPT") or + not getenv("HTTP_ACCEPT"):find("application/xhtml+xml", nil, true) then + mime = "text/html; charset=UTF-8" + end + header("Vary", "Accept") + end + header("Content-Type", mime) + end +end + +--- Get the RAW HTTP input source +-- @return HTTP LTN12 source +function source() + return context.request.input +end + +--- Set the HTTP status code and status message. +-- @param code Status code +-- @param message Status message +function status(code, message) + code = code or 200 + message = message or "OK" + context.status = code + coroutine.yield(1, code, message) +end + +--- Send a chunk of content data to the client. +-- This function is as a valid LTN12 sink. +-- If the content chunk is nil this function will automatically invoke close. +-- @param content Content chunk +-- @param src_err Error object from source (optional) +-- @see close +function write(content, src_err) + if not content then + if src_err then + error(src_err) + else + close() + end + return true + elseif #content == 0 then + return true + else + if not context.eoh then + if not context.status then + status() + end + if not context.headers or not context.headers["content-type"] then + header("Content-Type", "text/html; charset=utf-8") + end + if not context.headers["cache-control"] then + header("Cache-Control", "no-cache") + header("Expires", "0") + end + + + context.eoh = true + coroutine.yield(3) + end + coroutine.yield(4, content) + return true + end +end + +--- Splice data from a filedescriptor to the client. +-- @param fp File descriptor +-- @param size Bytes to splice (optional) +function splice(fd, size) + coroutine.yield(6, fd, size) +end + +--- Redirects the client to a new URL and closes the connection. +-- @param url Target URL +function redirect(url) + status(302, "Found") + header("Location", url) + close() +end + +--- Create a querystring out of a table of key - value pairs. +-- @param table Query string source table +-- @return Encoded HTTP query string +function build_querystring(q) + local s = { "?" } + + for k, v in pairs(q) do + if #s > 1 then s[#s+1] = "&" end + + s[#s+1] = urldecode(k) + s[#s+1] = "=" + s[#s+1] = urldecode(v) + end + + return table.concat(s, "") +end + +--- Return the URL-decoded equivalent of a string. +-- @param str URL-encoded string +-- @param no_plus Don't decode + to " " +-- @return URL-decoded string +-- @see urlencode +urldecode = protocol.urldecode + +--- Return the URL-encoded equivalent of a string. +-- @param str Source string +-- @return URL-encoded string +-- @see urldecode +urlencode = protocol.urlencode + +--- Send the given data as JSON encoded string. +-- @param data Data to send +function write_json(x) + if x == nil then + write("null") + elseif type(x) == "table" then + local k, v + if type(next(x)) == "number" then + write("[ ") + for k, v in ipairs(x) do + write_json(v) + if next(x, k) then + write(", ") + end + end + write(" ]") + else + write("{ ") + for k, v in pairs(x) do + write("%q: " % k) + write_json(v) + if next(x, k) then + write(", ") + end + end + write(" }") + end + elseif type(x) == "number" or type(x) == "boolean" then + if (x ~= x) then + -- NaN is the only value that doesn't equal to itself. + write("Number.NaN") + else + write(tostring(x)) + end + else + write('"%s"' % tostring(x):gsub('["%z\1-\31]', function(c) + return '\\u%04x' % c:byte(1) + end)) + end +end diff --git a/modules/base/luasrc/luasrc/http/protocol.lua b/modules/base/luasrc/luasrc/http/protocol.lua new file mode 100644 index 0000000000..0d41550b23 --- /dev/null +++ b/modules/base/luasrc/luasrc/http/protocol.lua @@ -0,0 +1,688 @@ +--[[ + +HTTP protocol implementation for LuCI +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +--- LuCI http protocol class. +-- This class contains several functions useful for http message- and content +-- decoding and to retrive form data from raw http messages. +module("luci.http.protocol", package.seeall) + +local ltn12 = require("luci.ltn12") + +HTTP_MAX_CONTENT = 1024*8 -- 8 kB maximum content size + +--- Decode an urlencoded string - optionally without decoding +-- the "+" sign to " " - and return the decoded string. +-- @param str Input string in x-www-urlencoded format +-- @param no_plus Don't decode "+" signs to spaces +-- @return The decoded string +-- @see urlencode +function urldecode( str, no_plus ) + + local function __chrdec( hex ) + return string.char( tonumber( hex, 16 ) ) + end + + if type(str) == "string" then + if not no_plus then + str = str:gsub( "+", " " ) + end + + str = str:gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec ) + end + + return str +end + +--- Extract and split urlencoded data pairs, separated bei either "&" or ";" +-- from given url or string. Returns a table with urldecoded values. +-- Simple parameters are stored as string values associated with the parameter +-- name within the table. Parameters with multiple values are stored as array +-- containing the corresponding values. +-- @param url The url or string which contains x-www-urlencoded form data +-- @param tbl Use the given table for storing values (optional) +-- @return Table containing the urldecoded parameters +-- @see urlencode_params +function urldecode_params( url, tbl ) + + local params = tbl or { } + + if url:find("?") then + url = url:gsub( "^.+%?([^?]+)", "%1" ) + end + + for pair in url:gmatch( "[^&;]+" ) do + + -- find key and value + local key = urldecode( pair:match("^([^=]+)") ) + local val = urldecode( pair:match("^[^=]+=(.+)$") ) + + -- store + if type(key) == "string" and key:len() > 0 then + if type(val) ~= "string" then val = "" end + + if not params[key] then + params[key] = val + elseif type(params[key]) ~= "table" then + params[key] = { params[key], val } + else + table.insert( params[key], val ) + end + end + end + + return params +end + +--- Encode given string to x-www-urlencoded format. +-- @param str String to encode +-- @return String containing the encoded data +-- @see urldecode +function urlencode( str ) + + local function __chrenc( chr ) + return string.format( + "%%%02x", string.byte( chr ) + ) + end + + if type(str) == "string" then + str = str:gsub( + "([^a-zA-Z0-9$_%-%.%+!*'(),])", + __chrenc + ) + end + + return str +end + +--- Encode each key-value-pair in given table to x-www-urlencoded format, +-- separated by "&". Tables are encoded as parameters with multiple values by +-- repeating the parameter name with each value. +-- @param tbl Table with the values +-- @return String containing encoded values +-- @see urldecode_params +function urlencode_params( tbl ) + local enc = "" + + for k, v in pairs(tbl) do + if type(v) == "table" then + for i, v2 in ipairs(v) do + enc = enc .. ( #enc > 0 and "&" or "" ) .. + urlencode(k) .. "=" .. urlencode(v2) + end + else + enc = enc .. ( #enc > 0 and "&" or "" ) .. + urlencode(k) .. "=" .. urlencode(v) + end + end + + return enc +end + +-- (Internal function) +-- Initialize given parameter and coerce string into table when the parameter +-- already exists. +-- @param tbl Table where parameter should be created +-- @param key Parameter name +-- @return Always nil +local function __initval( tbl, key ) + if tbl[key] == nil then + tbl[key] = "" + elseif type(tbl[key]) == "string" then + tbl[key] = { tbl[key], "" } + else + table.insert( tbl[key], "" ) + end +end + +-- (Internal function) +-- Append given data to given parameter, either by extending the string value +-- or by appending it to the last string in the parameter's value table. +-- @param tbl Table containing the previously initialized parameter value +-- @param key Parameter name +-- @param chunk String containing the data to append +-- @return Always nil +-- @see __initval +local function __appendval( tbl, key, chunk ) + if type(tbl[key]) == "table" then + tbl[key][#tbl[key]] = tbl[key][#tbl[key]] .. chunk + else + tbl[key] = tbl[key] .. chunk + end +end + +-- (Internal function) +-- Finish the value of given parameter, either by transforming the string value +-- or - in the case of multi value parameters - the last element in the +-- associated values table. +-- @param tbl Table containing the previously initialized parameter value +-- @param key Parameter name +-- @param handler Function which transforms the parameter value +-- @return Always nil +-- @see __initval +-- @see __appendval +local function __finishval( tbl, key, handler ) + if handler then + if type(tbl[key]) == "table" then + tbl[key][#tbl[key]] = handler( tbl[key][#tbl[key]] ) + else + tbl[key] = handler( tbl[key] ) + end + end +end + + +-- Table of our process states +local process_states = { } + +-- Extract "magic", the first line of a http message. +-- Extracts the message type ("get", "post" or "response"), the requested uri +-- or the status code if the line descripes a http response. +process_states['magic'] = function( msg, chunk, err ) + + if chunk ~= nil then + -- ignore empty lines before request + if #chunk == 0 then + return true, nil + end + + -- Is it a request? + local method, uri, http_ver = chunk:match("^([A-Z]+) ([^ ]+) HTTP/([01]%.[019])$") + + -- Yup, it is + if method then + + msg.type = "request" + msg.request_method = method:lower() + msg.request_uri = uri + msg.http_version = tonumber( http_ver ) + msg.headers = { } + + -- We're done, next state is header parsing + return true, function( chunk ) + return process_states['headers']( msg, chunk ) + end + + -- Is it a response? + else + + local http_ver, code, message = chunk:match("^HTTP/([01]%.[019]) ([0-9]+) ([^\r\n]+)$") + + -- Is a response + if code then + + msg.type = "response" + msg.status_code = code + msg.status_message = message + msg.http_version = tonumber( http_ver ) + msg.headers = { } + + -- We're done, next state is header parsing + return true, function( chunk ) + return process_states['headers']( msg, chunk ) + end + end + end + end + + -- Can't handle it + return nil, "Invalid HTTP message magic" +end + + +-- Extract headers from given string. +process_states['headers'] = function( msg, chunk ) + + if chunk ~= nil then + + -- Look for a valid header format + local hdr, val = chunk:match( "^([A-Za-z][A-Za-z0-9%-_]+): +(.+)$" ) + + if type(hdr) == "string" and hdr:len() > 0 and + type(val) == "string" and val:len() > 0 + then + msg.headers[hdr] = val + + -- Valid header line, proceed + return true, nil + + elseif #chunk == 0 then + -- Empty line, we won't accept data anymore + return false, nil + else + -- Junk data + return nil, "Invalid HTTP header received" + end + else + return nil, "Unexpected EOF" + end +end + + +--- Creates a ltn12 source from the given socket. The source will return it's +-- data line by line with the trailing \r\n stripped of. +-- @param sock Readable network socket +-- @return Ltn12 source function +function header_source( sock ) + return ltn12.source.simplify( function() + + local chunk, err, part = sock:receive("*l") + + -- Line too long + if chunk == nil then + if err ~= "timeout" then + return nil, part + and "Line exceeds maximum allowed length" + or "Unexpected EOF" + else + return nil, err + end + + -- Line ok + elseif chunk ~= nil then + + -- Strip trailing CR + chunk = chunk:gsub("\r$","") + + return chunk, nil + end + end ) +end + +--- Decode a mime encoded http message body with multipart/form-data +-- Content-Type. Stores all extracted data associated with its parameter name +-- in the params table withing the given message object. Multiple parameter +-- values are stored as tables, ordinary ones as strings. +-- If an optional file callback function is given then it is feeded with the +-- file contents chunk by chunk and only the extracted file name is stored +-- within the params table. The callback function will be called subsequently +-- with three arguments: +-- o Table containing decoded (name, file) and raw (headers) mime header data +-- o String value containing a chunk of the file data +-- o Boolean which indicates wheather the current chunk is the last one (eof) +-- @param src Ltn12 source function +-- @param msg HTTP message object +-- @param filecb File callback function (optional) +-- @return Value indicating successful operation (not nil means "ok") +-- @return String containing the error if unsuccessful +-- @see parse_message_header +function mimedecode_message_body( src, msg, filecb ) + + if msg and msg.env.CONTENT_TYPE then + msg.mime_boundary = msg.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$") + end + + if not msg.mime_boundary then + return nil, "Invalid Content-Type found" + end + + + local tlen = 0 + local inhdr = false + local field = nil + local store = nil + local lchunk = nil + + local function parse_headers( chunk, field ) + + local stat + repeat + chunk, stat = chunk:gsub( + "^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n", + function(k,v) + field.headers[k] = v + return "" + end + ) + until stat == 0 + + chunk, stat = chunk:gsub("^\r\n","") + + -- End of headers + if stat > 0 then + if field.headers["Content-Disposition"] then + if field.headers["Content-Disposition"]:match("^form%-data; ") then + field.name = field.headers["Content-Disposition"]:match('name="(.-)"') + field.file = field.headers["Content-Disposition"]:match('filename="(.+)"$') + end + end + + if not field.headers["Content-Type"] then + field.headers["Content-Type"] = "text/plain" + end + + if field.name and field.file and filecb then + __initval( msg.params, field.name ) + __appendval( msg.params, field.name, field.file ) + + store = filecb + elseif field.name then + __initval( msg.params, field.name ) + + store = function( hdr, buf, eof ) + __appendval( msg.params, field.name, buf ) + end + else + store = nil + end + + return chunk, true + end + + return chunk, false + end + + local function snk( chunk ) + + tlen = tlen + ( chunk and #chunk or 0 ) + + if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then + return nil, "Message body size exceeds Content-Length" + end + + if chunk and not lchunk then + lchunk = "\r\n" .. chunk + + elseif lchunk then + local data = lchunk .. ( chunk or "" ) + local spos, epos, found + + repeat + spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "\r\n", 1, true ) + + if not spos then + spos, epos = data:find( "\r\n--" .. msg.mime_boundary .. "--\r\n", 1, true ) + end + + + if spos then + local predata = data:sub( 1, spos - 1 ) + + if inhdr then + predata, eof = parse_headers( predata, field ) + + if not eof then + return nil, "Invalid MIME section header" + elseif not field.name then + return nil, "Invalid Content-Disposition header" + end + end + + if store then + store( field, predata, true ) + end + + + field = { headers = { } } + found = found or true + + data, eof = parse_headers( data:sub( epos + 1, #data ), field ) + inhdr = not eof + end + until not spos + + if found then + -- We found at least some boundary. Save + -- the unparsed remaining data for the + -- next chunk. + lchunk, data = data, nil + else + -- There was a complete chunk without a boundary. Parse it as headers or + -- append it as data, depending on our current state. + if inhdr then + lchunk, eof = parse_headers( data, field ) + inhdr = not eof + else + -- We're inside data, so append the data. Note that we only append + -- lchunk, not all of data, since there is a chance that chunk + -- contains half a boundary. Assuming that each chunk is at least the + -- boundary in size, this should prevent problems + store( field, lchunk, false ) + lchunk, chunk = chunk, nil + end + end + end + + return true + end + + return ltn12.pump.all( src, snk ) +end + +--- Decode an urlencoded http message body with application/x-www-urlencoded +-- Content-Type. Stores all extracted data associated with its parameter name +-- in the params table withing the given message object. Multiple parameter +-- values are stored as tables, ordinary ones as strings. +-- @param src Ltn12 source function +-- @param msg HTTP message object +-- @return Value indicating successful operation (not nil means "ok") +-- @return String containing the error if unsuccessful +-- @see parse_message_header +function urldecode_message_body( src, msg ) + + local tlen = 0 + local lchunk = nil + + local function snk( chunk ) + + tlen = tlen + ( chunk and #chunk or 0 ) + + if msg.env.CONTENT_LENGTH and tlen > tonumber(msg.env.CONTENT_LENGTH) + 2 then + return nil, "Message body size exceeds Content-Length" + elseif tlen > HTTP_MAX_CONTENT then + return nil, "Message body size exceeds maximum allowed length" + end + + if not lchunk and chunk then + lchunk = chunk + + elseif lchunk then + local data = lchunk .. ( chunk or "&" ) + local spos, epos + + repeat + spos, epos = data:find("^.-[;&]") + + if spos then + local pair = data:sub( spos, epos - 1 ) + local key = pair:match("^(.-)=") + local val = pair:match("=([^%s]*)%s*$") + + if key and #key > 0 then + __initval( msg.params, key ) + __appendval( msg.params, key, val ) + __finishval( msg.params, key, urldecode ) + end + + data = data:sub( epos + 1, #data ) + end + until not spos + + lchunk = data + end + + return true + end + + return ltn12.pump.all( src, snk ) +end + +--- Try to extract an http message header including information like protocol +-- version, message headers and resulting CGI environment variables from the +-- given ltn12 source. +-- @param src Ltn12 source function +-- @return HTTP message object +-- @see parse_message_body +function parse_message_header( src ) + + local ok = true + local msg = { } + + local sink = ltn12.sink.simplify( + function( chunk ) + return process_states['magic']( msg, chunk ) + end + ) + + -- Pump input data... + while ok do + + -- get data + ok, err = ltn12.pump.step( src, sink ) + + -- error + if not ok and err then + return nil, err + + -- eof + elseif not ok then + + -- Process get parameters + if ( msg.request_method == "get" or msg.request_method == "post" ) and + msg.request_uri:match("?") + then + msg.params = urldecode_params( msg.request_uri ) + else + msg.params = { } + end + + -- Populate common environment variables + msg.env = { + CONTENT_LENGTH = msg.headers['Content-Length']; + CONTENT_TYPE = msg.headers['Content-Type'] or msg.headers['Content-type']; + REQUEST_METHOD = msg.request_method:upper(); + REQUEST_URI = msg.request_uri; + SCRIPT_NAME = msg.request_uri:gsub("?.+$",""); + SCRIPT_FILENAME = ""; -- XXX implement me + SERVER_PROTOCOL = "HTTP/" .. string.format("%.1f", msg.http_version); + QUERY_STRING = msg.request_uri:match("?") + and msg.request_uri:gsub("^.+?","") or "" + } + + -- Populate HTTP_* environment variables + for i, hdr in ipairs( { + 'Accept', + 'Accept-Charset', + 'Accept-Encoding', + 'Accept-Language', + 'Connection', + 'Cookie', + 'Host', + 'Referer', + 'User-Agent', + } ) do + local var = 'HTTP_' .. hdr:upper():gsub("%-","_") + local val = msg.headers[hdr] + + msg.env[var] = val + end + end + end + + return msg +end + +--- Try to extract and decode a http message body from the given ltn12 source. +-- This function will examine the Content-Type within the given message object +-- to select the appropriate content decoder. +-- Currently the application/x-www-urlencoded and application/form-data +-- mime types are supported. If the encountered content encoding can't be +-- handled then the whole message body will be stored unaltered as "content" +-- property within the given message object. +-- @param src Ltn12 source function +-- @param msg HTTP message object +-- @param filecb File data callback (optional, see mimedecode_message_body()) +-- @return Value indicating successful operation (not nil means "ok") +-- @return String containing the error if unsuccessful +-- @see parse_message_header +function parse_message_body( src, msg, filecb ) + -- Is it multipart/mime ? + if msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and + msg.env.CONTENT_TYPE:match("^multipart/form%-data") + then + + return mimedecode_message_body( src, msg, filecb ) + + -- Is it application/x-www-form-urlencoded ? + elseif msg.env.REQUEST_METHOD == "POST" and msg.env.CONTENT_TYPE and + msg.env.CONTENT_TYPE:match("^application/x%-www%-form%-urlencoded") + then + return urldecode_message_body( src, msg, filecb ) + + + -- Unhandled encoding + -- If a file callback is given then feed it chunk by chunk, else + -- store whole buffer in message.content + else + + local sink + + -- If we have a file callback then feed it + if type(filecb) == "function" then + sink = filecb + + -- ... else append to .content + else + msg.content = "" + msg.content_length = 0 + + sink = function( chunk, err ) + if chunk then + if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then + msg.content = msg.content .. chunk + msg.content_length = msg.content_length + #chunk + return true + else + return nil, "POST data exceeds maximum allowed length" + end + end + return true + end + end + + -- Pump data... + while true do + local ok, err = ltn12.pump.step( src, sink ) + + if not ok and err then + return nil, err + elseif not err then + return true + end + end + + return true + end +end + +--- Table containing human readable messages for several http status codes. +-- @class table +statusmsg = { + [200] = "OK", + [206] = "Partial Content", + [301] = "Moved Permanently", + [302] = "Found", + [304] = "Not Modified", + [400] = "Bad Request", + [403] = "Forbidden", + [404] = "Not Found", + [405] = "Method Not Allowed", + [408] = "Request Time-out", + [411] = "Length Required", + [412] = "Precondition Failed", + [416] = "Requested range not satisfiable", + [500] = "Internal Server Error", + [503] = "Server Unavailable", +} diff --git a/modules/base/luasrc/luasrc/http/protocol/conditionals.lua b/modules/base/luasrc/luasrc/http/protocol/conditionals.lua new file mode 100644 index 0000000000..75e1f7b37c --- /dev/null +++ b/modules/base/luasrc/luasrc/http/protocol/conditionals.lua @@ -0,0 +1,153 @@ +--[[ + +HTTP protocol implementation for LuCI - RFC2616 / 14.19, 14.24 - 14.28 +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +--- LuCI http protocol implementation - HTTP/1.1 bits. +-- This class provides basic ETag handling and implements most of the +-- conditional HTTP/1.1 headers specified in RFC2616 Sct. 14.24 - 14.28 . +module("luci.http.protocol.conditionals", package.seeall) + +local date = require("luci.http.protocol.date") + + +--- Implement 14.19 / ETag. +-- @param stat A file.stat structure +-- @return String containing the generated tag suitable for ETag headers +function mk_etag( stat ) + if stat ~= nil then + return string.format( '"%x-%x-%x"', stat.ino, stat.size, stat.mtime ) + end +end + +--- 14.24 / If-Match +-- Test whether the given message object contains an "If-Match" header and +-- compare it against the given stat object. +-- @param req HTTP request message object +-- @param stat A file.stat object +-- @return Boolean indicating whether the precondition is ok +-- @return Alternative status code if the precondition failed +function if_match( req, stat ) + local h = req.headers + local etag = mk_etag( stat ) + + -- Check for matching resource + if type(h['If-Match']) == "string" then + for ent in h['If-Match']:gmatch("([^, ]+)") do + if ( ent == '*' or ent == etag ) and stat ~= nil then + return true + end + end + + return false, 412 + end + + return true +end + +--- 14.25 / If-Modified-Since +-- Test whether the given message object contains an "If-Modified-Since" header +-- and compare it against the given stat object. +-- @param req HTTP request message object +-- @param stat A file.stat object +-- @return Boolean indicating whether the precondition is ok +-- @return Alternative status code if the precondition failed +-- @return Table containing extra HTTP headers if the precondition failed +function if_modified_since( req, stat ) + local h = req.headers + + -- Compare mtimes + if type(h['If-Modified-Since']) == "string" then + local since = date.to_unix( h['If-Modified-Since'] ) + + if stat == nil or since < stat.mtime then + return true + end + + return false, 304, { + ["ETag"] = mk_etag( stat ); + ["Date"] = date.to_http( os.time() ); + ["Last-Modified"] = date.to_http( stat.mtime ) + } + end + + return true +end + +--- 14.26 / If-None-Match +-- Test whether the given message object contains an "If-None-Match" header and +-- compare it against the given stat object. +-- @param req HTTP request message object +-- @param stat A file.stat object +-- @return Boolean indicating whether the precondition is ok +-- @return Alternative status code if the precondition failed +-- @return Table containing extra HTTP headers if the precondition failed +function if_none_match( req, stat ) + local h = req.headers + local etag = mk_etag( stat ) + local method = req.env and req.env.REQUEST_METHOD or "GET" + + -- Check for matching resource + if type(h['If-None-Match']) == "string" then + for ent in h['If-None-Match']:gmatch("([^, ]+)") do + if ( ent == '*' or ent == etag ) and stat ~= nil then + if method == "GET" or method == "HEAD" then + return false, 304, { + ["ETag"] = etag; + ["Date"] = date.to_http( os.time() ); + ["Last-Modified"] = date.to_http( stat.mtime ) + } + else + return false, 412 + end + end + end + end + + return true +end + +--- 14.27 / If-Range +-- The If-Range header is currently not implemented due to the lack of general +-- byte range stuff in luci.http.protocol . This function will always return +-- false, 412 to indicate a failed precondition. +-- @param req HTTP request message object +-- @param stat A file.stat object +-- @return Boolean indicating whether the precondition is ok +-- @return Alternative status code if the precondition failed +function if_range( req, stat ) + -- Sorry, no subranges (yet) + return false, 412 +end + +--- 14.28 / If-Unmodified-Since +-- Test whether the given message object contains an "If-Unmodified-Since" +-- header and compare it against the given stat object. +-- @param req HTTP request message object +-- @param stat A file.stat object +-- @return Boolean indicating whether the precondition is ok +-- @return Alternative status code if the precondition failed +function if_unmodified_since( req, stat ) + local h = req.headers + + -- Compare mtimes + if type(h['If-Unmodified-Since']) == "string" then + local since = date.to_unix( h['If-Unmodified-Since'] ) + + if stat ~= nil and since <= stat.mtime then + return false, 412 + end + end + + return true +end diff --git a/modules/base/luasrc/luasrc/http/protocol/date.lua b/modules/base/luasrc/luasrc/http/protocol/date.lua new file mode 100644 index 0000000000..83d11e2c25 --- /dev/null +++ b/modules/base/luasrc/luasrc/http/protocol/date.lua @@ -0,0 +1,115 @@ +--[[ + +HTTP protocol implementation for LuCI - date handling +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +--- LuCI http protocol implementation - date helper class. +-- This class contains functions to parse, compare and format http dates. +module("luci.http.protocol.date", package.seeall) + +require("luci.sys.zoneinfo") + + +MONTHS = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", + "Sep", "Oct", "Nov", "Dec" +} + +--- Return the time offset in seconds between the UTC and given time zone. +-- @param tz Symbolic or numeric timezone specifier +-- @return Time offset to UTC in seconds +function tz_offset(tz) + + if type(tz) == "string" then + + -- check for a numeric identifier + local s, v = tz:match("([%+%-])([0-9]+)") + if s == '+' then s = 1 else s = -1 end + if v then v = tonumber(v) end + + if s and v then + return s * 60 * ( math.floor( v / 100 ) * 60 + ( v % 100 ) ) + + -- lookup symbolic tz + elseif luci.sys.zoneinfo.OFFSET[tz:lower()] then + return luci.sys.zoneinfo.OFFSET[tz:lower()] + end + + end + + -- bad luck + return 0 +end + +--- Parse given HTTP date string and convert it to unix epoch time. +-- @param data String containing the date +-- @return Unix epoch time +function to_unix(date) + + local wd, day, mon, yr, hr, min, sec, tz = date:match( + "([A-Z][a-z][a-z]), ([0-9]+) " .. + "([A-Z][a-z][a-z]) ([0-9]+) " .. + "([0-9]+):([0-9]+):([0-9]+) " .. + "([A-Z0-9%+%-]+)" + ) + + if day and mon and yr and hr and min and sec then + -- find month + local month = 1 + for i = 1, 12 do + if MONTHS[i] == mon then + month = i + break + end + end + + -- convert to epoch time + return tz_offset(tz) + os.time( { + year = yr, + month = month, + day = day, + hour = hr, + min = min, + sec = sec + } ) + end + + return 0 +end + +--- Convert the given unix epoch time to valid HTTP date string. +-- @param time Unix epoch time +-- @return String containing the formatted date +function to_http(time) + return os.date( "%a, %d %b %Y %H:%M:%S GMT", time ) +end + +--- Compare two dates which can either be unix epoch times or HTTP date strings. +-- @param d1 The first date or epoch time to compare +-- @param d2 The first date or epoch time to compare +-- @return -1 - if d1 is lower then d2 +-- @return 0 - if both dates are equal +-- @return 1 - if d1 is higher then d2 +function compare(d1, d2) + + if d1:match("[^0-9]") then d1 = to_unix(d1) end + if d2:match("[^0-9]") then d2 = to_unix(d2) end + + if d1 == d2 then + return 0 + elseif d1 < d2 then + return -1 + else + return 1 + end +end diff --git a/modules/base/luasrc/luasrc/http/protocol/mime.lua b/modules/base/luasrc/luasrc/http/protocol/mime.lua new file mode 100644 index 0000000000..c878160664 --- /dev/null +++ b/modules/base/luasrc/luasrc/http/protocol/mime.lua @@ -0,0 +1,99 @@ +--[[ + +HTTP protocol implementation for LuCI - mime handling +(c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +--- LuCI http protocol implementation - mime helper class. +-- This class provides functions to guess mime types from file extensions and +-- vice versa. +module("luci.http.protocol.mime", package.seeall) + +require("luci.util") + +--- MIME mapping table containg extension - mimetype relations. +-- @class table +MIME_TYPES = { + ["txt"] = "text/plain"; + ["js"] = "text/javascript"; + ["css"] = "text/css"; + ["htm"] = "text/html"; + ["html"] = "text/html"; + ["patch"] = "text/x-patch"; + ["c"] = "text/x-csrc"; + ["h"] = "text/x-chdr"; + ["o"] = "text/x-object"; + ["ko"] = "text/x-object"; + + ["bmp"] = "image/bmp"; + ["gif"] = "image/gif"; + ["png"] = "image/png"; + ["jpg"] = "image/jpeg"; + ["jpeg"] = "image/jpeg"; + ["svg"] = "image/svg+xml"; + + ["zip"] = "application/zip"; + ["pdf"] = "application/pdf"; + ["xml"] = "application/xml"; + ["xsl"] = "application/xml"; + ["doc"] = "application/msword"; + ["ppt"] = "application/vnd.ms-powerpoint"; + ["xls"] = "application/vnd.ms-excel"; + ["odt"] = "application/vnd.oasis.opendocument.text"; + ["odp"] = "application/vnd.oasis.opendocument.presentation"; + ["pl"] = "application/x-perl"; + ["sh"] = "application/x-shellscript"; + ["php"] = "application/x-php"; + ["deb"] = "application/x-deb"; + ["iso"] = "application/x-cd-image"; + ["tgz"] = "application/x-compressed-tar"; + + ["mp3"] = "audio/mpeg"; + ["ogg"] = "audio/x-vorbis+ogg"; + ["wav"] = "audio/x-wav"; + + ["mpg"] = "video/mpeg"; + ["mpeg"] = "video/mpeg"; + ["avi"] = "video/x-msvideo"; +} + +--- Extract extension from a filename and return corresponding mime-type or +-- "application/octet-stream" if the extension is unknown. +-- @param filename The filename for which the mime type is guessed +-- @return String containign the determined mime type +function to_mime(filename) + if type(filename) == "string" then + local ext = filename:match("[^%.]+$") + + if ext and MIME_TYPES[ext:lower()] then + return MIME_TYPES[ext:lower()] + end + end + + return "application/octet-stream" +end + +--- Return corresponding extension for a given mime type or nil if the +-- given mime-type is unknown. +-- @param mimetype The mimetype to retrieve the extension from +-- @return String with the extension or nil for unknown type +function to_ext(mimetype) + if type(mimetype) == "string" then + for ext, type in luci.util.kspairs( MIME_TYPES ) do + if type == mimetype then + return ext + end + end + end + + return nil +end diff --git a/modules/base/luasrc/luasrc/i18n.lua b/modules/base/luasrc/luasrc/i18n.lua new file mode 100644 index 0000000000..545a8aed93 --- /dev/null +++ b/modules/base/luasrc/luasrc/i18n.lua @@ -0,0 +1,104 @@ +--[[ +LuCI - Internationalisation + +Description: +A very minimalistic but yet effective internationalisation module + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +--- LuCI translation library. +module("luci.i18n", package.seeall) +require("luci.util") + +local tparser = require "luci.template.parser" + +table = {} +i18ndir = luci.util.libpath() .. "/i18n/" +loaded = {} +context = luci.util.threadlocal() +default = "en" + +--- Clear the translation table. +function clear() +end + +--- Load a translation and copy its data into the translation table. +-- @param file Language file +-- @param lang Two-letter language code +-- @param force Force reload even if already loaded (optional) +-- @return Success status +function load(file, lang, force) +end + +--- Load a translation file using the default translation language. +-- Alternatively load the translation of the fallback language. +-- @param file Language file +-- @param force Force reload even if already loaded (optional) +function loadc(file, force) +end + +--- Set the context default translation language. +-- @param lang Two-letter language code +function setlanguage(lang) + context.lang = lang:gsub("_", "-") + context.parent = (context.lang:match("^([a-z][a-z])_")) + if not tparser.load_catalog(context.lang, i18ndir) then + if context.parent then + tparser.load_catalog(context.parent, i18ndir) + return context.parent + end + end + return context.lang +end + +--- Return the translated value for a specific translation key. +-- @param key Default translation text +-- @return Translated string +function translate(key) + return tparser.translate(key) or key +end + +--- Return the translated value for a specific translation key and use it as sprintf pattern. +-- @param key Default translation text +-- @param ... Format parameters +-- @return Translated and formatted string +function translatef(key, ...) + return tostring(translate(key)):format(...) +end + +--- Return the translated value for a specific translation key +-- and ensure that the returned value is a Lua string value. +-- This is the same as calling <code>tostring(translate(...))</code> +-- @param key Default translation text +-- @return Translated string +function string(key) + return tostring(translate(key)) +end + +--- Return the translated value for a specific translation key and use it as sprintf pattern. +-- Ensure that the returned value is a Lua string value. +-- This is the same as calling <code>tostring(translatef(...))</code> +-- @param key Default translation text +-- @param ... Format parameters +-- @return Translated and formatted string +function stringf(key, ...) + return tostring(translate(key)):format(...) +end diff --git a/modules/base/luasrc/luasrc/sauth.lua b/modules/base/luasrc/luasrc/sauth.lua new file mode 100644 index 0000000000..32f172dcda --- /dev/null +++ b/modules/base/luasrc/luasrc/sauth.lua @@ -0,0 +1,127 @@ +--[[ + +Session authentication +(c) 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +--- LuCI session library. +module("luci.sauth", package.seeall) +require("luci.util") +require("luci.sys") +require("luci.config") +local nixio = require "nixio", require "nixio.util" +local fs = require "nixio.fs" + + +luci.config.sauth = luci.config.sauth or {} +sessionpath = luci.config.sauth.sessionpath +sessiontime = tonumber(luci.config.sauth.sessiontime) or 15 * 60 + +--- Prepare session storage by creating the session directory. +function prepare() + fs.mkdir(sessionpath, 700) + if not sane() then + error("Security Exception: Session path is not sane!") + end +end + +local function _read(id) + local blob = fs.readfile(sessionpath .. "/" .. id) + return blob +end + +local function _write(id, data) + local f = nixio.open(sessionpath .. "/" .. id, "w", 600) + f:writeall(data) + f:close() +end + +local function _checkid(id) + return not not (id and #id == 32 and id:match("^[a-fA-F0-9]+$")) +end + +--- Write session data to a session file. +-- @param id Session identifier +-- @param data Session data table +function write(id, data) + if not sane() then + prepare() + end + + assert(_checkid(id), "Security Exception: Session ID is invalid!") + assert(type(data) == "table", "Security Exception: Session data invalid!") + + data.atime = luci.sys.uptime() + + _write(id, luci.util.get_bytecode(data)) +end + +--- Read a session and return its content. +-- @param id Session identifier +-- @return Session data table or nil if the given id is not found +function read(id) + if not id or #id == 0 then + return nil + end + + assert(_checkid(id), "Security Exception: Session ID is invalid!") + + if not sane(sessionpath .. "/" .. id) then + return nil + end + + local blob = _read(id) + local func = loadstring(blob) + setfenv(func, {}) + + local sess = func() + assert(type(sess) == "table", "Session data invalid!") + + if sess.atime and sess.atime + sessiontime < luci.sys.uptime() then + kill(id) + return nil + end + + -- refresh atime in session + write(id, sess) + + return sess +end + +--- Check whether Session environment is sane. +-- @return Boolean status +function sane(file) + return luci.sys.process.info("uid") + == fs.stat(file or sessionpath, "uid") + and fs.stat(file or sessionpath, "modestr") + == (file and "rw-------" or "rwx------") +end + +--- Kills a session +-- @param id Session identifier +function kill(id) + assert(_checkid(id), "Security Exception: Session ID is invalid!") + fs.unlink(sessionpath .. "/" .. id) +end + +--- Remove all expired session data files +function reap() + if sane() then + local id + for id in nixio.fs.dir(sessionpath) do + if _checkid(id) then + -- reading the session will kill it if it is expired + read(id) + end + end + end +end diff --git a/modules/base/luasrc/luasrc/template.lua b/modules/base/luasrc/luasrc/template.lua new file mode 100644 index 0000000000..72127d1df1 --- /dev/null +++ b/modules/base/luasrc/luasrc/template.lua @@ -0,0 +1,107 @@ +--[[ +LuCI - Template Parser + +Description: +A template parser supporting includes, translations, Lua code blocks +and more. It can be used either as a compiler or as an interpreter. + +FileId: $Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +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 + + +-- 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) + + self.template = self.cache[name] + self.name = name + + -- 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 = viewdir .. "/" .. name .. ".htm" + + self.template, _, err = tparser.parse(sourcefile) + + -- If we have no valid template throw error, otherwise cache the template + if not self.template then + error("Failed to load template '" .. name .. "'.\n" .. + "Error while parsing template '" .. sourcefile .. "':\n" .. + (err or "Unknown syntax error")) + else + 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 occured: " .. tostring(err or "(nil)")) + end +end diff --git a/modules/base/luasrc/luasrc/view/cbi/apply_xhr.htm b/modules/base/luasrc/luasrc/view/cbi/apply_xhr.htm new file mode 100644 index 0000000000..1814c9393b --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/apply_xhr.htm @@ -0,0 +1,43 @@ +<% export("cbi_apply_xhr", function(id, configs, redirect) -%> +<fieldset class="cbi-section" id="cbi-apply-<%=id%>"> + <legend><%:Applying changes%></legend> + <script type="text/javascript">//<![CDATA[ + var apply_xhr = new XHR(); + + apply_xhr.get('<%=luci.dispatcher.build_url("servicectl", "restart", table.concat(configs, ","))%>', null, + function() { + var checkfinish = function() { + apply_xhr.get('<%=luci.dispatcher.build_url("servicectl", "status")%>', null, + function(x) { + if( x.responseText == 'finish' ) + { + var e = document.getElementById('cbi-apply-<%=id%>-status'); + if( e ) + { + e.innerHTML = '<%:Configuration applied.%>'; + window.setTimeout(function() { + e.parentNode.style.display = 'none'; + <% if redirect then %>location.href='<%=redirect%>';<% end %> + }, 1000); + } + } + else + { + var e = document.getElementById('cbi-apply-<%=id%>-status'); + if( e && x.responseText ) e.innerHTML = x.responseText; + + window.setTimeout(checkfinish, 1000); + } + } + ); + } + + window.setTimeout(checkfinish, 1000); + } + ); + //]]></script> + + <img src="<%=resource%>/icons/loading.gif" alt="<%:Loading%>" style="vertical-align:middle" /> + <span id="cbi-apply-<%=id%>-status"><%:Waiting for changes to be applied...%></span> +</fieldset> +<%- end) %> diff --git a/modules/base/luasrc/luasrc/view/cbi/browser.htm b/modules/base/luasrc/luasrc/view/cbi/browser.htm new file mode 100644 index 0000000000..e4a4077d55 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/browser.htm @@ -0,0 +1,7 @@ +<% local v = self:cfgvalue(section) -%> +<%+cbi/valueheader%> + <input class="cbi-input-text" type="text"<%= attr("value", v) .. attr("name", cbid) .. attr("id", cbid) %> /> + <script type="text/javascript"> +cbi_browser_init('<%=cbid%>', '<%=resource%>', '<%=luci.dispatcher.build_url("admin", "filebrowser")%>'<%=self.default_path and ", '"..self.default_path.."'"%>); + </script> +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/button.htm b/modules/base/luasrc/luasrc/view/cbi/button.htm new file mode 100644 index 0000000000..30f8ddfda5 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/button.htm @@ -0,0 +1,7 @@ +<%+cbi/valueheader%> + <% if self:cfgvalue(section) ~= false then %> + <input class="cbi-button cbi-input-<%=self.inputstyle or "button" %>" type="submit"<%= attr("name", cbid) .. attr("id", cbid) .. attr("value", self.inputtitle or self.title)%> /> + <% else %> + - + <% end %> +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/cell_valuefooter.htm b/modules/base/luasrc/luasrc/view/cbi/cell_valuefooter.htm new file mode 100644 index 0000000000..220ebd42ba --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/cell_valuefooter.htm @@ -0,0 +1,20 @@ +</div> +<div id="cbip-<%=self.config.."-"..section.."-"..self.option%>"></div> +</td> + +<% if #self.deps > 0 then -%> + <script type="text/javascript"> + <% for j, d in ipairs(self.deps) do -%> + cbi_d_add("cbi-<%=self.config.."-"..section.."-"..self.option..d.add%>", { + <%- + for k,v in pairs(d.deps) do + -%> + <%-=string.format('"cbid.%s.%s.%s"', self.config, section, k) .. ":" .. string.format("%q", v)-%> + <%-if next(d.deps, k) then-%>,<%-end-%> + <%- + end + -%> + }, "cbip-<%=self.config.."-"..section.."-"..self.option%>"); + <%- end %> + </script> +<%- end %> diff --git a/modules/base/luasrc/luasrc/view/cbi/cell_valueheader.htm b/modules/base/luasrc/luasrc/view/cbi/cell_valueheader.htm new file mode 100644 index 0000000000..9e2e145ddb --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/cell_valueheader.htm @@ -0,0 +1,2 @@ +<td class="cbi-value-field<% if self.error and self.error[section] then %> cbi-value-error<% end %>"> +<div id="cbi-<%=self.config.."-"..section.."-"..self.option%>"> diff --git a/modules/base/luasrc/luasrc/view/cbi/compound.htm b/modules/base/luasrc/luasrc/view/cbi/compound.htm new file mode 100644 index 0000000000..12d02bb1d8 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/compound.htm @@ -0,0 +1 @@ +<%- self:render_children() %> diff --git a/modules/base/luasrc/luasrc/view/cbi/delegator.htm b/modules/base/luasrc/luasrc/view/cbi/delegator.htm new file mode 100644 index 0000000000..4fd19265d8 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/delegator.htm @@ -0,0 +1,24 @@ +<%- self.active:render() %> + <div class="cbi-page-actions"> + <input type="hidden" name="cbi.delg.current" value="<%=self.current%>" /> +<% for _, x in ipairs(self.chain) do %> + <input type="hidden" name="cbi.delg.path" value="<%=x%>" /> +<% end %> +<% if not self.disallow_pageactions then %> +<% if self.allow_finish and not self:get_next(self.current) then %> + <input class="cbi-button cbi-button-finish" type="submit" value="<%:Finish%>" /> +<% elseif self:get_next(self.current) then %> + <input class="cbi-button cbi-button-next" type="submit" value="<%:Next »%>" /> +<% end %> +<% if self.allow_cancel then %> + <input class="cbi-button cbi-button-cancel" type="submit" name="cbi.cancel" value="<%:Cancel%>" /> +<% end %> +<% if self.allow_reset then %> + <input class="cbi-button cbi-button-reset" type="reset" value="<%:Reset%>" /> +<% end %> +<% if self.allow_back and self:get_prev(self.current) then %> + <input class="cbi-button cbi-button-back" type="submit" name="cbi.delg.back" value="<%:« Back%>" /> +<% end %> +<% end %> + <script type="text/javascript">cbi_d_update();</script> + </div> diff --git a/modules/base/luasrc/luasrc/view/cbi/dvalue.htm b/modules/base/luasrc/luasrc/view/cbi/dvalue.htm new file mode 100644 index 0000000000..78e6f323d7 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/dvalue.htm @@ -0,0 +1,13 @@ +<%+cbi/valueheader%> +<% if self.href then %><a href="<%=self.href%>"><% end -%> + <% + local val = self:cfgvalue(section) or self.default or "" + if not self.rawhtml then + write(pcdata(val)) + else + write(val) + end + %> +<%- if self.href then %></a><%end%> +<input type="hidden" id="<%=cbid%>" value="<%=pcdata(self:cfgvalue(section) or self.default or "")%>" /> +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/dynlist.htm b/modules/base/luasrc/luasrc/view/cbi/dynlist.htm new file mode 100644 index 0000000000..fd626a4ecf --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/dynlist.htm @@ -0,0 +1,26 @@ +<%+cbi/valueheader%> +<div> +<% + local vals = self:cfgvalue(section) or {} + for i=1, #vals + 1 do + local val = vals[i] + if (val and #val > 0) or (i == 1) then +%> + <input class="cbi-input-text" value="<%=pcdata(val)%>" onchange="cbi_d_update(this.id)" type="text"<%= + attr("id", cbid .. "." .. i) .. attr("name", cbid) .. ifattr(self.size, "size") .. + ifattr(i == 1 and self.placeholder, "placeholder", self.placeholder) + %> /><br /> +<% end end %> +</div> +<script type="text/javascript"> +cbi_dynlist_init( + '<%=cbid%>', '<%=resource%>', '<%=self.datatype%>', + <%=tostring(self.optional or self.rmempty)%> + <%- if #self.keylist > 0 then -%>, [{ + <%- for i, k in ipairs(self.keylist) do -%> + <%-=string.format("%q", k) .. ":" .. string.format("%q", self.vallist[i])-%> + <%-if i<#self.keylist then-%>,<%-end-%> + <%- end -%> + }, '<%: -- custom -- %>']<% end -%>); +</script> +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/filebrowser.htm b/modules/base/luasrc/luasrc/view/cbi/filebrowser.htm new file mode 100644 index 0000000000..a79beebba7 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/filebrowser.htm @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<head> + <title>Filebrowser - LuCI</title> + <style type="text/css"> + #path, #listing { + font-size: 85%; + } + + ul { + padding-left: 0; + list-style-type: none; + } + + li img { + vertical-align: bottom; + margin-right: 0.2em; + } + </style> + + <script type="text/javascript"> + function callback(path) { + if( window.opener ) { + var input = window.opener.document.getElementById('<%=luci.http.formvalue('field')%>'); + if( input ) { + input.value = path; + window.close(); + } + } + } + </script> +</head> +<body> + <% + require("nixio.fs") + require("nixio.util") + require("luci.http") + require("luci.dispatcher") + + local field = luci.http.formvalue('field') + local request = luci.dispatcher.context.args + local path = { '' } + + for i = 1, #request do + if request[i] ~= '..' and #request[i] > 0 then + path[#path+1] = request[i] + end + end + + local filepath = table.concat( path, '/' ) + local filestat = nixio.fs.stat( filepath ) + local baseurl = luci.dispatcher.build_url('admin', 'filebrowser') + + if filestat and filestat.type == "reg" then + table.remove( path, #path ) + filepath = table.concat( path, '/' ) .. '/' + elseif not ( filestat and filestat.type == "dir" ) then + path = { '' } + filepath = '/' + else + filepath = filepath .. '/' + end + + local entries = nixio.util.consume((nixio.fs.dir(filepath))) + -%> + <div id="path"> + Location: + <% for i, dir in ipairs(path) do %> + <% if i == 1 then %> + <a href="<%=baseurl%>?field=<%=field%>">(root)</a> + <% elseif next(path, i) then %> + <% baseurl = baseurl .. '/' .. dir %> + / <a href="<%=baseurl%>?field=<%=field%>"><%=dir%></a> + <% else %> + <% baseurl = baseurl .. '/' .. dir %> + / <%=dir%> + <% end %> + <% end %> + </div> + + <hr /> + + <div id="listing"> + <ul> + <% for _, e in luci.util.vspairs(entries) do + local stat = nixio.fs.stat(filepath..e) + if stat and stat.type == 'dir' then + -%> + <li class="dir"> + <img src="<%=resource%>/cbi/folder.gif" alt="<%:Directory%>" /> + <a href="<%=baseurl%>/<%=e%>?field=<%=field%>"><%=e%>/</a> + </li> + <% end end -%> + + <% for _, e in luci.util.vspairs(entries) do + local stat = nixio.fs.stat(filepath..e) + if stat and stat.type ~= 'dir' then + -%> + <li class="file"> + <img src="<%=resource%>/cbi/file.gif" alt="<%:File%>" /> + <a href="#" onclick="callback('<%=filepath..e%>')"><%=e%></a> + </li> + <% end end -%> + </ul> + </div> +</body> +</html> diff --git a/modules/base/luasrc/luasrc/view/cbi/firewall_zoneforwards.htm b/modules/base/luasrc/luasrc/view/cbi/firewall_zoneforwards.htm new file mode 100644 index 0000000000..2a433b5696 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/firewall_zoneforwards.htm @@ -0,0 +1,59 @@ +<%+cbi/valueheader%> + +<%- + local utl = require "luci.util" + local fwm = require "luci.model.firewall".init() + local nwm = require "luci.model.network".init() + + local zone, fwd, fz + local value = self:formvalue(section) + if not value or value == "-" then + value = self:cfgvalue(section) or self.default + end + + local def = fwm:get_defaults() + local zone = fwm:get_zone(value) + local empty = true +-%> + +<% if zone then %> +<div style="white-space:nowrap"> + <label class="zonebadge" style="background-color:<%=zone:get_color()%>"> + <strong><%=zone:name()%>:</strong> + <%- + local zempty = true + for _, net in ipairs(zone:get_networks()) do + net = nwm:get_network(net) + if net then + zempty = false + -%> + <span class="ifacebadge<% if net:name() == self.network then %> ifacebadge-active<% end %>"><%=net:name()%>: + <% + local nempty = true + for _, iface in ipairs(net:is_bridge() and net:get_interfaces() or { net:get_interface() }) do + nempty = false + %> + <img<%=attr("title", iface:get_i18n())%> style="width:16px; height:16px; vertical-align:middle" src="<%=resource%>/icons/<%=iface:type()%><%=iface:is_up() and "" or "_disabled"%>.png" /> + <% end %> + <% if nempty then %><em><%:(empty)%></em><% end %> + </span> + <%- end end -%> + <%- if zempty then %><em><%:(empty)%></em><% end -%> + </label> +  ⇒  + <% for _, fwd in ipairs(zone:get_forwardings_by("src")) do + fz = fwd:dest_zone() + empty = false %> + <label class="zonebadge" style="background-color:<%=fz:get_color()%>"> + <strong><%=fz:name()%></strong> + </label>  + <% end %> + <% if empty then %> + <label class="zonebadge zonebadge-empty"> + <strong><%=zone:forward():upper()%></strong> + </label> + <% end %> +</div> +<% end %> + +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/firewall_zonelist.htm b/modules/base/luasrc/luasrc/view/cbi/firewall_zonelist.htm new file mode 100644 index 0000000000..7973437f40 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/firewall_zonelist.htm @@ -0,0 +1,89 @@ +<%+cbi/valueheader%> + +<%- + local utl = require "luci.util" + local fwm = require "luci.model.firewall".init() + local nwm = require "luci.model.network".init() + + local zone, net, iface + local zones = fwm:get_zones() + local value = self:formvalue(section) + if not value or value == "-" then + value = self:cfgvalue(section) or self.default + end + + local selected = false + local checked = { } + + for value in utl.imatch(value) do + checked[value] = true + end + + if not next(checked) then + checked[""] = true + end +-%> + +<ul style="margin:0; list-style-type:none; text-align:left"> + <% if self.allowlocal then %> + <li style="padding:0.5em"> + <input class="cbi-input-radio" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)"<%=attr("type", self.widget or "radio") .. attr("id", cbid .. "_empty") .. attr("name", cbid) .. attr("value", "") .. ifattr(checked[""], "checked", "checked")%> />   + <label<%=attr("for", cbid .. "_empty")%> style="background-color:<%=fwm.zone.get_color()%>" class="zonebadge"> + <strong><%:Device%></strong> + <% if self.allowany and self.allowlocal then %>(<%:input%>)<% end %> + </label> + </li> + <% end %> + <% if self.allowany then %> + <li style="padding:0.5em"> + <input class="cbi-input-radio" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)"<%=attr("type", self.widget or "radio") .. attr("id", cbid .. "_any") .. attr("name", cbid) .. attr("value", "*") .. ifattr(checked["*"], "checked", "checked")%> />   + <label<%=attr("for", cbid .. "_any")%> style="background-color:<%=fwm.zone.get_color()%>" class="zonebadge"> + <strong><%:Any zone%></strong> + <% if self.allowany and self.allowlocal then %>(<%:forward%>)<% end %> + </label> + </li> + <% end %> + <% + for _, zone in utl.spairs(zones, function(a,b) return (zones[a]:name() < zones[b]:name()) end) do + if zone:name() ~= self.exclude then + selected = selected or (value == zone:name()) + %> + <li style="padding:0.5em"> + <input class="cbi-input-radio" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)"<%=attr("type", self.widget or "radio") .. attr("id", cbid .. "." .. zone:name()) .. attr("name", cbid) .. attr("value", zone:name()) .. ifattr(checked[zone:name()], "checked", "checked")%> />   + <label<%=attr("for", cbid .. "." .. zone:name())%> style="background-color:<%=zone:get_color()%>" class="zonebadge"> + <strong><%=zone:name()%>:</strong> + <% + local zempty = true + for _, net in ipairs(zone:get_networks()) do + net = nwm:get_network(net) + if net then + zempty = false + %> + <span class="ifacebadge<% if net:name() == self.network then %> ifacebadge-active<% end %>"><%=net:name()%>: + <% + local nempty = true + for _, iface in ipairs(net:is_bridge() and net:get_interfaces() or { net:get_interface() }) do + nempty = false + %> + <img<%=attr("title", iface:get_i18n())%> style="width:16px; height:16px; vertical-align:middle" src="<%=resource%>/icons/<%=iface:type()%><%=iface:is_up() and "" or "_disabled"%>.png" /> + <% end %> + <% if nempty then %><em><%:(empty)%></em><% end %> + </span> + <% end end %> + <% if zempty then %><em><%:(empty)%></em><% end %> + </label> + </li> + <% end end %> + + <% if self.widget ~= "checkbox" and not self.nocreate then %> + <li style="padding:0.5em"> + <input class="cbi-input-radio" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)" type="radio"<%=attr("id", cbid .. "_new") .. attr("name", cbid) .. attr("value", "-") .. ifattr(not selected, "checked", "checked")%> />   + <div onclick="document.getElementById('<%=cbid%>_new').checked=true" class="zonebadge" style="background-color:<%=fwm.zone.get_color()%>"> + <em><%:unspecified -or- create:%> </em> + <input type="text"<%=attr("name", cbid .. ".newzone") .. ifattr(not selected, "value", luci.http.formvalue(cbid .. ".newzone") or self.default)%> onfocus="document.getElementById('<%=cbid%>_new').checked=true" /> + </div> + </li> + <% end %> +</ul> + +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/footer.htm b/modules/base/luasrc/luasrc/view/cbi/footer.htm new file mode 100644 index 0000000000..2c34028e58 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/footer.htm @@ -0,0 +1,26 @@ + <%- if pageaction then -%> + <div class="cbi-page-actions"> + <% if redirect then %> + <div style="float:left"> + <input class="cbi-button cbi-button-link" type="button" value="<%:Back to Overview%>" onclick="location.href='<%=pcdata(redirect)%>'" /> + </div> + <% end %> + + <% if flow.skip then %> + <input class="cbi-button cbi-button-skip" type="submit" name="cbi.skip" value="<%:Skip%>" /> + <% end %> + <% if not autoapply and not flow.hideapplybtn then %> + <input class="cbi-button cbi-button-apply" type="submit" name="cbi.apply" value="<%:Save & Apply%>" /> + <% end %> + <% if not flow.hidesavebtn then %> + <input class="cbi-button cbi-button-save" type="submit" value="<%:Save%>" /> + <% end %> + <% if not flow.hideresetbtn then %> + <input class="cbi-button cbi-button-reset" type="reset" value="<%:Reset%>" /> + <% end %> + + <script type="text/javascript">cbi_d_update();</script> + </div> + <%- end -%> +</form> +<%+footer%> diff --git a/modules/base/luasrc/luasrc/view/cbi/full_valuefooter.htm b/modules/base/luasrc/luasrc/view/cbi/full_valuefooter.htm new file mode 100644 index 0000000000..4876fbcc99 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/full_valuefooter.htm @@ -0,0 +1,59 @@ + <% if self.description and #self.description > 0 then -%> + <% if not luci.util.instanceof(self, luci.cbi.DynamicList) and (not luci.util.instanceof(self, luci.cbi.Flag) or self.orientation == "horizontal") then -%> + <br /> + <%- end %> + <div class="cbi-value-description"> + <span class="cbi-value-helpicon"><img src="<%=resource%>/cbi/help.gif" alt="<%:help%>" /></span> + <%=self.description%> + </div> + <%- end %> + <%- if self.title and #self.title > 0 then -%> + </div> + <%- end -%> +</div> + + +<% if #self.deps > 0 or #self.subdeps > 0 then -%> + <script type="text/javascript" id="cbip-<%=self.config.."-"..section.."-"..self.option%>"> + <% for j, d in ipairs(self.subdeps) do -%> + cbi_d_add("cbi-<%=self.config.."-"..section.."-"..self.option..d.add%>", { + <%- + for k,v in pairs(d.deps) do + local depk + if k:find("!", 1, true) then + depk = string.format('"%s"', k) + elseif k:find(".", 1, true) then + depk = string.format('"cbid.%s"', k) + else + depk = string.format('"cbid.%s.%s.%s"', self.config, section, k) + end + -%> + <%-= depk .. ":" .. string.format("%q", v)-%> + <%-if next(d.deps, k) then-%>,<%-end-%> + <%- + end + -%> + }, "cbip-<%=self.config.."-"..section.."-"..self.option..d.add%>"); + <%- end %> + <% for j, d in ipairs(self.deps) do -%> + cbi_d_add("cbi-<%=self.config.."-"..section.."-"..self.option..d.add%>", { + <%- + for k,v in pairs(d.deps) do + local depk + if k:find("!", 1, true) then + depk = string.format('"%s"', k) + elseif k:find(".", 1, true) then + depk = string.format('"cbid.%s"', k) + else + depk = string.format('"cbid.%s.%s.%s"', self.config, section, k) + end + -%> + <%-= depk .. ":" .. string.format("%q", v)-%> + <%-if next(d.deps, k) then-%>,<%-end-%> + <%- + end + -%> + }, "cbip-<%=self.config.."-"..section.."-"..self.option..d.add%>"); + <%- end %> + </script> +<%- end %> diff --git a/modules/base/luasrc/luasrc/view/cbi/full_valueheader.htm b/modules/base/luasrc/luasrc/view/cbi/full_valueheader.htm new file mode 100644 index 0000000000..aaf085473a --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/full_valueheader.htm @@ -0,0 +1,9 @@ +<div class="cbi-value<% if self.error and self.error[section] then %> cbi-value-error<% end %><% if self.last_child then %> cbi-value-last<% end %>" id="cbi-<%=self.config.."-"..section.."-"..self.option%>"> + <%- if self.title and #self.title > 0 then -%> + <label class="cbi-value-title"<%= attr("for", cbid) %>> + <%- if self.titleref then -%><a title="<%=self.titledesc or translate('Go to relevant configuration page')%>" class="cbi-title-ref" href="<%=self.titleref%>"><%- end -%> + <%-=self.title-%> + <%- if self.titleref then -%></a><%- end -%> + </label> + <div class="cbi-value-field"> + <%- end -%> diff --git a/modules/base/luasrc/luasrc/view/cbi/fvalue.htm b/modules/base/luasrc/luasrc/view/cbi/fvalue.htm new file mode 100644 index 0000000000..a1e0808e8d --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/fvalue.htm @@ -0,0 +1,9 @@ +<%+cbi/valueheader%> + <input type="hidden" value="1"<%= + attr("name", "cbi.cbe." .. self.config .. "." .. section .. "." .. self.option) + %> /> + <input class="cbi-input-checkbox" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)" type="checkbox"<%= + attr("id", cbid) .. attr("name", cbid) .. attr("value", self.enabled or 1) .. + ifattr((self:cfgvalue(section) or self.default) == self.enabled, "checked", "checked") + %> /> +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/header.htm b/modules/base/luasrc/luasrc/view/cbi/header.htm new file mode 100644 index 0000000000..2bddaba61a --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/header.htm @@ -0,0 +1,7 @@ +<%+header%> +<form method="post" name="cbi" action="<%=REQUEST_URI%>" enctype="multipart/form-data" onreset="return cbi_validate_reset(this)" onsubmit="return cbi_validate_form(this, '<%:Some fields are invalid, cannot save values!%>')"> + <div> + <script type="text/javascript" src="<%=resource%>/cbi.js"></script> + <input type="hidden" name="cbi.submit" value="1" /> + <input type="submit" value="<%:Save%>" class="hidden" /> + </div> diff --git a/modules/base/luasrc/luasrc/view/cbi/lvalue.htm b/modules/base/luasrc/luasrc/view/cbi/lvalue.htm new file mode 100644 index 0000000000..8cc086db42 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/lvalue.htm @@ -0,0 +1,18 @@ +<%+cbi/valueheader%> +<% if self.widget == "select" then %> + <select class="cbi-input-select" onchange="cbi_d_update(this.id)"<%= attr("id", cbid) .. attr("name", cbid) .. ifattr(self.size, "size") %>> + <% for i, key in pairs(self.keylist) do -%> + <option id="cbi-<%=self.config.."-"..section.."-"..self.option.."-"..key%>"<%= attr("value", key) .. ifattr(tostring(self:cfgvalue(section) or self.default) == key, "selected", "selected") %>><%=striptags(self.vallist[i])%></option> + <%- end %> + </select> +<% elseif self.widget == "radio" then + local c = 0 + for i, key in pairs(self.keylist) do + c = c + 1 +%> + <input class="cbi-input-radio" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)" type="radio"<%= attr("id", cbid..c) .. attr("name", cbid) .. attr("value", key) .. ifattr((self:cfgvalue(section) or self.default) == key, "checked", "checked") %> /> + <label<%= attr("for", cbid..c) %>><%=self.vallist[i]%></label> +<% if c == self.size then c = 0 %><% if self.orientation == "horizontal" then %> <% else %><br /><% end %> +<% end end %> +<% end %> +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/map.htm b/modules/base/luasrc/luasrc/view/cbi/map.htm new file mode 100644 index 0000000000..053220d185 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/map.htm @@ -0,0 +1,13 @@ +<%- if firstmap and messages then local msg; for _, msg in ipairs(messages) do -%> + <div class="errorbox"><%=pcdata(msg)%></div> +<%- end end -%> + +<%-+cbi/apply_xhr-%> + +<div class="cbi-map" id="cbi-<%=self.config%>"> + <% if self.title and #self.title > 0 then %><h2><a id="content" name="content"><%=self.title%></a></h2><% end %> + <% if self.description and #self.description > 0 then %><div class="cbi-map-descr"><%=self.description%></div><% end %> + <%- if firstmap and applymap then cbi_apply_xhr(self.config, parsechain, redirect) end -%> + <%- self:render_children() %> + <br /> +</div> diff --git a/modules/base/luasrc/luasrc/view/cbi/mvalue.htm b/modules/base/luasrc/luasrc/view/cbi/mvalue.htm new file mode 100644 index 0000000000..6a0b3881d0 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/mvalue.htm @@ -0,0 +1,19 @@ +<% local v = self:valuelist(section) or {} -%> +<%+cbi/valueheader%> +<% if self.widget == "select" then %> + <select class="cbi-input-select" multiple="multiple" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)"<%= attr("name", cbid) .. ifattr(self.size, "size") %>> + <% for i, key in pairs(self.keylist) do -%> + <option<%= attr("value", key) .. ifattr(luci.util.contains(v, key), "selected", "selected") %>><%=striptags(self.vallist[i])%></option> + <%- end %> + </select> +<% elseif self.widget == "checkbox" then + local c = 0; + for i, key in pairs(self.keylist) do + c = c + 1 +%> + <input class="cbi-input-checkbox" type="checkbox" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)"<%= attr("id", cbid..c) .. attr("name", cbid) .. attr("value", key) .. ifattr(luci.util.contains(v, key), "checked", "checked") %> /> + <label<%= attr("for", cbid..c) %>><%=self.vallist[i]%></label><br /> +<% if c == self.size then c = 0 %><br /> +<% end end %> +<% end %> +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/network_ifacelist.htm b/modules/base/luasrc/luasrc/view/cbi/network_ifacelist.htm new file mode 100644 index 0000000000..643d849a50 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/network_ifacelist.htm @@ -0,0 +1,81 @@ +<%+cbi/valueheader%> + +<%- + local utl = require "luci.util" + local net = require "luci.model.network".init() + local cbeid = luci.cbi.FEXIST_PREFIX .. self.config .. "." .. section .. "." .. self.option + + local iface + local ifaces = net:get_interfaces() + local value + + if self.map:formvalue(cbeid) == "1" then + value = self:formvalue(section) or self.default or "" + else + value = self:cfgvalue(section) or self.default + end + + local checked = { } + + if value then + for value in utl.imatch(value) do + checked[value] = true + end + else + local n = self.network and net:get_network(self.network) + if n then + local i + for _, i in ipairs(n:get_interfaces() or { n:get_interface() }) do + checked[i:name()] = true + end + end + end +-%> + +<input type="hidden" name="<%=cbeid%>" value="1" /> +<ul style="margin:0; list-style-type:none"> + <% for _, iface in ipairs(ifaces) do + local link = iface:adminlink() + if (not self.nobridges or not iface:is_bridge()) and + (not self.noinactive or iface:is_up()) and + iface:name() ~= self.exclude + then %> + <li> + <input class="cbi-input-<%=self.widget or "radio"%>" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)"<%= + attr("type", self.widget or "radio") .. + attr("id", cbid .. "." .. iface:name()) .. + attr("name", cbid) .. attr("value", iface:name()) .. + ifattr(checked[iface:name()], "checked", "checked") + %> />   + <label<%=attr("for", cbid .. "." .. iface:name())%>> + <% if link then -%><a href="<%=link%>"><% end -%> + <img<%=attr("title", iface:get_i18n())%> style="width:16px; height:16px; vertical-align:middle" src="<%=resource%>/icons/<%=iface:type()%><%=iface:is_up() and "" or "_disabled"%>.png" /> + <% if link then -%></a><% end -%> + <%=pcdata(iface:get_i18n())%> + <% local ns = iface:get_networks(); if #ns > 0 then %>( + <%- local i, n; for i, n in ipairs(ns) do -%> + <%-= (i>1) and ', ' -%> + <a href="<%=n:adminlink()%>"><%=n:name()%></a> + <%- end -%> + )<% end %> + </label> + </li> + <% end end %> + <% if not self.nocreate then %> + <li> + <input class="cbi-input-<%=self.widget or "radio"%>" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)"<%= + attr("type", self.widget or "radio") .. + attr("id", cbid .. "_custom") .. + attr("name", cbid) .. + attr("value", " ") + %> />   + <label<%=attr("for", cbid .. "_custom")%>> + <img title="<%:Custom Interface%>" style="width:16px; height:16px; vertical-align:middle" src="<%=resource%>/icons/ethernet_disabled.png" /> + <%:Custom Interface%>: + </label> + <input type="text" style="width:50px" onfocus="document.getElementById('<%=cbid%>_custom').checked=true" onblur="var x=document.getElementById('<%=cbid%>_custom'); x.value=this.value; x.checked=true" /> + </li> + <% end %> +</ul> + +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/network_netinfo.htm b/modules/base/luasrc/luasrc/view/cbi/network_netinfo.htm new file mode 100644 index 0000000000..4fd84112a4 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/network_netinfo.htm @@ -0,0 +1,27 @@ +<%+cbi/valueheader%> + +<%- + local value = self:formvalue(section) + if not value or value == "-" then + value = self:cfgvalue(section) or self.default + end + + local nwm = require "luci.model.network".init() + local net = nwm:get_network(value) +-%> + +<% if net then %> +<span class="ifacebadge"><%=net:name()%>: + <% + local empty = true + for _, iface in ipairs(net:get_interfaces() or { net:get_interface() }) do + if not iface:is_bridge() then + empty = false + %> + <img<%=attr("title", iface:get_i18n())%> style="width:16px; height:16px; vertical-align:middle" src="<%=resource%>/icons/<%=iface:type()%><%=iface:is_up() and "" or "_disabled"%>.png" /> + <% end end %> + <% if empty then %><em><%:(no interfaces attached)%></em><% end %> +</span> +<% end %> + +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/network_netlist.htm b/modules/base/luasrc/luasrc/view/cbi/network_netlist.htm new file mode 100644 index 0000000000..7e23d149a8 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/network_netlist.htm @@ -0,0 +1,81 @@ +<%+cbi/valueheader%> + +<%- + local utl = require "luci.util" + local nwm = require "luci.model.network".init() + + local net, iface + local networks = nwm:get_networks() + local value = self:formvalue(section) + + self.cast = nil + + if not value or value == "-" then + value = self:cfgvalue(section) or self.default + end + + local checked = { } + for value in utl.imatch(value) do + checked[value] = true + end +-%> + +<ul style="margin:0; list-style-type:none; text-align:left"> + <% for _, net in ipairs(networks) do + if (net:name() ~= "loopback") and + (net:name() ~= self.exclude) and + (not self.novirtual or not net:is_virtual()) + then %> + <li style="padding:0.25em 0"> + <input class="cbi-input-<%=self.widget or "radio"%>" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)"<%= + attr("type", self.widget or "radio") .. + attr("id", cbid .. "." .. net:name()) .. + attr("name", cbid) .. attr("value", net:name()) .. + ifattr(checked[net:name()], "checked", "checked") + %> />   + <label<%=attr("for", cbid .. "." .. net:name())%>> + <span class="ifacebadge"><%=net:name()%>: + <% + local empty = true + for _, iface in ipairs(net:is_bridge() and net:get_interfaces() or { net:get_interface() }) do + if not iface:is_bridge() then + empty = false + %> + <img<%=attr("title", iface:get_i18n())%> style="width:16px; height:16px; vertical-align:middle" src="<%=resource%>/icons/<%=iface:type()%><%=iface:is_up() and "" or "_disabled"%>.png" /> + <% end end %> + <% if empty then %><em><%:(no interfaces attached)%></em><% end %> + </span> + </label> + </li> + <% end end %> + + <% if not self.nocreate then %> + <li style="padding:0.25em 0"> + <input class="cbi-input-<%=self.widget or "radio"%>" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)"<%=attr("type", self.widget or "radio") .. attr("id", cbid .. "_new") .. attr("name", cbid) .. attr("value", "-") .. ifattr(not value and self.widget ~= "checkbox", "checked", "checked")%> />   + <div style="padding:0.5em; display:inline"> + <label<%=attr("for", cbid .. "_new")%>><em> + <%- if self.widget == "checkbox" then -%> + <%:create:%> + <%- else -%> + <%:unspecified -or- create:%> + <%- end -%> </em></label> + <input style="width:6em" type="text"<%=attr("name", cbid .. ".newnet")%> onfocus="document.getElementById('<%=cbid%>_new').checked=true" /> + </div> + </li> + <% elseif self.widget ~= "checkbox" and self.unspecified then %> + <li style="padding:0.25em 0"> + <input class="cbi-input-<%=self.widget or "radio"%>" onclick="cbi_d_update(this.id)" onchange="cbi_d_update(this.id)"<%= + attr("type", self.widget or "radio") .. + attr("id", cbid .. "_uns") .. + attr("name", cbid) .. + attr("value", "") .. + ifattr(not value or #value == 0, "checked", "checked") + %> />   + <div style="padding:0.5em; display:inline"> + <label<%=attr("for", cbid .. "_uns")%>><em><%:unspecified%></em></label> + </div> + </li> + <% end %> +</ul> + +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/nsection.htm b/modules/base/luasrc/luasrc/view/cbi/nsection.htm new file mode 100644 index 0000000000..95e7658822 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/nsection.htm @@ -0,0 +1,31 @@ +<% if self:cfgvalue(self.section) then section = self.section %> + <fieldset class="cbi-section" id="cbi-<%=self.config%>-<%=section%>"> + <% if self.title and #self.title > 0 then -%> + <legend><%=self.title%></legend> + <%- end %> + <% if self.description and #self.description > 0 then -%> + <div class="cbi-section-descr"><%=self.description%></div> + <%- end %> + <% if self.addremove then -%> + <div class="cbi-section-remove right"> + <input type="submit" name="cbi.rns.<%=self.config%>.<%=section%>" value="<%:Delete%>" /> + </div> + <%- end %> + <%+cbi/tabmenu%> + <div class="cbi-section-node<% if self.tabs then %> cbi-section-node-tabbed<% end %>" id="cbi-<%=self.config%>-<%=section%>"> + <%+cbi/ucisection%> + </div> + <br /> + </fieldset> +<% elseif self.addremove then %> + <% if self.template_addremove then include(self.template_addremove) else -%> + <fieldset class="cbi-section" id="cbi-<%=self.config%>-<%=self.section%>"> + <% if self.title and #self.title > 0 then -%> + <legend><%=self.title%></legend> + <%- end %> + <div class="cbi-section-descr"><%=self.description%></div> + <input type="submit" class="cbi-button-add" name="cbi.cns.<%=self.config%>.<%=self.section%>" value="<%:Add%>" /> + </fieldset> + <%- end %> +<% end %> +<!-- /nsection --> diff --git a/modules/base/luasrc/luasrc/view/cbi/nullsection.htm b/modules/base/luasrc/luasrc/view/cbi/nullsection.htm new file mode 100644 index 0000000000..bd48950958 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/nullsection.htm @@ -0,0 +1,38 @@ +<fieldset class="cbi-section"> + <% if self.title and #self.title > 0 then -%> + <legend><%=self.title%></legend> + <%- end %> + <% if self.description and #self.description > 0 then -%> + <div class="cbi-section-descr"><%=self.description%></div> + <%- end %> + <div class="cbi-section-node" id="cbi-<%=self.config%>-<%=tostring(self):sub(8)%>"> + <div> + <% self:render_children(1, scope or {}) %> + </div> + <% if self.error and self.error[1] then -%> + <div class="cbi-section-error"> + <ul><% for _, e in ipairs(self.error[1]) do -%> + <li> + <%- if e == "invalid" then -%> + <%:One or more fields contain invalid values!%> + <%- elseif e == "missing" then -%> + <%:One or more required fields have no value!%> + <%- else -%> + <%=pcdata(e)%> + <%- end -%> + </li> + <%- end %></ul> + </div> + <%- end %> + </div> + <br /> +</fieldset> +<%- + if type(self.hidden) == "table" then + for k, v in pairs(self.hidden) do +-%> + <input type="hidden" id="<%=k%>" name="<%=k%>" value="<%=pcdata(v)%>" /> +<%- + end + end +%> diff --git a/modules/base/luasrc/luasrc/view/cbi/simpleform.htm b/modules/base/luasrc/luasrc/view/cbi/simpleform.htm new file mode 100644 index 0000000000..5216cd50f1 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/simpleform.htm @@ -0,0 +1,57 @@ +<% if not self.embedded then %> +<form method="post" enctype="multipart/form-data" action="<%=REQUEST_URI%>"> + <div> + <script type="text/javascript" src="<%=resource%>/cbi.js"></script> + <input type="hidden" name="cbi.submit" value="1" /> + </div> +<% end %> + <div class="cbi-map" id="cbi-<%=self.config%>"> + <% if self.title and #self.title > 0 then %><h2><a id="content" name="content"><%=self.title%></a></h2><% end %> + <% if self.description and #self.description > 0 then %><div class="cbi-map-descr"><%=self.description%></div><% end %> + <% self:render_children() %> + <br /> + </div> +<%- if self.message then %> + <div><%=self.message%></div> +<%- end %> +<%- if self.errmessage then %> + <div class="error"><%=self.errmessage%></div> +<%- end %> +<% if not self.embedded then %> + <div class="cbi-page-actions"> +<%- + if type(self.hidden) == "table" then + for k, v in pairs(self.hidden) do +-%> + <input type="hidden" id="<%=k%>" name="<%=k%>" value="<%=pcdata(v)%>" /> +<%- + end + end +%> +<% if redirect then %> + <div style="float:left"> + <input class="cbi-button cbi-button-link" type="button" value="<%:Back to Overview%>" onclick="location.href='<%=pcdata(redirect)%>'" /> + </div> +<% end %> +<%- if self.flow and self.flow.skip then %> + <input class="cbi-button cbi-button-skip" type="submit" name="cbi.skip" value="<%:Skip%>" /> +<% end %> +<%- if self.submit ~= false then %> + <input class="cbi-button cbi-button-save" type="submit" value=" + <%- if not self.submit then -%><%-:Submit-%><%-else-%><%=self.submit%><%end-%> + " /> +<% end %> +<%- if self.reset ~= false then %> + <input class="cbi-button cbi-button-reset" type="reset" value=" + <%- if not self.reset then -%><%-:Reset-%><%-else-%><%=self.reset%><%end-%> + " /> +<% end %> +<%- if self.cancel ~= false and self.on_cancel then %> + <input class="cbi-button cbi-button-reset" type="submit" name="cbi.cancel" value=" + <%- if not self.cancel then -%><%-:Cancel-%><%-else-%><%=self.cancel%><%end-%> + " /> +<% end %> + <script type="text/javascript">cbi_d_update();</script> + </div> +</form> +<% end %> diff --git a/modules/base/luasrc/luasrc/view/cbi/tabcontainer.htm b/modules/base/luasrc/luasrc/view/cbi/tabcontainer.htm new file mode 100644 index 0000000000..38c435d6a1 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/tabcontainer.htm @@ -0,0 +1,7 @@ +<% for tab, data in pairs(self.tabs) do %> + <div class="cbi-tabcontainer" id="container.<%=self.config%>.<%=section%>.<%=tab%>"<% if tab ~= self.selected_tab then %> style="display:none"<% end %>> + <% if data.description then %><div class="cbi-tab-descr"><%=data.description%></div><% end %> + <% self:render_tab(tab, section, scope or {}) %> + </div> + <script type="text/javascript">cbi_t_add('<%=self.config%>.<%=section%>', '<%=tab%>')</script> +<% end %> diff --git a/modules/base/luasrc/luasrc/view/cbi/tabmenu.htm b/modules/base/luasrc/luasrc/view/cbi/tabmenu.htm new file mode 100644 index 0000000000..b96ac9ce4b --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/tabmenu.htm @@ -0,0 +1,13 @@ +<%- if self.tabs then %> + <ul class="cbi-tabmenu"> + <%- self.selected_tab = luci.http.formvalue("tab." .. self.config .. "." .. section) %> + <%- for _, tab in ipairs(self.tab_names) do if #self.tabs[tab].childs > 0 then %> + <script type="text/javascript">cbi_c['container.<%=self.config%>.<%=section%>.<%=tab%>'] = <%=#self.tabs[tab].childs%>;</script> + <%- if not self.selected_tab then self.selected_tab = tab end %> + <li id="tab.<%=self.config%>.<%=section%>.<%=tab%>" class="cbi-tab<%=(tab == self.selected_tab) and '' or '-disabled'%>"> + <a onclick="this.blur(); return cbi_t_switch('<%=self.config%>.<%=section%>', '<%=tab%>')" href="<%=REQUEST_URI%>?tab.<%=self.config%>.<%=section%>=<%=tab%>"><%=self.tabs[tab].title%></a> + <% if tab == self.selected_tab then %><input type="hidden" id="tab.<%=self.config%>.<%=section%>" name="tab.<%=self.config%>.<%=section%>" value="<%=tab%>" /><% end %> + </li> + <% end end -%> + </ul> +<% end -%> diff --git a/modules/base/luasrc/luasrc/view/cbi/tblsection.htm b/modules/base/luasrc/luasrc/view/cbi/tblsection.htm new file mode 100644 index 0000000000..d928791167 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/tblsection.htm @@ -0,0 +1,146 @@ +<%- +local rowcnt = 1 +function rowstyle() + rowcnt = rowcnt + 1 + return (rowcnt % 2) + 1 +end + +function width(o) + if o.width then + if type(o.width) == 'number' then + return ' style="width:%dpx"' % o.width + end + return ' style="width:%s"' % o.width + end + return '' +end +-%> + +<!-- tblsection --> +<fieldset class="cbi-section" id="cbi-<%=self.config%>-<%=self.sectiontype%>"> + <% if self.title and #self.title > 0 then -%> + <legend><%=self.title%></legend> + <%- end %> + <%- if self.sortable then -%> + <input type="hidden" id="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" name="cbi.sts.<%=self.config%>.<%=self.sectiontype%>" value="" /> + <%- end -%> + <div class="cbi-section-descr"><%=self.description%></div> + <div class="cbi-section-node"> + <%- local count = 0 -%> + <table class="cbi-section-table"> + <tr class="cbi-section-table-titles"> + <%- if not self.anonymous then -%> + <%- if self.sectionhead then -%> + <th class="cbi-section-table-cell"><%=self.sectionhead%></th> + <%- else -%> + <th> </th> + <%- end -%> + <%- end -%> + <%- for i, k in pairs(self.children) do if not k.optional then -%> + <th class="cbi-section-table-cell"<%=width(k)%>> + <%- if k.titleref then -%><a title="<%=self.titledesc or translate('Go to relevant configuration page')%>" class="cbi-title-ref" href="<%=k.titleref%>"><%- end -%> + <%-=k.title-%> + <%- if k.titleref then -%></a><%- end -%> + </th> + <%- count = count + 1; end; end; if self.sortable then -%> + <th class="cbi-section-table-cell"><%:Sort%></th> + <%- end; if self.extedit or self.addremove then -%> + <th class="cbi-section-table-cell"> </th> + <%- count = count + 1; end -%> + </tr> + <tr class="cbi-section-table-descr"> + <%- if not self.anonymous then -%> + <%- if self.sectiondesc then -%> + <th class="cbi-section-table-cell"><%=self.sectiondesc%></th> + <%- else -%> + <th></th> + <%- end -%> + <%- end -%> + <%- for i, k in pairs(self.children) do if not k.optional then -%> + <th class="cbi-section-table-cell"<%=width(k)%>><%=k.description%></th> + <%- end; end; if self.sortable then -%> + <th class="cbi-section-table-cell"></th> + <%- end; if self.extedit or self.addremove then -%> + <th class="cbi-section-table-cell"></th> + <%- end -%> + </tr> + <%- local isempty = true + for i, k in ipairs(self:cfgsections()) do + section = k + isempty = false + scope = { valueheader = "cbi/cell_valueheader", valuefooter = "cbi/cell_valuefooter" } + -%> + <tr class="cbi-section-table-row<% if self.extedit or self.rowcolors then %> cbi-rowstyle-<%=rowstyle()%><% end %>" id="cbi-<%=self.config%>-<%=section%>"> + <% if not self.anonymous then -%> + <th><h3><%=(type(self.sectiontitle) == "function") and self:sectiontitle(section) or k%></h3></th> + <%- end %> + + + <%- + for k, node in ipairs(self.children) do + if not node.optional then + node:render(section, scope or {}) + end + end + -%> + + <%- if self.sortable then -%> + <td class="cbi-section-table-cell"> + <input class="cbi-button cbi-button-up" type="button" value="" onclick="return cbi_row_swap(this, true, 'cbi.sts.<%=self.config%>.<%=self.sectiontype%>')" alt="<%:Move up%>" title="<%:Move up%>" /> + <input class="cbi-button cbi-button-down" type="button" value="" onclick="return cbi_row_swap(this, false, 'cbi.sts.<%=self.config%>.<%=self.sectiontype%>')" alt="<%:Move down%>" title="<%:Move down%>" /> + </td> + <%- end -%> + + <%- if self.extedit or self.addremove then -%> + <td class="cbi-section-table-cell"> + <%- if self.extedit then -%> + <input class="cbi-button cbi-button-edit" type="button" value="<%:Edit%>" + <%- if type(self.extedit) == "string" then + %> onclick="location.href='<%=self.extedit:format(section)%>'" + <%- elseif type(self.extedit) == "function" then + %> onclick="location.href='<%=self:extedit(section)%>'" + <%- end + %> alt="<%:Edit%>" title="<%:Edit%>" /> + <%- end; if self.addremove then %> + <input class="cbi-button cbi-button-remove" type="submit" value="<%:Delete%>" onclick="this.form.cbi_state='del-section'; return true" name="cbi.rts.<%=self.config%>.<%=k%>" alt="<%:Delete%>" title="<%:Delete%>" /> + <%- end -%> + </td> + <%- end -%> + </tr> + <%- end -%> + + <%- if isempty then -%> + <tr class="cbi-section-table-row"> + <td colspan="<%=count%>"><em><br /><%:This section contains no values yet%></em></td> + </tr> + <%- end -%> + </table> + + <% if self.error then %> + <div class="cbi-section-error"> + <ul><% for _, c in pairs(self.error) do for _, e in ipairs(c) do -%> + <li><%=pcdata(e):gsub("\n","<br />")%></li> + <%- end end %></ul> + </div> + <% end %> + + <%- if self.addremove then -%> + <% if self.template_addremove then include(self.template_addremove) else -%> + <div class="cbi-section-create cbi-tblsection-create"> + <% if self.anonymous then %> + <input class="cbi-button cbi-button-add" type="submit" value="<%:Add%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" title="<%:Add%>" /> + <% else %> + <% if self.invalid_cts then -%><div class="cbi-section-error"><% end %> + <input type="text" class="cbi-section-create-name" id="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" /> + <script type="text/javascript">cbi_validate_field('cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>', true, 'uciname');</script> + <input class="cbi-button cbi-button-add" type="submit" onclick="this.form.cbi_state='add-section'; return true" value="<%:Add%>" title="<%:Add%>" /> + <% if self.invalid_cts then -%> + <br /><%:Invalid%></div> + <%- end %> + <% end %> + </div> + <%- end %> + <%- end -%> + </div> +</fieldset> +<!-- /tblsection --> diff --git a/modules/base/luasrc/luasrc/view/cbi/tsection.htm b/modules/base/luasrc/luasrc/view/cbi/tsection.htm new file mode 100644 index 0000000000..087548bf28 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/tsection.htm @@ -0,0 +1,48 @@ +<fieldset class="cbi-section" id="cbi-<%=self.config%>-<%=self.sectiontype%>"> + <% if self.title and #self.title > 0 then -%> + <legend><%=self.title%></legend> + <%- end %> + <div class="cbi-section-descr"><%=self.description%></div> + <% local isempty = true for i, k in ipairs(self:cfgsections()) do -%> + <% if self.addremove then -%> + <div class="cbi-section-remove right"> + <input type="submit" name="cbi.rts.<%=self.config%>.<%=k%>" onclick="this.form.cbi_state='del-section'; return true" value="<%:Delete%>" class="cbi-button" /> + </div> + <%- end %> + + <%- section = k; isempty = false -%> + + <% if not self.anonymous then -%> + <h3><%=section:upper()%></h3> + <%- end %> + + <%+cbi/tabmenu%> + + <fieldset class="cbi-section-node<% if self.tabs then %> cbi-section-node-tabbed<% end %>" id="cbi-<%=self.config%>-<%=section%>"> + <%+cbi/ucisection%> + </fieldset> + <br /> + <%- end %> + + <% if isempty then -%> + <em><%:This section contains no values yet%><br /><br /></em> + <%- end %> + + <% if self.addremove then -%> + <% if self.template_addremove then include(self.template_addremove) else -%> + <div class="cbi-section-create"> + <% if self.anonymous then -%> + <input type="submit" class="cbi-button cbi-button-add" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" value="<%:Add%>" /> + <%- else -%> + <% if self.invalid_cts then -%><div class="cbi-section-error"><% end %> + <input type="text" class="cbi-section-create-name" id="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" name="cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>" /> + <script type="text/javascript">cbi_validate_field('cbi.cts.<%=self.config%>.<%=self.sectiontype%>.<%=section%>', true, 'uciname');</script> + <input type="submit" class="cbi-button cbi-button-add" onclick="this.form.cbi_state='add-section'; return true" value="<%:Add%>" /> + <% if self.invalid_cts then -%> + <br /><%:Invalid%></div> + <%- end %> + <%- end %> + </div> + <%- end %> + <%- end %> +</fieldset> diff --git a/modules/base/luasrc/luasrc/view/cbi/tvalue.htm b/modules/base/luasrc/luasrc/view/cbi/tvalue.htm new file mode 100644 index 0000000000..fcf7a6c94c --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/tvalue.htm @@ -0,0 +1,5 @@ +<%+cbi/valueheader%> + <textarea class="cbi-input-textarea" <% if not self.size then %> style="width: 100%"<% else %> cols="<%=self.size%>"<% end %> onchange="cbi_d_update(this.id)"<%= attr("name", cbid) .. attr("id", cbid) .. ifattr(self.rows, "rows") .. ifattr(self.wrap, "wrap") %>> + <%-=pcdata(self:cfgvalue(section))-%> + </textarea> +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/ucisection.htm b/modules/base/luasrc/luasrc/view/cbi/ucisection.htm new file mode 100644 index 0000000000..3b69f12f2e --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/ucisection.htm @@ -0,0 +1,75 @@ +<%- + if type(self.hidden) == "table" then + for k, v in pairs(self.hidden) do +-%> + <input type="hidden" id="<%=k%>" name="<%=k%>" value="<%=pcdata(v)%>" /> +<%- + end + end +%> + +<% if self.tabs then %> + <%+cbi/tabcontainer%> +<% else %> + <% self:render_children(section, scope or {}) %> +<% end %> + +<% if self.error and self.error[section] then -%> + <div class="cbi-section-error"> + <ul><% for _, e in ipairs(self.error[section]) do -%> + <li> + <%- if e == "invalid" then -%> + <%:One or more fields contain invalid values!%> + <%- elseif e == "missing" then -%> + <%:One or more required fields have no value!%> + <%- else -%> + <%=pcdata(e)%> + <%- end -%> + </li> + <%- end %></ul> + </div> +<%- end %> + +<% if self.optionals[section] and #self.optionals[section] > 0 or self.dynamic then %> + <div class="cbi-optionals"> + <% if self.dynamic then %> + <input type="text" id="cbi.opt.<%=self.config%>.<%=section%>" name="cbi.opt.<%=self.config%>.<%=section%>" /> + <% if self.optionals[section] and #self.optionals[section] > 0 then %> + <script type="text/javascript"> + cbi_combobox_init('cbi.opt.<%=self.config%>.<%=section%>', { + <%- + for i, val in pairs(self.optionals[section]) do + -%> + <%-=string.format("%q", val.option) .. ":" .. string.format("%q", striptags(val.title))-%> + <%-if next(self.optionals[section], i) then-%>,<%-end-%> + <%- + end + -%> + }, '', '<%-: -- custom -- -%>'); + </script> + <% end %> + <% else %> + <select id="cbi.opt.<%=self.config%>.<%=section%>" name="cbi.opt.<%=self.config%>.<%=section%>"> + <option><%: -- Additional Field -- %></option> + <% for key, val in pairs(self.optionals[section]) do -%> + <option id="cbi-<%=self.config.."-"..section.."-"..val.option%>" value="<%=val.option%>"><%=striptags(val.title)%></option> + <%- end %> + </select> + <script type="text/javascript"><% for key, val in pairs(self.optionals[section]) do %> + <% if #val.deps > 0 then %><% for j, d in ipairs(val.deps) do -%> + cbi_d_add("cbi-<%=self.config.."-"..section.."-"..val.option..d.add%>", { + <%- + for k,v in pairs(d.deps) do + -%> + <%-=string.format('"cbid.%s.%s.%s"', self.config, section, k) .. ":" .. string.format("%q", v)-%> + <%-if next(d.deps, k) then-%>,<%-end-%> + <%- + end + -%> + }); + <%- end %><% end %> + <% end %></script> + <% end %> + <input type="submit" class="cbi-button cbi-button-fieldadd" value="<%:Add%>" /> + </div> +<% end %> diff --git a/modules/base/luasrc/luasrc/view/cbi/upload.htm b/modules/base/luasrc/luasrc/view/cbi/upload.htm new file mode 100644 index 0000000000..7770934111 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/upload.htm @@ -0,0 +1,14 @@ +<% + local t = require("luci.tools.webadmin") + local v = self:cfgvalue(section) + local s = v and nixio.fs.stat(v) +-%> +<%+cbi/valueheader%> + <% if s then %> + <%:Uploaded File%> (<%=t.byte_format(s.size)%>) + <input type="hidden"<%= attr("value", v) .. attr("name", cbid) .. attr("id", cbid) %> /> + <input class="cbi-button cbi-input-image" type="image" value="<%:Replace entry%>" name="cbi.rlf.<%=section .. "." .. self.option%>" alt="<%:Replace entry%>" title="<%:Replace entry%>" src="<%=resource%>/cbi/reload.gif" /> + <% else %> + <input class="cbi-input-file" type="file"<%= attr("name", cbid) .. attr("id", cbid) %> /> + <% end %> +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/value.htm b/modules/base/luasrc/luasrc/view/cbi/value.htm new file mode 100644 index 0000000000..d1a7bea5c6 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/value.htm @@ -0,0 +1,35 @@ +<%+cbi/valueheader%> + <input type="<%=self.password and 'password" class="cbi-input-password' or 'text" class="cbi-input-text' %>" onchange="cbi_d_update(this.id)"<%= + attr("name", cbid) .. attr("id", cbid) .. attr("value", self:cfgvalue(section) or self.default) .. + ifattr(self.size, "size") .. ifattr(self.placeholder, "placeholder") + %> /> + <% if self.password then %><img src="<%=resource%>/cbi/reload.gif" style="vertical-align:middle" title="<%:Reveal/hide password%>" onclick="var e = document.getElementById('<%=cbid%>'); e.type = (e.type=='password') ? 'text' : 'password';" /><% end %> + <% if #self.keylist > 0 or self.datatype then -%> + <script type="text/javascript">//<![CDATA[ + <% if #self.keylist > 0 then -%> + cbi_combobox_init('<%=cbid%>', { + <%- + for i, k in ipairs(self.keylist) do + -%> + <%-=string.format("%q", k) .. ":" .. string.format("%q", self.vallist[i])-%> + <%-if i<#self.keylist then-%>,<%-end-%> + <%- + end + -%> + }, '<%- if not self.rmempty and not self.optional then -%> + <%-: -- Please choose -- -%> + <%- elseif self.placeholder then -%> + <%-= pcdata(self.placeholder) -%> + <%- end -%>', ' + <%- if self.combobox_manual then -%> + <%-=self.combobox_manual-%> + <%- else -%> + <%-: -- custom -- -%> + <%- end -%>'); + <%- end %> + <% if self.datatype then -%> + cbi_validate_field('<%=cbid%>', <%=tostring((self.optional or self.rmempty) == true)%>, '<%=self.datatype:gsub("'", "\\'")%>'); + <%- end %> + //]]></script> + <% end -%> +<%+cbi/valuefooter%> diff --git a/modules/base/luasrc/luasrc/view/cbi/valuefooter.htm b/modules/base/luasrc/luasrc/view/cbi/valuefooter.htm new file mode 100644 index 0000000000..805312e451 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/valuefooter.htm @@ -0,0 +1 @@ +<% include( valuefooter or "cbi/full_valuefooter" ) %> diff --git a/modules/base/luasrc/luasrc/view/cbi/valueheader.htm b/modules/base/luasrc/luasrc/view/cbi/valueheader.htm new file mode 100644 index 0000000000..761a54aed0 --- /dev/null +++ b/modules/base/luasrc/luasrc/view/cbi/valueheader.htm @@ -0,0 +1 @@ +<% include( valueheader or "cbi/full_valueheader" ) %> diff --git a/modules/base/luasrc/model/cbi/admin_network/proto_dhcp.lua b/modules/base/luasrc/model/cbi/admin_network/proto_dhcp.lua new file mode 100644 index 0000000000..fe3fec6fa1 --- /dev/null +++ b/modules/base/luasrc/model/cbi/admin_network/proto_dhcp.lua @@ -0,0 +1,76 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2011-2012 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +]]-- + +local map, section, net = ... +local ifc = net:get_interface() + +local hostname, accept_ra, send_rs +local bcast, defaultroute, peerdns, dns, metric, clientid, vendorclass + + +hostname = section:taboption("general", Value, "hostname", + translate("Hostname to send when requesting DHCP")) + +hostname.placeholder = luci.sys.hostname() +hostname.datatype = "hostname" + + +bcast = section:taboption("advanced", Flag, "broadcast", + translate("Use broadcast flag"), + translate("Required for certain ISPs, e.g. Charter with DOCSIS 3")) + +bcast.default = bcast.disabled + + +defaultroute = section:taboption("advanced", Flag, "defaultroute", + translate("Use default gateway"), + translate("If unchecked, no default route is configured")) + +defaultroute.default = defaultroute.enabled + + +peerdns = section:taboption("advanced", Flag, "peerdns", + translate("Use DNS servers advertised by peer"), + translate("If unchecked, the advertised DNS server addresses are ignored")) + +peerdns.default = peerdns.enabled + + +dns = section:taboption("advanced", DynamicList, "dns", + translate("Use custom DNS servers")) + +dns:depends("peerdns", "") +dns.datatype = "ipaddr" +dns.cast = "string" + + +metric = section:taboption("advanced", Value, "metric", + translate("Use gateway metric")) + +metric.placeholder = "0" +metric.datatype = "uinteger" + + +clientid = section:taboption("advanced", Value, "clientid", + translate("Client ID to send when requesting DHCP")) + + +vendorclass = section:taboption("advanced", Value, "vendorid", + translate("Vendor Class to send when requesting DHCP")) + + +luci.tools.proto.opt_macaddr(section, ifc, translate("Override MAC address")) + + +mtu = section:taboption("advanced", Value, "mtu", translate("Override MTU")) +mtu.placeholder = "1500" +mtu.datatype = "max(9200)" diff --git a/modules/base/luasrc/model/cbi/admin_network/proto_none.lua b/modules/base/luasrc/model/cbi/admin_network/proto_none.lua new file mode 100644 index 0000000000..0e34b67de6 --- /dev/null +++ b/modules/base/luasrc/model/cbi/admin_network/proto_none.lua @@ -0,0 +1,13 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2011 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +]]-- + +local map, section, net = ... diff --git a/modules/base/luasrc/model/cbi/admin_network/proto_static.lua b/modules/base/luasrc/model/cbi/admin_network/proto_static.lua new file mode 100644 index 0000000000..338c0b7d83 --- /dev/null +++ b/modules/base/luasrc/model/cbi/admin_network/proto_static.lua @@ -0,0 +1,90 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2011 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +]]-- + +local map, section, net = ... +local ifc = net:get_interface() + +local ipaddr, netmask, gateway, broadcast, dns, accept_ra, send_rs, ip6addr, ip6gw +local mtu, metric + + +ipaddr = section:taboption("general", Value, "ipaddr", translate("IPv4 address")) +ipaddr.datatype = "ip4addr" + + +netmask = section:taboption("general", Value, "netmask", + translate("IPv4 netmask")) + +netmask.datatype = "ip4addr" +netmask:value("255.255.255.0") +netmask:value("255.255.0.0") +netmask:value("255.0.0.0") + + +gateway = section:taboption("general", Value, "gateway", translate("IPv4 gateway")) +gateway.datatype = "ip4addr" + + +broadcast = section:taboption("general", Value, "broadcast", translate("IPv4 broadcast")) +broadcast.datatype = "ip4addr" + + +dns = section:taboption("general", DynamicList, "dns", + translate("Use custom DNS servers")) + +dns.datatype = "ipaddr" +dns.cast = "string" + + +if luci.model.network:has_ipv6() then + + local ip6assign = section:taboption("general", Value, "ip6assign", translate("IPv6 assignment length"), + translate("Assign a part of given length of every public IPv6-prefix to this interface")) + ip6assign:value("", translate("disabled")) + ip6assign:value("64") + ip6assign.datatype = "max(64)" + + local ip6hint = section:taboption("general", Value, "ip6hint", translate("IPv6 assignment hint"), + translate("Assign prefix parts using this hexadecimal subprefix ID for this interface.")) + for i=33,64 do ip6hint:depends("ip6assign", i) end + + ip6addr = section:taboption("general", Value, "ip6addr", translate("IPv6 address")) + ip6addr.datatype = "ip6addr" + ip6addr:depends("ip6assign", "") + + + ip6gw = section:taboption("general", Value, "ip6gw", translate("IPv6 gateway")) + ip6gw.datatype = "ip6addr" + ip6gw:depends("ip6assign", "") + + + local ip6prefix = s:taboption("general", Value, "ip6prefix", translate("IPv6 routed prefix"), + translate("Public prefix routed to this device for distribution to clients.")) + ip6prefix.datatype = "ip6addr" + ip6prefix:depends("ip6assign", "") + +end + + +luci.tools.proto.opt_macaddr(section, ifc, translate("Override MAC address")) + + +mtu = section:taboption("advanced", Value, "mtu", translate("Override MTU")) +mtu.placeholder = "1500" +mtu.datatype = "max(9200)" + + +metric = section:taboption("advanced", Value, "metric", + translate("Use gateway metric")) + +metric.placeholder = "0" +metric.datatype = "uinteger" diff --git a/modules/base/luasrc/model/firewall.lua b/modules/base/luasrc/model/firewall.lua new file mode 100644 index 0000000000..a9f6fdb7fc --- /dev/null +++ b/modules/base/luasrc/model/firewall.lua @@ -0,0 +1,582 @@ +--[[ +LuCI - Firewall model + +Copyright 2009 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +local type, pairs, ipairs, table, luci, math + = type, pairs, ipairs, table, luci, math + +local tpl = require "luci.template.parser" +local utl = require "luci.util" +local uci = require "luci.model.uci" + +module "luci.model.firewall" + + +local uci_r, uci_s + +function _valid_id(x) + return (x and #x > 0 and x:match("^[a-zA-Z0-9_]+$")) +end + +function _get(c, s, o) + return uci_r:get(c, s, o) +end + +function _set(c, s, o, v) + if v ~= nil then + if type(v) == "boolean" then v = v and "1" or "0" end + return uci_r:set(c, s, o, v) + else + return uci_r:delete(c, s, o) + end +end + + +function init(cursor) + uci_r = cursor or uci_r or uci.cursor() + uci_s = uci_r:substate() + + return _M +end + +function save(self, ...) + uci_r:save(...) + uci_r:load(...) +end + +function commit(self, ...) + uci_r:commit(...) + uci_r:load(...) +end + +function get_defaults() + return defaults() +end + +function new_zone(self) + local name = "newzone" + local count = 1 + + while self:get_zone(name) do + count = count + 1 + name = "newzone%d" % count + end + + return self:add_zone(name) +end + +function add_zone(self, n) + if _valid_id(n) and not self:get_zone(n) then + local d = defaults() + local z = uci_r:section("firewall", "zone", nil, { + name = n, + network = " ", + input = d:input() or "DROP", + forward = d:forward() or "DROP", + output = d:output() or "DROP" + }) + + return z and zone(z) + end +end + +function get_zone(self, n) + if uci_r:get("firewall", n) == "zone" then + return zone(n) + else + local z + uci_r:foreach("firewall", "zone", + function(s) + if n and s.name == n then + z = s['.name'] + return false + end + end) + return z and zone(z) + end +end + +function get_zones(self) + local zones = { } + local znl = { } + + uci_r:foreach("firewall", "zone", + function(s) + if s.name then + znl[s.name] = zone(s['.name']) + end + end) + + local z + for z in utl.kspairs(znl) do + zones[#zones+1] = znl[z] + end + + return zones +end + +function get_zone_by_network(self, net) + local z + + uci_r:foreach("firewall", "zone", + function(s) + if s.name and net then + local n + for n in utl.imatch(s.network or s.name) do + if n == net then + z = s['.name'] + return false + end + end + end + end) + + return z and zone(z) +end + +function del_zone(self, n) + local r = false + + if uci_r:get("firewall", n) == "zone" then + local z = uci_r:get("firewall", n, "name") + r = uci_r:delete("firewall", n) + n = z + else + uci_r:foreach("firewall", "zone", + function(s) + if n and s.name == n then + r = uci_r:delete("firewall", s['.name']) + return false + end + end) + end + + if r then + uci_r:foreach("firewall", "rule", + function(s) + if s.src == n or s.dest == n then + uci_r:delete("firewall", s['.name']) + end + end) + + uci_r:foreach("firewall", "redirect", + function(s) + if s.src == n or s.dest == n then + uci_r:delete("firewall", s['.name']) + end + end) + + uci_r:foreach("firewall", "forwarding", + function(s) + if s.src == n or s.dest == n then + uci_r:delete("firewall", s['.name']) + end + end) + end + + return r +end + +function rename_zone(self, old, new) + local r = false + + if _valid_id(new) and not self:get_zone(new) then + uci_r:foreach("firewall", "zone", + function(s) + if old and s.name == old then + if not s.network then + uci_r:set("firewall", s['.name'], "network", old) + end + uci_r:set("firewall", s['.name'], "name", new) + r = true + return false + end + end) + + if r then + uci_r:foreach("firewall", "rule", + function(s) + if s.src == old then + uci_r:set("firewall", s['.name'], "src", new) + end + if s.dest == old then + uci_r:set("firewall", s['.name'], "dest", new) + end + end) + + uci_r:foreach("firewall", "redirect", + function(s) + if s.src == old then + uci_r:set("firewall", s['.name'], "src", new) + end + if s.dest == old then + uci_r:set("firewall", s['.name'], "dest", new) + end + end) + + uci_r:foreach("firewall", "forwarding", + function(s) + if s.src == old then + uci_r:set("firewall", s['.name'], "src", new) + end + if s.dest == old then + uci_r:set("firewall", s['.name'], "dest", new) + end + end) + end + end + + return r +end + +function del_network(self, net) + local z + if net then + for _, z in ipairs(self:get_zones()) do + z:del_network(net) + end + end +end + + +defaults = utl.class() +function defaults.__init__(self) + uci_r:foreach("firewall", "defaults", + function(s) + self.sid = s['.name'] + return false + end) + + self.sid = self.sid or uci_r:section("firewall", "defaults", nil, { }) +end + +function defaults.get(self, opt) + return _get("firewall", self.sid, opt) +end + +function defaults.set(self, opt, val) + return _set("firewall", self.sid, opt, val) +end + +function defaults.syn_flood(self) + return (self:get("syn_flood") == "1") +end + +function defaults.drop_invalid(self) + return (self:get("drop_invalid") == "1") +end + +function defaults.input(self) + return self:get("input") or "DROP" +end + +function defaults.forward(self) + return self:get("forward") or "DROP" +end + +function defaults.output(self) + return self:get("output") or "DROP" +end + + +zone = utl.class() +function zone.__init__(self, z) + if uci_r:get("firewall", z) == "zone" then + self.sid = z + self.data = uci_r:get_all("firewall", z) + else + uci_r:foreach("firewall", "zone", + function(s) + if s.name == z then + self.sid = s['.name'] + self.data = s + return false + end + end) + end +end + +function zone.get(self, opt) + return _get("firewall", self.sid, opt) +end + +function zone.set(self, opt, val) + return _set("firewall", self.sid, opt, val) +end + +function zone.masq(self) + return (self:get("masq") == "1") +end + +function zone.name(self) + return self:get("name") +end + +function zone.network(self) + return self:get("network") +end + +function zone.input(self) + return self:get("input") or defaults():input() or "DROP" +end + +function zone.forward(self) + return self:get("forward") or defaults():forward() or "DROP" +end + +function zone.output(self) + return self:get("output") or defaults():output() or "DROP" +end + +function zone.add_network(self, net) + if uci_r:get("network", net) == "interface" then + local nets = { } + + local n + for n in utl.imatch(self:get("network") or self:get("name")) do + if n ~= net then + nets[#nets+1] = n + end + end + + nets[#nets+1] = net + + _M:del_network(net) + self:set("network", table.concat(nets, " ")) + end +end + +function zone.del_network(self, net) + local nets = { } + + local n + for n in utl.imatch(self:get("network") or self:get("name")) do + if n ~= net then + nets[#nets+1] = n + end + end + + if #nets > 0 then + self:set("network", table.concat(nets, " ")) + else + self:set("network", " ") + end +end + +function zone.get_networks(self) + local nets = { } + + local n + for n in utl.imatch(self:get("network") or self:get("name")) do + nets[#nets+1] = n + end + + return nets +end + +function zone.clear_networks(self) + self:set("network", " ") +end + +function zone.get_forwardings_by(self, what) + local name = self:name() + local forwards = { } + + uci_r:foreach("firewall", "forwarding", + function(s) + if s.src and s.dest and s[what] == name then + forwards[#forwards+1] = forwarding(s['.name']) + end + end) + + return forwards +end + +function zone.add_forwarding_to(self, dest) + local exist, forward + + for _, forward in ipairs(self:get_forwardings_by('src')) do + if forward:dest() == dest then + exist = true + break + end + end + + if not exist and dest ~= self:name() and _valid_id(dest) then + local s = uci_r:section("firewall", "forwarding", nil, { + src = self:name(), + dest = dest + }) + + return s and forwarding(s) + end +end + +function zone.add_forwarding_from(self, src) + local exist, forward + + for _, forward in ipairs(self:get_forwardings_by('dest')) do + if forward:src() == src then + exist = true + break + end + end + + if not exist and src ~= self:name() and _valid_id(src) then + local s = uci_r:section("firewall", "forwarding", nil, { + src = src, + dest = self:name() + }) + + return s and forwarding(s) + end +end + +function zone.del_forwardings_by(self, what) + local name = self:name() + + uci_r:delete_all("firewall", "forwarding", + function(s) + return (s.src and s.dest and s[what] == name) + end) +end + +function zone.add_redirect(self, options) + options = options or { } + options.src = self:name() + + local s = uci_r:section("firewall", "redirect", nil, options) + return s and redirect(s) +end + +function zone.add_rule(self, options) + options = options or { } + options.src = self:name() + + local s = uci_r:section("firewall", "rule", nil, options) + return s and rule(s) +end + +function zone.get_color(self) + if self and self:name() == "lan" then + return "#90f090" + elseif self and self:name() == "wan" then + return "#f09090" + elseif self then + math.randomseed(tpl.hash(self:name())) + + local r = math.random(128) + local g = math.random(128) + local min = 0 + local max = 128 + + if ( r + g ) < 128 then + min = 128 - r - g + else + max = 255 - r - g + end + + local b = min + math.floor( math.random() * ( max - min ) ) + + return "#%02x%02x%02x" % { 0xFF - r, 0xFF - g, 0xFF - b } + else + return "#eeeeee" + end +end + + +forwarding = utl.class() +function forwarding.__init__(self, f) + self.sid = f +end + +function forwarding.src(self) + return uci_r:get("firewall", self.sid, "src") +end + +function forwarding.dest(self) + return uci_r:get("firewall", self.sid, "dest") +end + +function forwarding.src_zone(self) + return zone(self:src()) +end + +function forwarding.dest_zone(self) + return zone(self:dest()) +end + + +rule = utl.class() +function rule.__init__(self, f) + self.sid = f +end + +function rule.get(self, opt) + return _get("firewall", self.sid, opt) +end + +function rule.set(self, opt, val) + return _set("firewall", self.sid, opt, val) +end + +function rule.src(self) + return uci_r:get("firewall", self.sid, "src") +end + +function rule.dest(self) + return uci_r:get("firewall", self.sid, "dest") +end + +function rule.src_zone(self) + return zone(self:src()) +end + +function rule.dest_zone(self) + return zone(self:dest()) +end + + +redirect = utl.class() +function redirect.__init__(self, f) + self.sid = f +end + +function redirect.get(self, opt) + return _get("firewall", self.sid, opt) +end + +function redirect.set(self, opt, val) + return _set("firewall", self.sid, opt, val) +end + +function redirect.src(self) + return uci_r:get("firewall", self.sid, "src") +end + +function redirect.dest(self) + return uci_r:get("firewall", self.sid, "dest") +end + +function redirect.src_zone(self) + return zone(self:src()) +end + +function redirect.dest_zone(self) + return zone(self:dest()) +end diff --git a/modules/base/luasrc/model/ipkg.lua b/modules/base/luasrc/model/ipkg.lua new file mode 100644 index 0000000000..c927e71163 --- /dev/null +++ b/modules/base/luasrc/model/ipkg.lua @@ -0,0 +1,239 @@ +--[[ +LuCI - Lua Configuration Interface + +(c) 2008-2011 Jo-Philipp Wich <xm@subsignal.org> +(c) 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +]]-- + +local os = require "os" +local io = require "io" +local fs = require "nixio.fs" +local util = require "luci.util" + +local type = type +local pairs = pairs +local error = error +local table = table + +local ipkg = "opkg --force-removal-of-dependent-packages --force-overwrite --nocase" +local icfg = "/etc/opkg.conf" + +--- LuCI OPKG call abstraction library +module "luci.model.ipkg" + + +-- Internal action function +local function _action(cmd, ...) + local pkg = "" + for k, v in pairs({...}) do + pkg = pkg .. " '" .. v:gsub("'", "") .. "'" + end + + local c = "%s %s %s >/tmp/opkg.stdout 2>/tmp/opkg.stderr" %{ ipkg, cmd, pkg } + local r = os.execute(c) + local e = fs.readfile("/tmp/opkg.stderr") + local o = fs.readfile("/tmp/opkg.stdout") + + fs.unlink("/tmp/opkg.stderr") + fs.unlink("/tmp/opkg.stdout") + + return r, o or "", e or "" +end + +-- Internal parser function +local function _parselist(rawdata) + if type(rawdata) ~= "function" then + error("OPKG: Invalid rawdata given") + end + + local data = {} + local c = {} + local l = nil + + for line in rawdata do + if line:sub(1, 1) ~= " " then + local key, val = line:match("(.-): ?(.*)%s*") + + if key and val then + if key == "Package" then + c = {Package = val} + data[val] = c + elseif key == "Status" then + c.Status = {} + for j in val:gmatch("([^ ]+)") do + c.Status[j] = true + end + else + c[key] = val + end + l = key + end + else + -- Multi-line field + c[l] = c[l] .. "\n" .. line + end + end + + return data +end + +-- Internal lookup function +local function _lookup(act, pkg) + local cmd = ipkg .. " " .. act + if pkg then + cmd = cmd .. " '" .. pkg:gsub("'", "") .. "'" + end + + -- OPKG sometimes kills the whole machine because it sucks + -- Therefore we have to use a sucky approach too and use + -- tmpfiles instead of directly reading the output + local tmpfile = os.tmpname() + os.execute(cmd .. (" >%s 2>/dev/null" % tmpfile)) + + local data = _parselist(io.lines(tmpfile)) + os.remove(tmpfile) + return data +end + + +--- Return information about installed and available packages. +-- @param pkg Limit output to a (set of) packages +-- @return Table containing package information +function info(pkg) + return _lookup("info", pkg) +end + +--- Return the package status of one or more packages. +-- @param pkg Limit output to a (set of) packages +-- @return Table containing package status information +function status(pkg) + return _lookup("status", pkg) +end + +--- Install one or more packages. +-- @param ... List of packages to install +-- @return Boolean indicating the status of the action +-- @return OPKG return code, STDOUT and STDERR +function install(...) + return _action("install", ...) +end + +--- Determine whether a given package is installed. +-- @param pkg Package +-- @return Boolean +function installed(pkg) + local p = status(pkg)[pkg] + return (p and p.Status and p.Status.installed) +end + +--- Remove one or more packages. +-- @param ... List of packages to install +-- @return Boolean indicating the status of the action +-- @return OPKG return code, STDOUT and STDERR +function remove(...) + return _action("remove", ...) +end + +--- Update package lists. +-- @return Boolean indicating the status of the action +-- @return OPKG return code, STDOUT and STDERR +function update() + return _action("update") +end + +--- Upgrades all installed packages. +-- @return Boolean indicating the status of the action +-- @return OPKG return code, STDOUT and STDERR +function upgrade() + return _action("upgrade") +end + +-- List helper +function _list(action, pat, cb) + local fd = io.popen(ipkg .. " " .. action .. + (pat and (" '%s'" % pat:gsub("'", "")) or "")) + + if fd then + local name, version, desc + while true do + local line = fd:read("*l") + if not line then break end + + name, version, desc = line:match("^(.-) %- (.-) %- (.+)") + + if not name then + name, version = line:match("^(.-) %- (.+)") + desc = "" + end + + cb(name, version, desc) + + name = nil + version = nil + desc = nil + end + + fd:close() + end +end + +--- List all packages known to opkg. +-- @param pat Only find packages matching this pattern, nil lists all packages +-- @param cb Callback function invoked for each package, receives name, version and description as arguments +-- @return nothing +function list_all(pat, cb) + _list("list", pat, cb) +end + +--- List installed packages. +-- @param pat Only find packages matching this pattern, nil lists all packages +-- @param cb Callback function invoked for each package, receives name, version and description as arguments +-- @return nothing +function list_installed(pat, cb) + _list("list_installed", pat, cb) +end + +--- Find packages that match the given pattern. +-- @param pat Find packages whose names or descriptions match this pattern, nil results in zero results +-- @param cb Callback function invoked for each patckage, receives name, version and description as arguments +-- @return nothing +function find(pat, cb) + _list("find", pat, cb) +end + + +--- Determines the overlay root used by opkg. +-- @return String containing the directory path of the overlay root. +function overlay_root() + local od = "/" + local fd = io.open(icfg, "r") + + if fd then + local ln + + repeat + ln = fd:read("*l") + if ln and ln:match("^%s*option%s+overlay_root%s+") then + od = ln:match("^%s*option%s+overlay_root%s+(%S+)") + + local s = fs.stat(od) + if not s or s.type ~= "dir" then + od = "/" + end + + break + end + until not ln + + fd:close() + end + + return od +end diff --git a/modules/base/luasrc/model/network.lua b/modules/base/luasrc/model/network.lua new file mode 100644 index 0000000000..a409621f8e --- /dev/null +++ b/modules/base/luasrc/model/network.lua @@ -0,0 +1,1584 @@ +--[[ +LuCI - Network model + +Copyright 2009-2010 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +local type, next, pairs, ipairs, loadfile, table + = type, next, pairs, ipairs, loadfile, table + +local tonumber, tostring, math = tonumber, tostring, math + +local require = require + +local bus = require "ubus" +local nxo = require "nixio" +local nfs = require "nixio.fs" +local ipc = require "luci.ip" +local sys = require "luci.sys" +local utl = require "luci.util" +local dsp = require "luci.dispatcher" +local uci = require "luci.model.uci" +local lng = require "luci.i18n" + +module "luci.model.network" + + +IFACE_PATTERNS_VIRTUAL = { } +IFACE_PATTERNS_IGNORE = { "^wmaster%d", "^wifi%d", "^hwsim%d", "^imq%d", "^ifb%d", "^mon%.wlan%d", "^sit%d", "^gre%d", "^lo$" } +IFACE_PATTERNS_WIRELESS = { "^wlan%d", "^wl%d", "^ath%d", "^%w+%.network%d" } + + +protocol = utl.class() + +local _protocols = { } + +local _interfaces, _bridge, _switch, _tunnel +local _ubus, _ubusnetcache, _ubusdevcache, _ubuswificache +local _uci_real, _uci_state + +function _filter(c, s, o, r) + local val = _uci_real:get(c, s, o) + if val then + local l = { } + if type(val) == "string" then + for val in val:gmatch("%S+") do + if val ~= r then + l[#l+1] = val + end + end + if #l > 0 then + _uci_real:set(c, s, o, table.concat(l, " ")) + else + _uci_real:delete(c, s, o) + end + elseif type(val) == "table" then + for _, val in ipairs(val) do + if val ~= r then + l[#l+1] = val + end + end + if #l > 0 then + _uci_real:set(c, s, o, l) + else + _uci_real:delete(c, s, o) + end + end + end +end + +function _append(c, s, o, a) + local val = _uci_real:get(c, s, o) or "" + if type(val) == "string" then + local l = { } + for val in val:gmatch("%S+") do + if val ~= a then + l[#l+1] = val + end + end + l[#l+1] = a + _uci_real:set(c, s, o, table.concat(l, " ")) + elseif type(val) == "table" then + local l = { } + for _, val in ipairs(val) do + if val ~= a then + l[#l+1] = val + end + end + l[#l+1] = a + _uci_real:set(c, s, o, l) + end +end + +function _stror(s1, s2) + if not s1 or #s1 == 0 then + return s2 and #s2 > 0 and s2 + else + return s1 + end +end + +function _get(c, s, o) + return _uci_real:get(c, s, o) +end + +function _set(c, s, o, v) + if v ~= nil then + if type(v) == "boolean" then v = v and "1" or "0" end + return _uci_real:set(c, s, o, v) + else + return _uci_real:delete(c, s, o) + end +end + +function _wifi_iface(x) + local _, p + for _, p in ipairs(IFACE_PATTERNS_WIRELESS) do + if x:match(p) then + return true + end + end + return false +end + +function _wifi_state(key, val, field) + if not next(_ubuswificache) then + _ubuswificache = _ubus:call("network.wireless", "status", {}) or {} + end + + local radio, radiostate + for radio, radiostate in pairs(_ubuswificache) do + local ifc, ifcstate + for ifc, ifcstate in pairs(radiostate.interfaces) do + if ifcstate[key] == val then + return ifcstate[field] + end + end + end +end + +function _wifi_lookup(ifn) + -- got a radio#.network# pseudo iface, locate the corresponding section + local radio, ifnidx = ifn:match("^(%w+)%.network(%d+)$") + if radio and ifnidx then + local sid = nil + local num = 0 + + ifnidx = tonumber(ifnidx) + _uci_real:foreach("wireless", "wifi-iface", + function(s) + if s.device == radio then + num = num + 1 + if num == ifnidx then + sid = s['.name'] + return false + end + end + end) + + return sid + + -- looks like wifi, try to locate the section via state vars + elseif _wifi_iface(ifn) then + local sid = _wifi_state("ifname", ifn, "section") + if not sid then + _uci_state:foreach("wireless", "wifi-iface", + function(s) + if s.ifname == ifn then + sid = s['.name'] + return false + end + end) + end + + return sid + end +end + +function _iface_virtual(x) + local _, p + for _, p in ipairs(IFACE_PATTERNS_VIRTUAL) do + if x:match(p) then + return true + end + end + return false +end + +function _iface_ignore(x) + local _, p + for _, p in ipairs(IFACE_PATTERNS_IGNORE) do + if x:match(p) then + return true + end + end + return _iface_virtual(x) +end + + +function init(cursor) + _uci_real = cursor or _uci_real or uci.cursor() + _uci_state = _uci_real:substate() + + _interfaces = { } + _bridge = { } + _switch = { } + _tunnel = { } + + _ubus = bus.connect() + _ubusnetcache = { } + _ubusdevcache = { } + _ubuswificache = { } + + -- read interface information + local n, i + for n, i in ipairs(nxo.getifaddrs()) do + local name = i.name:match("[^:]+") + local prnt = name:match("^([^%.]+)%.") + + if _iface_virtual(name) then + _tunnel[name] = true + end + + if _tunnel[name] or not _iface_ignore(name) then + _interfaces[name] = _interfaces[name] or { + idx = i.ifindex or n, + name = name, + rawname = i.name, + flags = { }, + ipaddrs = { }, + ip6addrs = { } + } + + if prnt then + _switch[name] = true + _switch[prnt] = true + end + + if i.family == "packet" then + _interfaces[name].flags = i.flags + _interfaces[name].stats = i.data + _interfaces[name].macaddr = i.addr + elseif i.family == "inet" then + _interfaces[name].ipaddrs[#_interfaces[name].ipaddrs+1] = ipc.IPv4(i.addr, i.netmask) + elseif i.family == "inet6" then + _interfaces[name].ip6addrs[#_interfaces[name].ip6addrs+1] = ipc.IPv6(i.addr, i.netmask) + end + end + end + + -- read bridge informaton + local b, l + for l in utl.execi("brctl show") do + if not l:match("STP") then + local r = utl.split(l, "%s+", nil, true) + if #r == 4 then + b = { + name = r[1], + id = r[2], + stp = r[3] == "yes", + ifnames = { _interfaces[r[4]] } + } + if b.ifnames[1] then + b.ifnames[1].bridge = b + end + _bridge[r[1]] = b + elseif b then + b.ifnames[#b.ifnames+1] = _interfaces[r[2]] + b.ifnames[#b.ifnames].bridge = b + end + end + end + + return _M +end + +function save(self, ...) + _uci_real:save(...) + _uci_real:load(...) +end + +function commit(self, ...) + _uci_real:commit(...) + _uci_real:load(...) +end + +function ifnameof(self, x) + if utl.instanceof(x, interface) then + return x:name() + elseif utl.instanceof(x, protocol) then + return x:ifname() + elseif type(x) == "string" then + return x:match("^[^:]+") + end +end + +function get_protocol(self, protoname, netname) + local v = _protocols[protoname] + if v then + return v(netname or "__dummy__") + end +end + +function get_protocols(self) + local p = { } + local _, v + for _, v in ipairs(_protocols) do + p[#p+1] = v("__dummy__") + end + return p +end + +function register_protocol(self, protoname) + local proto = utl.class(protocol) + + function proto.__init__(self, name) + self.sid = name + end + + function proto.proto(self) + return protoname + end + + _protocols[#_protocols+1] = proto + _protocols[protoname] = proto + + return proto +end + +function register_pattern_virtual(self, pat) + IFACE_PATTERNS_VIRTUAL[#IFACE_PATTERNS_VIRTUAL+1] = pat +end + + +function has_ipv6(self) + return nfs.access("/proc/net/ipv6_route") +end + +function add_network(self, n, options) + local oldnet = self:get_network(n) + if n and #n > 0 and n:match("^[a-zA-Z0-9_]+$") and not oldnet then + if _uci_real:section("network", "interface", n, options) then + return network(n) + end + elseif oldnet and oldnet:is_empty() then + if options then + local k, v + for k, v in pairs(options) do + oldnet:set(k, v) + end + end + return oldnet + end +end + +function get_network(self, n) + if n and _uci_real:get("network", n) == "interface" then + return network(n) + end +end + +function get_networks(self) + local nets = { } + local nls = { } + + _uci_real:foreach("network", "interface", + function(s) + nls[s['.name']] = network(s['.name']) + end) + + local n + for n in utl.kspairs(nls) do + nets[#nets+1] = nls[n] + end + + return nets +end + +function del_network(self, n) + local r = _uci_real:delete("network", n) + if r then + _uci_real:delete_all("network", "alias", + function(s) return (s.interface == n) end) + + _uci_real:delete_all("network", "route", + function(s) return (s.interface == n) end) + + _uci_real:delete_all("network", "route6", + function(s) return (s.interface == n) end) + + _uci_real:foreach("wireless", "wifi-iface", + function(s) + local net + local rest = { } + for net in utl.imatch(s.network) do + if net ~= n then + rest[#rest+1] = net + end + end + if #rest > 0 then + _uci_real:set("wireless", s['.name'], "network", + table.concat(rest, " ")) + else + _uci_real:delete("wireless", s['.name'], "network") + end + end) + end + return r +end + +function rename_network(self, old, new) + local r + if new and #new > 0 and new:match("^[a-zA-Z0-9_]+$") and not self:get_network(new) then + r = _uci_real:section("network", "interface", new, _uci_real:get_all("network", old)) + + if r then + _uci_real:foreach("network", "alias", + function(s) + if s.interface == old then + _uci_real:set("network", s['.name'], "interface", new) + end + end) + + _uci_real:foreach("network", "route", + function(s) + if s.interface == old then + _uci_real:set("network", s['.name'], "interface", new) + end + end) + + _uci_real:foreach("network", "route6", + function(s) + if s.interface == old then + _uci_real:set("network", s['.name'], "interface", new) + end + end) + + _uci_real:foreach("wireless", "wifi-iface", + function(s) + local net + local list = { } + for net in utl.imatch(s.network) do + if net == old then + list[#list+1] = new + else + list[#list+1] = net + end + end + if #list > 0 then + _uci_real:set("wireless", s['.name'], "network", + table.concat(list, " ")) + end + end) + + _uci_real:delete("network", old) + end + end + return r or false +end + +function get_interface(self, i) + if _interfaces[i] or _wifi_iface(i) then + return interface(i) + else + local ifc + local num = { } + _uci_real:foreach("wireless", "wifi-iface", + function(s) + if s.device then + num[s.device] = num[s.device] and num[s.device] + 1 or 1 + if s['.name'] == i then + ifc = interface( + "%s.network%d" %{s.device, num[s.device] }) + return false + end + end + end) + return ifc + end +end + +function get_interfaces(self) + local iface + local ifaces = { } + local seen = { } + local nfs = { } + local baseof = { } + + -- find normal interfaces + _uci_real:foreach("network", "interface", + function(s) + for iface in utl.imatch(s.ifname) do + if not _iface_ignore(iface) and not _wifi_iface(iface) then + seen[iface] = true + nfs[iface] = interface(iface) + end + end + end) + + for iface in utl.kspairs(_interfaces) do + if not (seen[iface] or _iface_ignore(iface) or _wifi_iface(iface)) then + nfs[iface] = interface(iface) + end + end + + -- find vlan interfaces + _uci_real:foreach("network", "switch_vlan", + function(s) + if not s.device then + return + end + + local base = baseof[s.device] + if not base then + if not s.device:match("^eth%d") then + local l + for l in utl.execi("swconfig dev %q help 2>/dev/null" % s.device) do + if not base then + base = l:match("^%w+: (%w+)") + end + end + if not base or not base:match("^eth%d") then + base = "eth0" + end + else + base = s.device + end + baseof[s.device] = base + end + + local vid = tonumber(s.vid or s.vlan) + if vid ~= nil and vid >= 0 and vid <= 4095 then + local iface = "%s.%d" %{ base, vid } + if not seen[iface] then + seen[iface] = true + nfs[iface] = interface(iface) + end + end + end) + + for iface in utl.kspairs(nfs) do + ifaces[#ifaces+1] = nfs[iface] + end + + -- find wifi interfaces + local num = { } + local wfs = { } + _uci_real:foreach("wireless", "wifi-iface", + function(s) + if s.device then + num[s.device] = num[s.device] and num[s.device] + 1 or 1 + local i = "%s.network%d" %{ s.device, num[s.device] } + wfs[i] = interface(i) + end + end) + + for iface in utl.kspairs(wfs) do + ifaces[#ifaces+1] = wfs[iface] + end + + return ifaces +end + +function ignore_interface(self, x) + return _iface_ignore(x) +end + +function get_wifidev(self, dev) + if _uci_real:get("wireless", dev) == "wifi-device" then + return wifidev(dev) + end +end + +function get_wifidevs(self) + local devs = { } + local wfd = { } + + _uci_real:foreach("wireless", "wifi-device", + function(s) wfd[#wfd+1] = s['.name'] end) + + local dev + for _, dev in utl.vspairs(wfd) do + devs[#devs+1] = wifidev(dev) + end + + return devs +end + +function get_wifinet(self, net) + local wnet = _wifi_lookup(net) + if wnet then + return wifinet(wnet) + end +end + +function add_wifinet(self, net, options) + if type(options) == "table" and options.device and + _uci_real:get("wireless", options.device) == "wifi-device" + then + local wnet = _uci_real:section("wireless", "wifi-iface", nil, options) + return wifinet(wnet) + end +end + +function del_wifinet(self, net) + local wnet = _wifi_lookup(net) + if wnet then + _uci_real:delete("wireless", wnet) + return true + end + return false +end + +function get_status_by_route(self, addr, mask) + local _, object + for _, object in ipairs(_ubus:objects()) do + local net = object:match("^network%.interface%.(.+)") + if net then + local s = _ubus:call(object, "status", {}) + if s and s.route then + local rt + for _, rt in ipairs(s.route) do + if not rt.table and rt.target == addr and rt.mask == mask then + return net, s + end + end + end + end + end +end + +function get_status_by_address(self, addr) + local _, object + for _, object in ipairs(_ubus:objects()) do + local net = object:match("^network%.interface%.(.+)") + if net then + local s = _ubus:call(object, "status", {}) + if s and s['ipv4-address'] then + local a + for _, a in ipairs(s['ipv4-address']) do + if a.address == addr then + return net, s + end + end + end + if s and s['ipv6-address'] then + local a + for _, a in ipairs(s['ipv6-address']) do + if a.address == addr then + return net, s + end + end + end + end + end +end + +function get_wannet(self) + local net = self:get_status_by_route("0.0.0.0", 0) + return net and network(net) +end + +function get_wandev(self) + local _, stat = self:get_status_by_route("0.0.0.0", 0) + return stat and interface(stat.l3_device or stat.device) +end + +function get_wan6net(self) + local net = self:get_status_by_route("::", 0) + return net and network(net) +end + +function get_wan6dev(self) + local _, stat = self:get_status_by_route("::", 0) + return stat and interface(stat.l3_device or stat.device) +end + + +function network(name, proto) + if name then + local p = proto or _uci_real:get("network", name, "proto") + local c = p and _protocols[p] or protocol + return c(name) + end +end + +function protocol.__init__(self, name) + self.sid = name +end + +function protocol._get(self, opt) + local v = _uci_real:get("network", self.sid, opt) + if type(v) == "table" then + return table.concat(v, " ") + end + return v or "" +end + +function protocol._ubus(self, field) + if not _ubusnetcache[self.sid] then + _ubusnetcache[self.sid] = _ubus:call("network.interface.%s" % self.sid, + "status", { }) + end + if _ubusnetcache[self.sid] and field then + return _ubusnetcache[self.sid][field] + end + return _ubusnetcache[self.sid] +end + +function protocol.get(self, opt) + return _get("network", self.sid, opt) +end + +function protocol.set(self, opt, val) + return _set("network", self.sid, opt, val) +end + +function protocol.ifname(self) + local ifname + if self:is_floating() then + ifname = self:_ubus("l3_device") + else + ifname = self:_ubus("device") + end + if not ifname then + local num = { } + _uci_real:foreach("wireless", "wifi-iface", + function(s) + if s.device then + num[s.device] = num[s.device] + and num[s.device] + 1 or 1 + + local net + for net in utl.imatch(s.network) do + if net == self.sid then + ifname = "%s.network%d" %{ s.device, num[s.device] } + return false + end + end + end + end) + end + return ifname +end + +function protocol.proto(self) + return "none" +end + +function protocol.get_i18n(self) + local p = self:proto() + if p == "none" then + return lng.translate("Unmanaged") + elseif p == "static" then + return lng.translate("Static address") + elseif p == "dhcp" then + return lng.translate("DHCP client") + else + return lng.translate("Unknown") + end +end + +function protocol.type(self) + return self:_get("type") +end + +function protocol.name(self) + return self.sid +end + +function protocol.uptime(self) + return self:_ubus("uptime") or 0 +end + +function protocol.expires(self) + local a = tonumber(_uci_state:get("network", self.sid, "lease_acquired")) + local l = tonumber(_uci_state:get("network", self.sid, "lease_lifetime")) + if a and l then + l = l - (nxo.sysinfo().uptime - a) + return l > 0 and l or 0 + end + return -1 +end + +function protocol.metric(self) + return tonumber(_uci_state:get("network", self.sid, "metric")) or 0 +end + +function protocol.ipaddr(self) + local addrs = self:_ubus("ipv4-address") + return addrs and #addrs > 0 and addrs[1].address +end + +function protocol.netmask(self) + local addrs = self:_ubus("ipv4-address") + return addrs and #addrs > 0 and + ipc.IPv4("0.0.0.0/%d" % addrs[1].mask):mask():string() +end + +function protocol.gwaddr(self) + local _, route + for _, route in ipairs(self:_ubus("route") or { }) do + if route.target == "0.0.0.0" and route.mask == 0 then + return route.nexthop + end + end +end + +function protocol.dnsaddrs(self) + local dns = { } + local _, addr + for _, addr in ipairs(self:_ubus("dns-server") or { }) do + if not addr:match(":") then + dns[#dns+1] = addr + end + end + return dns +end + +function protocol.ip6addr(self) + local addrs = self:_ubus("ipv6-address") + if addrs and #addrs > 0 then + return "%s/%d" %{ addrs[1].address, addrs[1].mask } + else + addrs = self:_ubus("ipv6-prefix-assignment") + if addrs and #addrs > 0 then + return "%s/%d" %{ addrs[1].address, addrs[1].mask } + end + end +end + +function protocol.gw6addr(self) + local _, route + for _, route in ipairs(self:_ubus("route") or { }) do + if route.target == "::" and route.mask == 0 then + return ipc.IPv6(route.nexthop):string() + end + end +end + +function protocol.dns6addrs(self) + local dns = { } + local _, addr + for _, addr in ipairs(self:_ubus("dns-server") or { }) do + if addr:match(":") then + dns[#dns+1] = addr + end + end + return dns +end + +function protocol.is_bridge(self) + return (not self:is_virtual() and self:type() == "bridge") +end + +function protocol.opkg_package(self) + return nil +end + +function protocol.is_installed(self) + return true +end + +function protocol.is_virtual(self) + return false +end + +function protocol.is_floating(self) + return false +end + +function protocol.is_empty(self) + if self:is_floating() then + return false + else + local rv = true + + if (self:_get("ifname") or ""):match("%S+") then + rv = false + end + + _uci_real:foreach("wireless", "wifi-iface", + function(s) + local n + for n in utl.imatch(s.network) do + if n == self.sid then + rv = false + return false + end + end + end) + + return rv + end +end + +function protocol.add_interface(self, ifname) + ifname = _M:ifnameof(ifname) + if ifname and not self:is_floating() then + -- if its a wifi interface, change its network option + local wif = _wifi_lookup(ifname) + if wif then + _append("wireless", wif, "network", self.sid) + + -- add iface to our iface list + else + _append("network", self.sid, "ifname", ifname) + end + end +end + +function protocol.del_interface(self, ifname) + ifname = _M:ifnameof(ifname) + if ifname and not self:is_floating() then + -- if its a wireless interface, clear its network option + local wif = _wifi_lookup(ifname) + if wif then _filter("wireless", wif, "network", self.sid) end + + -- remove the interface + _filter("network", self.sid, "ifname", ifname) + end +end + +function protocol.get_interface(self) + if self:is_virtual() then + _tunnel[self:proto() .. "-" .. self.sid] = true + return interface(self:proto() .. "-" .. self.sid, self) + elseif self:is_bridge() then + _bridge["br-" .. self.sid] = true + return interface("br-" .. self.sid, self) + else + local ifn = nil + local num = { } + for ifn in utl.imatch(_uci_real:get("network", self.sid, "ifname")) do + ifn = ifn:match("^[^:/]+") + return ifn and interface(ifn, self) + end + ifn = nil + _uci_real:foreach("wireless", "wifi-iface", + function(s) + if s.device then + num[s.device] = num[s.device] and num[s.device] + 1 or 1 + + local net + for net in utl.imatch(s.network) do + if net == self.sid then + ifn = "%s.network%d" %{ s.device, num[s.device] } + return false + end + end + end + end) + return ifn and interface(ifn, self) + end +end + +function protocol.get_interfaces(self) + if self:is_bridge() or (self:is_virtual() and not self:is_floating()) then + local ifaces = { } + + local ifn + local nfs = { } + for ifn in utl.imatch(self:get("ifname")) do + ifn = ifn:match("^[^:/]+") + nfs[ifn] = interface(ifn, self) + end + + for ifn in utl.kspairs(nfs) do + ifaces[#ifaces+1] = nfs[ifn] + end + + local num = { } + local wfs = { } + _uci_real:foreach("wireless", "wifi-iface", + function(s) + if s.device then + num[s.device] = num[s.device] and num[s.device] + 1 or 1 + + local net + for net in utl.imatch(s.network) do + if net == self.sid then + ifn = "%s.network%d" %{ s.device, num[s.device] } + wfs[ifn] = interface(ifn, self) + end + end + end + end) + + for ifn in utl.kspairs(wfs) do + ifaces[#ifaces+1] = wfs[ifn] + end + + return ifaces + end +end + +function protocol.contains_interface(self, ifname) + ifname = _M:ifnameof(ifname) + if not ifname then + return false + elseif self:is_virtual() and self:proto() .. "-" .. self.sid == ifname then + return true + elseif self:is_bridge() and "br-" .. self.sid == ifname then + return true + else + local ifn + for ifn in utl.imatch(self:get("ifname")) do + ifn = ifn:match("[^:]+") + if ifn == ifname then + return true + end + end + + local wif = _wifi_lookup(ifname) + if wif then + local n + for n in utl.imatch(_uci_real:get("wireless", wif, "network")) do + if n == self.sid then + return true + end + end + end + end + + return false +end + +function protocol.adminlink(self) + return dsp.build_url("admin", "network", "network", self.sid) +end + + +interface = utl.class() + +function interface.__init__(self, ifname, network) + local wif = _wifi_lookup(ifname) + if wif then + self.wif = wifinet(wif) + self.ifname = _wifi_state("section", wif, "ifname") + end + + self.ifname = self.ifname or ifname + self.dev = _interfaces[self.ifname] + self.network = network +end + +function interface._ubus(self, field) + if not _ubusdevcache[self.ifname] then + _ubusdevcache[self.ifname] = _ubus:call("network.device", "status", + { name = self.ifname }) + end + if _ubusdevcache[self.ifname] and field then + return _ubusdevcache[self.ifname][field] + end + return _ubusdevcache[self.ifname] +end + +function interface.name(self) + return self.wif and self.wif:ifname() or self.ifname +end + +function interface.mac(self) + return (self:_ubus("macaddr") or "00:00:00:00:00:00"):upper() +end + +function interface.ipaddrs(self) + return self.dev and self.dev.ipaddrs or { } +end + +function interface.ip6addrs(self) + return self.dev and self.dev.ip6addrs or { } +end + +function interface.type(self) + if self.wif or _wifi_iface(self.ifname) then + return "wifi" + elseif _bridge[self.ifname] then + return "bridge" + elseif _tunnel[self.ifname] then + return "tunnel" + elseif self.ifname:match("%.") then + return "vlan" + elseif _switch[self.ifname] then + return "switch" + else + return "ethernet" + end +end + +function interface.shortname(self) + if self.wif then + return "%s %q" %{ + self.wif:active_mode(), + self.wif:active_ssid() or self.wif:active_bssid() + } + else + return self.ifname + end +end + +function interface.get_i18n(self) + if self.wif then + return "%s: %s %q" %{ + lng.translate("Wireless Network"), + self.wif:active_mode(), + self.wif:active_ssid() or self.wif:active_bssid() + } + else + return "%s: %q" %{ self:get_type_i18n(), self:name() } + end +end + +function interface.get_type_i18n(self) + local x = self:type() + if x == "wifi" then + return lng.translate("Wireless Adapter") + elseif x == "bridge" then + return lng.translate("Bridge") + elseif x == "switch" then + return lng.translate("Ethernet Switch") + elseif x == "vlan" then + return lng.translate("VLAN Interface") + elseif x == "tunnel" then + return lng.translate("Tunnel Interface") + else + return lng.translate("Ethernet Adapter") + end +end + +function interface.adminlink(self) + if self.wif then + return self.wif:adminlink() + end +end + +function interface.ports(self) + local members = self:_ubus("bridge-members") + if members then + local _, iface + local ifaces = { } + for _, iface in ipairs(members) do + ifaces[#ifaces+1] = interface(iface) + end + end +end + +function interface.bridge_id(self) + if self.br then + return self.br.id + else + return nil + end +end + +function interface.bridge_stp(self) + if self.br then + return self.br.stp + else + return false + end +end + +function interface.is_up(self) + return self:_ubus("up") or false +end + +function interface.is_bridge(self) + return (self:type() == "bridge") +end + +function interface.is_bridgeport(self) + return self.dev and self.dev.bridge and true or false +end + +function interface.tx_bytes(self) + local stat = self:_ubus("statistics") + return stat and stat.tx_bytes or 0 +end + +function interface.rx_bytes(self) + local stat = self:_ubus("statistics") + return stat and stat.rx_bytes or 0 +end + +function interface.tx_packets(self) + local stat = self:_ubus("statistics") + return stat and stat.tx_packets or 0 +end + +function interface.rx_packets(self) + local stat = self:_ubus("statistics") + return stat and stat.rx_packets or 0 +end + +function interface.get_network(self) + return self:get_networks()[1] +end + +function interface.get_networks(self) + if not self.networks then + local nets = { } + local _, net + for _, net in ipairs(_M:get_networks()) do + if net:contains_interface(self.ifname) or + net:ifname() == self.ifname + then + nets[#nets+1] = net + end + end + table.sort(nets, function(a, b) return a.sid < b.sid end) + self.networks = nets + return nets + else + return self.networks + end +end + +function interface.get_wifinet(self) + return self.wif +end + + +wifidev = utl.class() + +function wifidev.__init__(self, dev) + self.sid = dev + self.iwinfo = dev and sys.wifi.getiwinfo(dev) or { } +end + +function wifidev.get(self, opt) + return _get("wireless", self.sid, opt) +end + +function wifidev.set(self, opt, val) + return _set("wireless", self.sid, opt, val) +end + +function wifidev.name(self) + return self.sid +end + +function wifidev.hwmodes(self) + local l = self.iwinfo.hwmodelist + if l and next(l) then + return l + else + return { b = true, g = true } + end +end + +function wifidev.get_i18n(self) + local t = "Generic" + if self.iwinfo.type == "wl" then + t = "Broadcom" + elseif self.iwinfo.type == "madwifi" then + t = "Atheros" + end + + local m = "" + local l = self:hwmodes() + if l.a then m = m .. "a" end + if l.b then m = m .. "b" end + if l.g then m = m .. "g" end + if l.n then m = m .. "n" end + + return "%s 802.11%s Wireless Controller (%s)" %{ t, m, self:name() } +end + +function wifidev.is_up(self) + if _ubuswificache[self.sid] then + return (_ubuswificache[self.sid].up == true) + end + + local up = false + _uci_state:foreach("wireless", "wifi-iface", + function(s) + if s.device == self.sid then + if s.up == "1" then + up = true + return false + end + end + end) + + return up +end + +function wifidev.get_wifinet(self, net) + if _uci_real:get("wireless", net) == "wifi-iface" then + return wifinet(net) + else + local wnet = _wifi_lookup(net) + if wnet then + return wifinet(wnet) + end + end +end + +function wifidev.get_wifinets(self) + local nets = { } + + _uci_real:foreach("wireless", "wifi-iface", + function(s) + if s.device == self.sid then + nets[#nets+1] = wifinet(s['.name']) + end + end) + + return nets +end + +function wifidev.add_wifinet(self, options) + options = options or { } + options.device = self.sid + + local wnet = _uci_real:section("wireless", "wifi-iface", nil, options) + if wnet then + return wifinet(wnet, options) + end +end + +function wifidev.del_wifinet(self, net) + if utl.instanceof(net, wifinet) then + net = net.sid + elseif _uci_real:get("wireless", net) ~= "wifi-iface" then + net = _wifi_lookup(net) + end + + if net and _uci_real:get("wireless", net, "device") == self.sid then + _uci_real:delete("wireless", net) + return true + end + + return false +end + + +wifinet = utl.class() + +function wifinet.__init__(self, net, data) + self.sid = net + + local num = { } + local netid + _uci_real:foreach("wireless", "wifi-iface", + function(s) + if s.device then + num[s.device] = num[s.device] and num[s.device] + 1 or 1 + if s['.name'] == self.sid then + netid = "%s.network%d" %{ s.device, num[s.device] } + return false + end + end + end) + + local dev = _wifi_state("section", self.sid, "ifname") or netid + + self.netid = netid + self.wdev = dev + self.iwinfo = dev and sys.wifi.getiwinfo(dev) or { } + self.iwdata = data or _uci_state:get_all("wireless", self.sid) or + _uci_real:get_all("wireless", self.sid) or { } +end + +function wifinet.get(self, opt) + return _get("wireless", self.sid, opt) +end + +function wifinet.set(self, opt, val) + return _set("wireless", self.sid, opt, val) +end + +function wifinet.mode(self) + return _uci_state:get("wireless", self.sid, "mode") or "ap" +end + +function wifinet.ssid(self) + return _uci_state:get("wireless", self.sid, "ssid") +end + +function wifinet.bssid(self) + return _uci_state:get("wireless", self.sid, "bssid") +end + +function wifinet.network(self) + return _uci_state:get("wifinet", self.sid, "network") +end + +function wifinet.id(self) + return self.netid +end + +function wifinet.name(self) + return self.sid +end + +function wifinet.ifname(self) + local ifname = self.iwinfo.ifname + if not ifname or ifname:match("^wifi%d") or ifname:match("^radio%d") then + ifname = self.wdev + end + return ifname +end + +function wifinet.get_device(self) + if self.iwdata.device then + return wifidev(self.iwdata.device) + end +end + +function wifinet.is_up(self) + local ifc = self:get_interface() + return (ifc and ifc:is_up() or false) +end + +function wifinet.active_mode(self) + local m = _stror(self.iwinfo.mode, self.iwdata.mode) or "ap" + + if m == "ap" then m = "Master" + elseif m == "sta" then m = "Client" + elseif m == "adhoc" then m = "Ad-Hoc" + elseif m == "mesh" then m = "Mesh" + elseif m == "monitor" then m = "Monitor" + end + + return m +end + +function wifinet.active_mode_i18n(self) + return lng.translate(self:active_mode()) +end + +function wifinet.active_ssid(self) + return _stror(self.iwinfo.ssid, self.iwdata.ssid) +end + +function wifinet.active_bssid(self) + return _stror(self.iwinfo.bssid, self.iwdata.bssid) or "00:00:00:00:00:00" +end + +function wifinet.active_encryption(self) + local enc = self.iwinfo and self.iwinfo.encryption + return enc and enc.description or "-" +end + +function wifinet.assoclist(self) + return self.iwinfo.assoclist or { } +end + +function wifinet.frequency(self) + local freq = self.iwinfo.frequency + if freq and freq > 0 then + return "%.03f" % (freq / 1000) + end +end + +function wifinet.bitrate(self) + local rate = self.iwinfo.bitrate + if rate and rate > 0 then + return (rate / 1000) + end +end + +function wifinet.channel(self) + return self.iwinfo.channel or + tonumber(_uci_state:get("wireless", self.iwdata.device, "channel")) +end + +function wifinet.signal(self) + return self.iwinfo.signal or 0 +end + +function wifinet.noise(self) + return self.iwinfo.noise or 0 +end + +function wifinet.country(self) + return self.iwinfo.country or "00" +end + +function wifinet.txpower(self) + local pwr = (self.iwinfo.txpower or 0) + return pwr + self:txpower_offset() +end + +function wifinet.txpower_offset(self) + return self.iwinfo.txpower_offset or 0 +end + +function wifinet.signal_level(self, s, n) + if self:active_bssid() ~= "00:00:00:00:00:00" then + local signal = s or self:signal() + local noise = n or self:noise() + + if signal < 0 and noise < 0 then + local snr = -1 * (noise - signal) + return math.floor(snr / 5) + else + return 0 + end + else + return -1 + end +end + +function wifinet.signal_percent(self) + local qc = self.iwinfo.quality or 0 + local qm = self.iwinfo.quality_max or 0 + + if qc > 0 and qm > 0 then + return math.floor((100 / qm) * qc) + else + return 0 + end +end + +function wifinet.shortname(self) + return "%s %q" %{ + lng.translate(self:active_mode()), + self:active_ssid() or self:active_bssid() + } +end + +function wifinet.get_i18n(self) + return "%s: %s %q (%s)" %{ + lng.translate("Wireless Network"), + lng.translate(self:active_mode()), + self:active_ssid() or self:active_bssid(), + self:ifname() + } +end + +function wifinet.adminlink(self) + return dsp.build_url("admin", "network", "wireless", self.netid) +end + +function wifinet.get_network(self) + return self:get_networks()[1] +end + +function wifinet.get_networks(self) + local nets = { } + local net + for net in utl.imatch(tostring(self.iwdata.network)) do + if _uci_real:get("network", net) == "interface" then + nets[#nets+1] = network(net) + end + end + table.sort(nets, function(a, b) return a.sid < b.sid end) + return nets +end + +function wifinet.get_interface(self) + return interface(self:ifname()) +end + + +-- setup base protocols +_M:register_protocol("static") +_M:register_protocol("dhcp") +_M:register_protocol("none") + +-- load protocol extensions +local exts = nfs.dir(utl.libpath() .. "/model/network") +if exts then + local ext + for ext in exts do + if ext:match("%.lua$") then + require("luci.model.network." .. ext:gsub("%.lua$", "")) + end + end +end diff --git a/modules/base/luasrc/model/uci.lua b/modules/base/luasrc/model/uci.lua new file mode 100644 index 0000000000..a394563047 --- /dev/null +++ b/modules/base/luasrc/model/uci.lua @@ -0,0 +1,404 @@ +--[[ +LuCI - UCI model + +Description: +Generalized UCI model + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- +local os = require "os" +local uci = require "uci" +local util = require "luci.util" +local table = require "table" + + +local setmetatable, rawget, rawset = setmetatable, rawget, rawset +local require, getmetatable = require, getmetatable +local error, pairs, ipairs = error, pairs, ipairs +local type, tostring, tonumber, unpack = type, tostring, tonumber, unpack + +--- 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 deamons etc. are +-- reloaded. +-- @cstyle instance +module "luci.model.uci" + +--- Create a new UCI-Cursor. +-- @class function +-- @name cursor +-- @return UCI-Cursor +cursor = uci.cursor + +APIVERSION = uci.APIVERSION + +--- Create a new Cursor initialized to the state directory. +-- @return UCI cursor +function cursor_state() + return cursor(nil, "/var/state") +end + + +inst = cursor() +inst_state = cursor_state() + +local Cursor = getmetatable(inst) + +--- Applies UCI configuration changes +-- @param configlist List of UCI configurations +-- @param command Don't apply only return the command +function Cursor.apply(self, configlist, command) + configlist = self:_affected(configlist) + if command then + return { "/sbin/luci-reload", unpack(configlist) } + else + return os.execute("/sbin/luci-reload %s >/dev/null 2>&1" + % table.concat(configlist, " ")) + end +end + + +--- Delete all sections of a given type that match certain criteria. +-- @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) +function Cursor.delete_all(self, config, stype, comparator) + local del = {} + + if type(comparator) == "table" then + local tbl = comparator + comparator = function(section) + for k, v in pairs(tbl) do + if section[k] ~= v then + return false + end + end + return true + end + end + + local function helper (section) + + if not comparator or comparator(section) then + del[#del+1] = section[".name"] + end + end + + self:foreach(config, stype, helper) + + for i, j in ipairs(del) do + self:delete(config, j) + end +end + +--- Create a new section and initialize it with data. +-- @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 +function Cursor.section(self, config, type, name, values) + local stat = true + if name then + stat = self:set(config, name, type) + else + name = self:add(config, type) + stat = name and true + end + + if stat and values then + stat = self:tset(config, name, values) + end + + return stat and name +end + +--- Updated the data of a section using data from a table. +-- @param config UCI config +-- @param section UCI section name (optional) +-- @param values Table of key - value pairs to update the section with +function Cursor.tset(self, config, section, values) + local stat = true + for k, v in pairs(values) do + if k:sub(1, 1) ~= "." then + stat = stat and self:set(config, section, k, v) + end + end + return stat +end + +--- Get a boolean option and return it's value as true or false. +-- @param config UCI config +-- @param section UCI section name +-- @param option UCI option +-- @return Boolean +function Cursor.get_bool(self, ...) + local val = self:get(...) + return ( val == "1" or val == "true" or val == "yes" or val == "on" ) +end + +--- Get an option or list and return values as table. +-- @param config UCI config +-- @param section UCI section name +-- @param option UCI option +-- @return UCI value +function Cursor.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 nil +end + +--- Get the given option from the first section with the given type. +-- @param config UCI config +-- @param type UCI section type +-- @param option UCI option (optional) +-- @param default Default value (optional) +-- @return UCI value +function Cursor.get_first(self, conf, stype, opt, def) + local rv = def + + self:foreach(conf, stype, + function(s) + local val = not opt and s['.name'] or s[opt] + + if type(def) == "number" then + val = tonumber(val) + elseif type(def) == "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 + +--- Set given values as list. +-- @param config UCI config +-- @param section UCI section name +-- @param option UCI option +-- @param value UCI value +-- @return Boolean whether operation succeeded +function Cursor.set_list(self, config, section, option, value) + if config and section and option then + return self:set( + config, section, option, + ( type(value) == "table" and value or { value } ) + ) + end + return false +end + +-- Return a list of initscripts affected by configuration changes. +function Cursor._affected(self, configlist) + configlist = type(configlist) == "table" and configlist or {configlist} + + local c = cursor() + c:load("ucitrack") + + -- Resolve dependencies + local reloadlist = {} + + local function _resolve_deps(name) + local reload = {name} + local deps = {} + + c:foreach("ucitrack", name, + function(section) + if section.affects then + for i, aff in ipairs(section.affects) do + deps[#deps+1] = aff + end + end + end) + + for i, dep in ipairs(deps) do + for j, add in ipairs(_resolve_deps(dep)) do + reload[#reload+1] = add + end + end + + return reload + end + + -- Collect initscripts + for j, config in ipairs(configlist) do + for i, e in ipairs(_resolve_deps(config)) do + if not util.contains(reloadlist, e) then + reloadlist[#reloadlist+1] = e + end + end + end + + return reloadlist +end + +--- Create a sub-state of this cursor. The sub-state is tied to the parent +-- curser, means it the parent unloads or loads configs, the sub state will +-- do so as well. +-- @return UCI state cursor tied to the parent cursor +function Cursor.substate(self) + Cursor._substates = Cursor._substates or { } + Cursor._substates[self] = Cursor._substates[self] or cursor_state() + return Cursor._substates[self] +end + +local _load = Cursor.load +function Cursor.load(self, ...) + if Cursor._substates and Cursor._substates[self] then + _load(Cursor._substates[self], ...) + end + return _load(self, ...) +end + +local _unload = Cursor.unload +function Cursor.unload(self, ...) + if Cursor._substates and Cursor._substates[self] then + _unload(Cursor._substates[self], ...) + end + return _unload(self, ...) +end + + +--- 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. +-- @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 nil 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 + +--- Set the configuration directory. +-- @class function +-- @name Cursor.set_confdir +-- @param directory UCI configuration directory +-- @return Boolean whether operation succeeded + +--- Set the directory for uncommited changes. +-- @class function +-- @name Cursor.set_savedir +-- @param directory UCI changes directory +-- @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 diff --git a/modules/base/luasrc/sgi/cgi.lua b/modules/base/luasrc/sgi/cgi.lua new file mode 100644 index 0000000000..04ae9aa592 --- /dev/null +++ b/modules/base/luasrc/sgi/cgi.lua @@ -0,0 +1,95 @@ +--[[ +LuCI - SGI-Module for CGI + +Description: +Server Gateway Interface for CGI + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- +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 diff --git a/modules/base/luasrc/sgi/uhttpd.lua b/modules/base/luasrc/sgi/uhttpd.lua new file mode 100644 index 0000000000..db8780f7ec --- /dev/null +++ b/modules/base/luasrc/sgi/uhttpd.lua @@ -0,0 +1,105 @@ +--[[ +LuCI - Server Gateway Interface for the uHTTPd server + +Copyright 2010 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +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 + } + + 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 diff --git a/modules/base/luasrc/store.lua b/modules/base/luasrc/store.lua new file mode 100644 index 0000000000..c33ef07e11 --- /dev/null +++ b/modules/base/luasrc/store.lua @@ -0,0 +1,16 @@ +--[[ + +LuCI - Lua Development Framework +(c) 2009 Steven Barth <steven@midlink.org> +(c) 2009 Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +]]-- + +local util = require "luci.util" +module("luci.store", util.threadlocal)
\ No newline at end of file diff --git a/modules/base/luasrc/sys.lua b/modules/base/luasrc/sys.lua new file mode 100644 index 0000000000..df6280dda0 --- /dev/null +++ b/modules/base/luasrc/sys.lua @@ -0,0 +1,961 @@ +--[[ +LuCI - System library + +Description: +Utilities for interaction with the Linux system + +FileId: +$Id$ + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + + +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 = + tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select + + +--- 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 +function call(...) + return os.execute(...) / 256 +end + +--- Execute a given shell command and capture its standard output +-- @class function +-- @name exec +-- @param command Command to call +-- @return String containg the return the output of the command +exec = luci.util.exec + +--- Retrieve information about currently mounted file systems. +-- @return Table containing mount information +function mounts() + local data = {} + local k = {"fs", "blocks", "used", "available", "percent", "mountpoint"} + local ps = luci.util.execi("df") + + if not ps then + return + else + ps() + end + + for line in ps do + local row = {} + + local j = 1 + for value in line:gmatch("[^%s]+") do + row[k[j]] = value + j = j + 1 + end + + if row[k[1]] then + + -- this is a rather ugly workaround to cope with wrapped lines in + -- the df output: + -- + -- /dev/scsi/host0/bus0/target0/lun0/part3 + -- 114382024 93566472 15005244 86% /mnt/usb + -- + + if not row[k[2]] then + j = 2 + line = ps() + for value in line:gmatch("[^%s]+") do + row[k[j]] = value + j = j + 1 + end + end + + table.insert(data, row) + end + end + + return data +end + +--- 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 containg the value of the specified variable +-- @return Table containing all variables if no variable name is given +getenv = nixio.getenv + +--- Get or set the current hostname. +-- @param String containing a new hostname to set (optional) +-- @return String containing the system hostname +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 + +--- Returns the contents of a documented referred by an URL. +-- @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 +function httpget(url, stream, target) + if not target then + local source = stream and io.popen or luci.util.exec + return source("wget -qO- '"..url:gsub("'", "").."'") + else + return os.execute("wget -qO '%s' '%s'" % + {target:gsub("'", ""), url:gsub("'", "")}) + end +end + +--- Returns the system load average values. +-- @return String containing the average load value 1 minute ago +-- @return String containing the average load value 5 minutes ago +-- @return String containing the average load value 15 minutes ago +function loadavg() + local info = nixio.sysinfo() + return info.loads[1], info.loads[2], info.loads[3] +end + +--- Initiate a system reboot. +-- @return Return value of os.execute() +function reboot() + return os.execute("reboot >/dev/null 2>&1") +end + +--- Returns the system type, cpu name and installed physical memory. +-- @return String containing the system or platform identifier +-- @return String containing hardware model information +-- @return String containing the total memory amount in kB +-- @return String containing the memory used for caching in kB +-- @return String containing the memory used for buffering in kB +-- @return String containing the free memory amount in kB +-- @return String containing the cpu bogomips (number) +function sysinfo() + local cpuinfo = fs.readfile("/proc/cpuinfo") + local meminfo = fs.readfile("/proc/meminfo") + + local memtotal = tonumber(meminfo:match("MemTotal:%s*(%d+)")) + local memcached = tonumber(meminfo:match("\nCached:%s*(%d+)")) + local memfree = tonumber(meminfo:match("MemFree:%s*(%d+)")) + local membuffers = tonumber(meminfo:match("Buffers:%s*(%d+)")) + local bogomips = tonumber(cpuinfo:match("[Bb]ogo[Mm][Ii][Pp][Ss].-: ([^\n]+)")) or 0 + local swaptotal = tonumber(meminfo:match("SwapTotal:%s*(%d+)")) + local swapcached = tonumber(meminfo:match("SwapCached:%s*(%d+)")) + local swapfree = tonumber(meminfo:match("SwapFree:%s*(%d+)")) + + local system = + cpuinfo:match("system type\t+: ([^\n]+)") or + cpuinfo:match("Processor\t+: ([^\n]+)") or + cpuinfo:match("model name\t+: ([^\n]+)") + + local model = + luci.util.pcdata(fs.readfile("/tmp/sysinfo/model")) or + cpuinfo:match("machine\t+: ([^\n]+)") or + cpuinfo:match("Hardware\t+: ([^\n]+)") or + luci.util.pcdata(fs.readfile("/proc/diag/model")) or + nixio.uname().machine or + system + + return system, model, memtotal, memcached, membuffers, memfree, bogomips, swaptotal, swapcached, swapfree +end + +--- Retrieves the output of the "logread" command. +-- @return String containing the current log buffer +function syslog() + return luci.util.exec("logread") +end + +--- Retrieves the output of the "dmesg" command. +-- @return String containing the current log buffer +function dmesg() + return luci.util.exec("dmesg") +end + +--- Generates a random id with specified length. +-- @param bytes Number of bytes for the unique id +-- @return String containing hex encoded id +function uniqueid(bytes) + local rand = fs.readfile("/dev/urandom", bytes) + return rand and nixio.bin.hexlify(rand) +end + +--- Returns the current system uptime stats. +-- @return String containing total uptime in seconds +function uptime() + return nixio.sysinfo().uptime +end + + +--- LuCI system utilities / network related functions. +-- @class module +-- @name luci.sys.net +net = {} + +--- Returns the current arp-table entries as two-dimensional table. +-- @return Table of table containing the current arp entries. +-- The following fields are defined for arp entry objects: +-- { "IP address", "HW address", "HW type", "Flags", "Mask", "Device" } +function net.arptable(callback) + local arp = (not callback) and {} or nil + local e, r, v + if fs.access("/proc/net/arp") then + for e in io.lines("/proc/net/arp") do + local r = { }, v + for v in e:gmatch("%S+") do + r[#r+1] = v + end + + if r[1] ~= "IP" then + local x = { + ["IP address"] = r[1], + ["HW type"] = r[2], + ["Flags"] = r[3], + ["HW address"] = r[4], + ["Mask"] = r[5], + ["Device"] = r[6] + } + + if callback then + callback(x) + else + arp = arp or { } + arp[#arp+1] = x + end + end + end + end + return arp +end + +local function _nethints(what, callback) + local _, k, e, mac, ip, name + local cur = uci.cursor() + local ifn = { } + local hosts = { } + + 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 + + if fs.access("/proc/net/arp") then + for e in io.lines("/proc/net/arp") do + ip, mac = e:match("^([%d%.]+)%s+%S+%s+%S+%s+([a-fA-F0-9:]+)%s+") + if ip and mac then + _add(what, mac:upper(), ip, nil, nil) + end + end + end + + if fs.access("/etc/ethers") then + for e in io.lines("/etc/ethers") do + mac, ip = e:match("^([a-f0-9]%S+) (%S+)") + if mac and ip then + _add(what, mac:upper(), ip, nil, nil) + end + end + end + + if fs.access("/var/dhcp.leases") then + for e in io.lines("/var/dhcp.leases") do + mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)") + if mac and ip then + _add(what, mac:upper(), ip, nil, name ~= "*" and name) + end + end + end + + cur:foreach("dhcp", "host", + function(s) + for mac in luci.util.imatch(s.mac) do + _add(what, mac:upper(), s.ip, nil, s.name) + 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 luci.util.kspairs(hosts) do + callback(e[1], e[2], e[3], e[4]) + end +end + +--- Returns a two-dimensional table of mac address hints. +-- @return Table of table containing known hosts from various sources. +-- 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 nixio.getnameinfo(v4 or v6, nil, 100) or v4 + if name and name ~= mac then + callback(mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4) + end + end) + else + local rv = { } + _nethints(1, function(mac, v4, v6, name) + name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4 + if name and name ~= mac then + rv[#rv+1] = { mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4 } + end + end) + return rv + end +end + +--- Returns a two-dimensional table of IPv4 address hints. +-- @return Table of table containing known hosts from various sources. +-- 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 nixio.getnameinfo(v4, nil, 100) 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 nixio.getnameinfo(v4, nil, 100) or mac + if name and name ~= v4 then + rv[#rv+1] = { v4, name } + end + end) + return rv + end +end + +--- Returns a two-dimensional table of IPv6 address hints. +-- @return Table of table containing known hosts from various sources. +-- 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 nixio.getnameinfo(v6, nil, 100) 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 nixio.getnameinfo(v6, nil, 100) or mac + if name and name ~= v6 then + rv[#rv+1] = { v6, name } + end + end) + return rv + end +end + +--- Returns conntrack information +-- @return Table with the currently tracked IP connections +function net.conntrack(callback) + local connt = {} + if fs.access("/proc/net/nf_conntrack", "r") then + for line in io.lines("/proc/net/nf_conntrack") do + line = line:match "^(.-( [^ =]+=).-)%2" + local entry, flags = _parse_mixed_record(line, " +") + if flags[6] ~= "TIME_WAIT" then + entry.layer3 = flags[1] + entry.layer4 = flags[3] + for i=1, #entry do + entry[i] = nil + end + + if callback then + callback(entry) + else + connt[#connt+1] = entry + end + end + end + elseif fs.access("/proc/net/ip_conntrack", "r") then + for line in io.lines("/proc/net/ip_conntrack") do + line = line:match "^(.-( [^ =]+=).-)%2" + local entry, flags = _parse_mixed_record(line, " +") + if flags[4] ~= "TIME_WAIT" then + entry.layer3 = "ipv4" + entry.layer4 = flags[1] + for i=1, #entry do + entry[i] = nil + end + + if callback then + callback(entry) + else + connt[#connt+1] = entry + end + end + end + else + return nil + end + return connt +end + +--- Determine the current IPv4 default route. If multiple default routes exist, +-- return the one with the lowest metric. +-- @return Table with the properties of the current default route. +-- The following fields are defined: +-- { "dest", "gateway", "metric", "refcount", "usecount", "irtt", +-- "flags", "device" } +function net.defaultroute() + local route + + net.routes(function(rt) + if rt.dest:prefix() == 0 and (not route or route.metric > rt.metric) then + route = rt + end + end) + + return route +end + +--- Determine the current IPv6 default route. If multiple default routes exist, +-- return the one with the lowest metric. +-- @return Table with the properties of the current default route. +-- The following fields are defined: +-- { "source", "dest", "nexthop", "metric", "refcount", "usecount", +-- "flags", "device" } +function net.defaultroute6() + local route + + net.routes6(function(rt) + if rt.dest:prefix() == 0 and rt.device ~= "lo" and + (not route or route.metric > rt.metric) + then + route = rt + end + end) + + if not route then + local global_unicast = luci.ip.IPv6("2000::/3") + net.routes6(function(rt) + if rt.dest:equal(global_unicast) and + (not route or route.metric > rt.metric) + then + route = rt + end + end) + end + + return route +end + +--- Determine the names of available network interfaces. +-- @return Table containing all current interface names +function net.devices() + local devs = {} + for k, v in ipairs(nixio.getifaddrs()) do + if v.family == "packet" then + devs[#devs+1] = v.name + end + end + return devs +end + + +--- Return information about available network interfaces. +-- @return Table containing all current interface names and their information +function net.deviceinfo() + local devs = {} + for k, v in ipairs(nixio.getifaddrs()) do + if v.family == "packet" then + local d = v.data + d[1] = d.rx_bytes + d[2] = d.rx_packets + d[3] = d.rx_errors + d[4] = d.rx_dropped + d[5] = 0 + d[6] = 0 + d[7] = 0 + d[8] = d.multicast + d[9] = d.tx_bytes + d[10] = d.tx_packets + d[11] = d.tx_errors + d[12] = d.tx_dropped + d[13] = 0 + d[14] = d.collisions + d[15] = 0 + d[16] = 0 + devs[v.name] = d + end + end + return devs +end + + +-- Determine the MAC address belonging to the given IP address. +-- @param ip IPv4 address +-- @return String containing the MAC address or nil if it cannot be found +function net.ip4mac(ip) + local mac = nil + net.arptable(function(e) + if e["IP address"] == ip then + mac = e["HW address"] + end + end) + return mac +end + +--- Returns the current kernel routing table entries. +-- @return Table of tables with properties of the corresponding routes. +-- The following fields are defined for route entry tables: +-- { "dest", "gateway", "metric", "refcount", "usecount", "irtt", +-- "flags", "device" } +function net.routes(callback) + local routes = { } + + for line in io.lines("/proc/net/route") do + + local dev, dst_ip, gateway, flags, refcnt, usecnt, metric, + dst_mask, mtu, win, irtt = line:match( + "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" .. + "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)" + ) + + if dev then + gateway = luci.ip.Hex( gateway, 32, luci.ip.FAMILY_INET4 ) + dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 ) + dst_ip = luci.ip.Hex( + dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4 + ) + + local rt = { + dest = dst_ip, + gateway = gateway, + metric = tonumber(metric), + refcount = tonumber(refcnt), + usecount = tonumber(usecnt), + mtu = tonumber(mtu), + window = tonumber(window), + irtt = tonumber(irtt), + flags = tonumber(flags, 16), + device = dev + } + + if callback then + callback(rt) + else + routes[#routes+1] = rt + end + end + end + + return routes +end + +--- Returns the current ipv6 kernel routing table entries. +-- @return Table of tables with properties of the corresponding routes. +-- The following fields are defined for route entry tables: +-- { "source", "dest", "nexthop", "metric", "refcount", "usecount", +-- "flags", "device" } +function net.routes6(callback) + if fs.access("/proc/net/ipv6_route", "r") then + local routes = { } + + for line in io.lines("/proc/net/ipv6_route") do + + local dst_ip, dst_prefix, src_ip, src_prefix, nexthop, + metric, refcnt, usecnt, flags, dev = line:match( + "([a-f0-9]+) ([a-f0-9]+) " .. + "([a-f0-9]+) ([a-f0-9]+) " .. + "([a-f0-9]+) ([a-f0-9]+) " .. + "([a-f0-9]+) ([a-f0-9]+) " .. + "([a-f0-9]+) +([^%s]+)" + ) + + if dst_ip and dst_prefix and + src_ip and src_prefix and + nexthop and metric and + refcnt and usecnt and + flags and dev + then + src_ip = luci.ip.Hex( + src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false + ) + + dst_ip = luci.ip.Hex( + dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false + ) + + nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false ) + + local rt = { + source = src_ip, + dest = dst_ip, + nexthop = nexthop, + metric = tonumber(metric, 16), + refcount = tonumber(refcnt, 16), + usecount = tonumber(usecnt, 16), + flags = tonumber(flags, 16), + device = dev, + + -- lua number is too small for storing the metric + -- add a metric_raw field with the original content + metric_raw = metric + } + + if callback then + callback(rt) + else + routes[#routes+1] = rt + end + end + end + + return routes + end +end + +--- Tests whether the given host responds to ping probes. +-- @param host String containing a hostname or IPv4 address +-- @return Number containing 0 on success and >= 1 on error +function net.pingtest(host) + return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1") +end + + +--- LuCI system utilities / process related functions. +-- @class module +-- @name luci.sys.process +process = {} + +--- Get the current process id. +-- @class function +-- @name process.info +-- @return Number containing the current pid +function process.info(key) + local s = {uid = nixio.getuid(), gid = nixio.getgid()} + return not key and s or s[key] +end + +--- Retrieve information about currently running processes. +-- @return Table containing process information +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][W ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)" + ) + + local idx = tonumber(pid) + if idx then + data[idx] = { + ['PID'] = pid, + ['PPID'] = ppid, + ['USER'] = user, + ['STAT'] = stat, + ['VSZ'] = vsz, + ['%MEM'] = mem, + ['%CPU'] = cpu, + ['COMMAND'] = cmd + } + end + end + + return data +end + +--- Set the gid of a process identified by given pid. +-- @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 +function process.setgroup(gid) + return nixio.setgid(gid) +end + +--- Set the uid of a process identified by given pid. +-- @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 +function process.setuser(uid) + return nixio.setuid(uid) +end + +--- 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 +process.signal = nixio.kill + + +--- LuCI system utilities / user related functions. +-- @class module +-- @name luci.sys.user +user = {} + +--- Retrieve user informations 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" } +user.getuser = nixio.getpw + +--- Retrieve the current user password hash. +-- @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 +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 or pwh == "!" or pwh == "x" then + return nil, pwe + else + return pwh, pwe + end +end + +--- Test whether given string matches the password of a given system user. +-- @param username String containing the Unix user name +-- @param pass String containing the password to compare +-- @return Boolean indicating wheather the passwords are equal +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 + +--- Change the password of given user. +-- @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 +function user.setpasswd(username, password) + if password then + password = password:gsub("'", [['"'"']]) + end + + if username then + username = username:gsub("'", [['"'"']]) + end + + return os.execute( + "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " .. + "passwd '" .. username .. "' >/dev/null 2>&1" + ) +end + + +--- LuCI system utilities / wifi related functions. +-- @class module +-- @name luci.sys.wifi +wifi = {} + +--- Get wireless information for given interface. +-- @param ifname String containing the interface name +-- @return A wrapped iwinfo object instance +function wifi.getiwinfo(ifname) + local stat, iwinfo = pcall(require, "iwinfo") + + if ifname then + local c = 0 + local u = uci.cursor_state() + local d, n = ifname:match("^(%w+)%.network(%d+)") + if d and n then + ifname = d + n = tonumber(n) + u:foreach("wireless", "wifi-iface", + function(s) + if s.device == d then + c = c + 1 + if c == n then + ifname = s.ifname or s.device + return false + end + end + end) + elseif u:get("wireless", ifname) == "wifi-device" then + u:foreach("wireless", "wifi-iface", + function(s) + if s.device == ifname and s.ifname then + ifname = s.ifname + return false + end + end) + end + + local t = stat and iwinfo.type(ifname) + local x = t and iwinfo[t] or { } + return setmetatable({}, { + __index = function(t, k) + if k == "ifname" then + return ifname + elseif x[k] then + return x[k](ifname) + end + end + }) + end +end + + +--- LuCI system utilities / init related functions. +-- @class module +-- @name luci.sys.init +init = {} +init.dir = "/etc/init.d/" + +--- Get the names of all installed init scripts +-- @return Table containing the names of all inistalled init scripts +function init.names() + local names = { } + for name in fs.glob(init.dir.."*") do + names[#names+1] = fs.basename(name) + end + return names +end + +--- Get the index of he given init script +-- @param name Name of the init script +-- @return Numeric index value +function init.index(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) + if fs.access(init.dir..name) then + return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action }) + end +end + +--- Test whether the given init script is enabled +-- @param name Name of the init script +-- @return Boolean indicating whether init is enabled +function init.enabled(name) + return (init_action("enabled", name) == 0) +end + +--- Enable the given init script +-- @param name Name of the init script +-- @return Boolean indicating success +function init.enable(name) + return (init_action("enable", name) == 1) +end + +--- Disable the given init script +-- @param name Name of the init script +-- @return Boolean indicating success +function init.disable(name) + return (init_action("disable", name) == 0) +end + +--- Start the given init script +-- @param name Name of the init script +-- @return Boolean indicating success +function init.start(name) + return (init_action("start", name) == 0) +end + +--- Stop the given init script +-- @param name Name of the init script +-- @return Boolean indicating success +function init.stop(name) + return (init_action("stop", name) == 0) +end + + +-- Internal functions + +function _parse_mixed_record(cnt, delimiter) + delimiter = delimiter or " " + local data = {} + local flags = {} + + for i, l in pairs(luci.util.split(luci.util.trim(cnt), "\n")) do + for j, f in pairs(luci.util.split(luci.util.trim(l), delimiter, nil, true)) do + local k, x, v = f:match('([^%s][^:=]*) *([:=]*) *"*([^\n"]*)"*') + + if k then + if x == "" then + table.insert(flags, k) + else + data[k] = v + end + end + end + end + + return data, flags +end diff --git a/modules/base/luasrc/sys/iptparser.lua b/modules/base/luasrc/sys/iptparser.lua new file mode 100644 index 0000000000..d82363309a --- /dev/null +++ b/modules/base/luasrc/sys/iptparser.lua @@ -0,0 +1,373 @@ +--[[ + +Iptables parser and query library +(c) 2008-2009 Jo-Philipp Wich <xm@leipzig.freifunk.net> +(c) 2008-2009 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +]]-- + +local luci = {} +luci.util = require "luci.util" +luci.sys = require "luci.sys" +luci.ip = require "luci.ip" + +local tonumber, ipairs, table = tonumber, ipairs, table + +--- LuCI iptables parser and query library +-- @cstyle instance +module("luci.sys.iptparser") + +--- Create a new iptables parser object. +-- @class function +-- @name IptParser +-- @param family Number specifying the address family. 4 for IPv4, 6 for IPv6 +-- @return IptParser instance +IptParser = luci.util.class() + +function IptParser.__init__( self, family ) + self._family = (tonumber(family) == 6) and 6 or 4 + self._rules = { } + self._chains = { } + + if self._family == 4 then + self._nulladdr = "0.0.0.0/0" + self._tables = { "filter", "nat", "mangle", "raw" } + self._command = "iptables -t %s --line-numbers -nxvL" + else + self._nulladdr = "::/0" + self._tables = { "filter", "mangle", "raw" } + self._command = "ip6tables -t %s --line-numbers -nxvL" + end + + self:_parse_rules() +end + +--- Find all firewall rules that match the given criteria. Expects a table with +-- search criteria as only argument. If args is nil or an empty table then all +-- rules will be returned. +-- +-- The following keys in the args table are recognized: +-- <ul> +-- <li> table - Match rules that are located within the given table +-- <li> chain - Match rules that are located within the given chain +-- <li> target - Match rules with the given target +-- <li> protocol - Match rules that match the given protocol, rules with +-- protocol "all" are always matched +-- <li> source - Match rules with the given source, rules with source +-- "0.0.0.0/0" (::/0) are always matched +-- <li> destination - Match rules with the given destination, rules with +-- destination "0.0.0.0/0" (::/0) are always matched +-- <li> inputif - Match rules with the given input interface, rules +-- with input interface "*" (=all) are always matched +-- <li> outputif - Match rules with the given output interface, rules +-- with output interface "*" (=all) are always matched +-- <li> flags - Match rules that match the given flags, current +-- supported values are "-f" (--fragment) +-- and "!f" (! --fragment) +-- <li> options - Match rules containing all given options +-- </ul> +-- The return value is a list of tables representing the matched rules. +-- Each rule table contains the following fields: +-- <ul> +-- <li> index - The index number of the rule +-- <li> table - The table where the rule is located, can be one +-- of "filter", "nat" or "mangle" +-- <li> chain - The chain where the rule is located, e.g. "INPUT" +-- or "postrouting_wan" +-- <li> target - The rule target, e.g. "REJECT" or "DROP" +-- <li> protocol The matching protocols, e.g. "all" or "tcp" +-- <li> flags - Special rule options ("--", "-f" or "!f") +-- <li> inputif - Input interface of the rule, e.g. "eth0.0" +-- or "*" for all interfaces +-- <li> outputif - Output interface of the rule,e.g. "eth0.0" +-- or "*" for all interfaces +-- <li> source - The source ip range, e.g. "0.0.0.0/0" (::/0) +-- <li> destination - The destination ip range, e.g. "0.0.0.0/0" (::/0) +-- <li> options - A list of specific options of the rule, +-- e.g. { "reject-with", "tcp-reset" } +-- <li> packets - The number of packets matched by the rule +-- <li> bytes - The number of total bytes matched by the rule +-- </ul> +-- Example: +-- <pre> +-- ip = luci.sys.iptparser.IptParser() +-- result = ip.find( { +-- target="REJECT", +-- protocol="tcp", +-- options={ "reject-with", "tcp-reset" } +-- } ) +-- </pre> +-- This will match all rules with target "-j REJECT", +-- protocol "-p tcp" (or "-p all") +-- and the option "--reject-with tcp-reset". +-- @params args Table containing the search arguments (optional) +-- @return Table of matching rule tables +function IptParser.find( self, args ) + + local args = args or { } + local rv = { } + + args.source = args.source and self:_parse_addr(args.source) + args.destination = args.destination and self:_parse_addr(args.destination) + + for i, rule in ipairs(self._rules) do + local match = true + + -- match table + if not ( not args.table or args.table:lower() == rule.table ) then + match = false + end + + -- match chain + if not ( match == true and ( + not args.chain or args.chain == rule.chain + ) ) then + match = false + end + + -- match target + if not ( match == true and ( + not args.target or args.target == rule.target + ) ) then + match = false + end + + -- match protocol + if not ( match == true and ( + not args.protocol or rule.protocol == "all" or + args.protocol:lower() == rule.protocol + ) ) then + match = false + end + + -- match source + if not ( match == true and ( + not args.source or rule.source == self._nulladdr or + self:_parse_addr(rule.source):contains(args.source) + ) ) then + match = false + end + + -- match destination + if not ( match == true and ( + not args.destination or rule.destination == self._nulladdr or + self:_parse_addr(rule.destination):contains(args.destination) + ) ) then + match = false + end + + -- match input interface + if not ( match == true and ( + not args.inputif or rule.inputif == "*" or + args.inputif == rule.inputif + ) ) then + match = false + end + + -- match output interface + if not ( match == true and ( + not args.outputif or rule.outputif == "*" or + args.outputif == rule.outputif + ) ) then + match = false + end + + -- match flags (the "opt" column) + if not ( match == true and ( + not args.flags or rule.flags == args.flags + ) ) then + match = false + end + + -- match specific options + if not ( match == true and ( + not args.options or + self:_match_options( rule.options, args.options ) + ) ) then + match = false + end + + -- insert match + if match == true then + rv[#rv+1] = rule + end + end + + return rv +end + + +--- Rebuild the internal lookup table, for example when rules have changed +-- through external commands. +-- @return nothing +function IptParser.resync( self ) + self._rules = { } + self._chain = nil + self:_parse_rules() +end + + +--- Find the names of all tables. +-- @return Table of table names. +function IptParser.tables( self ) + return self._tables +end + + +--- Find the names of all chains within the given table name. +-- @param table String containing the table name +-- @return Table of chain names in the order they occur. +function IptParser.chains( self, table ) + local lookup = { } + local chains = { } + for _, r in ipairs(self:find({table=table})) do + if not lookup[r.chain] then + lookup[r.chain] = true + chains[#chains+1] = r.chain + end + end + return chains +end + + +--- Return the given firewall chain within the given table name. +-- @param table String containing the table name +-- @param chain String containing the chain name +-- @return Table containing the fields "policy", "packets", "bytes" +-- and "rules". The "rules" field is a table of rule tables. +function IptParser.chain( self, table, chain ) + return self._chains[table:lower()] and self._chains[table:lower()][chain] +end + + +--- Test whether the given target points to a custom chain. +-- @param target String containing the target action +-- @return Boolean indicating whether target is a custom chain. +function IptParser.is_custom_target( self, target ) + for _, r in ipairs(self._rules) do + if r.chain == target then + return true + end + end + return false +end + + +-- [internal] Parse address according to family. +function IptParser._parse_addr( self, addr ) + if self._family == 4 then + return luci.ip.IPv4(addr) + else + return luci.ip.IPv6(addr) + end +end + +-- [internal] Parse iptables output from all tables. +function IptParser._parse_rules( self ) + + for i, tbl in ipairs(self._tables) do + + self._chains[tbl] = { } + + for i, rule in ipairs(luci.util.execl(self._command % tbl)) do + + if rule:find( "^Chain " ) == 1 then + + local crefs + local cname, cpol, cpkt, cbytes = rule:match( + "^Chain ([^%s]*) %(policy (%w+) " .. + "(%d+) packets, (%d+) bytes%)" + ) + + if not cname then + cname, crefs = rule:match( + "^Chain ([^%s]*) %((%d+) references%)" + ) + end + + self._chain = cname + self._chains[tbl][cname] = { + policy = cpol, + packets = tonumber(cpkt or 0), + bytes = tonumber(cbytes or 0), + references = tonumber(crefs or 0), + rules = { } + } + + else + if rule:find("%d") == 1 then + + local rule_parts = luci.util.split( rule, "%s+", nil, true ) + local rule_details = { } + + -- cope with rules that have no target assigned + if rule:match("^%d+%s+%d+%s+%d+%s%s") then + table.insert(rule_parts, 4, nil) + end + + -- ip6tables opt column is usually zero-width + if self._family == 6 then + table.insert(rule_parts, 6, "--") + end + + rule_details["table"] = tbl + rule_details["chain"] = self._chain + rule_details["index"] = tonumber(rule_parts[1]) + rule_details["packets"] = tonumber(rule_parts[2]) + rule_details["bytes"] = tonumber(rule_parts[3]) + rule_details["target"] = rule_parts[4] + rule_details["protocol"] = rule_parts[5] + rule_details["flags"] = rule_parts[6] + rule_details["inputif"] = rule_parts[7] + rule_details["outputif"] = rule_parts[8] + rule_details["source"] = rule_parts[9] + rule_details["destination"] = rule_parts[10] + rule_details["options"] = { } + + for i = 11, #rule_parts do + if #rule_parts[i] > 0 then + rule_details["options"][i-10] = rule_parts[i] + end + end + + self._rules[#self._rules+1] = rule_details + + self._chains[tbl][self._chain].rules[ + #self._chains[tbl][self._chain].rules + 1 + ] = rule_details + end + end + end + end + + self._chain = nil +end + + +-- [internal] Return true if optlist1 contains all elements of optlist 2. +-- Return false in all other cases. +function IptParser._match_options( self, o1, o2 ) + + -- construct a hashtable of first options list to speed up lookups + local oh = { } + for i, opt in ipairs( o1 ) do oh[opt] = true end + + -- iterate over second options list + -- each string in o2 must be also present in o1 + -- if o2 contains a string which is not found in o1 then return false + for i, opt in ipairs( o2 ) do + if not oh[opt] then + return false + end + end + + return true +end diff --git a/modules/base/luasrc/sys/zoneinfo.lua b/modules/base/luasrc/sys/zoneinfo.lua new file mode 100644 index 0000000000..f5a12bfcb6 --- /dev/null +++ b/modules/base/luasrc/sys/zoneinfo.lua @@ -0,0 +1,28 @@ +--[[ +LuCI - Autogenerated Zoneinfo Module + +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 + +]]-- + +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 +}) diff --git a/modules/base/luasrc/sys/zoneinfo/tzdata.lua b/modules/base/luasrc/sys/zoneinfo/tzdata.lua new file mode 100644 index 0000000000..1a99f6a7eb --- /dev/null +++ b/modules/base/luasrc/sys/zoneinfo/tzdata.lua @@ -0,0 +1,420 @@ +--[[ +LuCI - Autogenerated Zoneinfo Module + +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 + +]]-- + +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/Casablanca', 'WET0' }, + { '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', 'WET0' }, + { 'Africa/Freetown', 'GMT0' }, + { 'Africa/Gaborone', 'CAT-2' }, + { 'Africa/Harare', 'CAT-2' }, + { 'Africa/Johannesburg', 'SAST-2' }, + { 'Africa/Juba', 'EAT-3' }, + { 'Africa/Kampala', 'EAT-3' }, + { 'Africa/Khartoum', 'EAT-3' }, + { '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', 'WAT-1WAST,M9.1.0,M4.1.0' }, + { 'America/Adak', 'HAST10HADT,M3.2.0,M11.1.0' }, + { 'America/Anchorage', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Anguilla', 'AST4' }, + { 'America/Antigua', 'AST4' }, + { 'America/Araguaina', 'BRT3' }, + { 'America/Argentina/Buenos Aires', 'ART3' }, + { 'America/Argentina/Catamarca', 'ART3' }, + { 'America/Argentina/Cordoba', 'ART3' }, + { 'America/Argentina/Jujuy', 'ART3' }, + { 'America/Argentina/La Rioja', 'ART3' }, + { 'America/Argentina/Mendoza', 'ART3' }, + { 'America/Argentina/Rio Gallegos', 'ART3' }, + { 'America/Argentina/Salta', 'ART3' }, + { 'America/Argentina/San Juan', 'ART3' }, + { 'America/Argentina/Tucuman', 'ART3' }, + { 'America/Argentina/Ushuaia', 'ART3' }, + { 'America/Aruba', 'AST4' }, + { 'America/Asuncion', 'PYT4PYST,M10.1.0/0,M4.2.0/0' }, + { 'America/Atikokan', 'EST5' }, + { 'America/Bahia', 'BRT3BRST,M10.3.0/0,M2.3.0/0' }, + { 'America/Bahia Banderas', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Barbados', 'AST4' }, + { 'America/Belem', 'BRT3' }, + { 'America/Belize', 'CST6' }, + { 'America/Blanc-Sablon', 'AST4' }, + { 'America/Boa Vista', 'AMT4' }, + { 'America/Bogota', 'COT5' }, + { 'America/Boise', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Cambridge Bay', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Campo Grande', 'AMT4AMST,M10.3.0/0,M2.3.0/0' }, + { 'America/Cancun', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Caracas', 'VET4:30' }, + { 'America/Cayenne', 'GFT3' }, + { 'America/Cayman', 'EST5' }, + { 'America/Chicago', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Chihuahua', 'MST7MDT,M4.1.0,M10.5.0' }, + { 'America/Costa Rica', 'CST6' }, + { 'America/Cuiaba', 'AMT4AMST,M10.3.0/0,M2.3.0/0' }, + { 'America/Curacao', 'AST4' }, + { 'America/Danmarkshavn', 'GMT0' }, + { 'America/Dawson', 'PST8PDT,M3.2.0,M11.1.0' }, + { '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', 'AMT4' }, + { 'America/El Salvador', 'CST6' }, + { 'America/Fortaleza', 'BRT3' }, + { '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', 'ECT5' }, + { 'America/Guyana', 'GYT4' }, + { 'America/Halifax', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Havana', 'CST5CDT,M3.2.0/0,M10.5.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', 'BOT4' }, + { 'America/Lima', 'PET5' }, + { 'America/Los Angeles', 'PST8PDT,M3.2.0,M11.1.0' }, + { 'America/Lower Princes', 'AST4' }, + { 'America/Maceio', 'BRT3' }, + { 'America/Managua', 'CST6' }, + { 'America/Manaus', 'AMT4' }, + { 'America/Marigot', 'AST4' }, + { 'America/Martinique', 'AST4' }, + { 'America/Matamoros', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Mazatlan', 'MST7MDT,M4.1.0,M10.5.0' }, + { 'America/Menominee', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Merida', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Metlakatla', 'MeST8' }, + { 'America/Mexico City', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Miquelon', 'PMST3PMDT,M3.2.0,M11.1.0' }, + { 'America/Moncton', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Monterrey', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Montevideo', 'UYT3UYST,M10.1.0,M3.2.0' }, + { 'America/Montreal', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Montserrat', 'AST4' }, + { 'America/Nassau', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/New York', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Nipigon', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Nome', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Noronha', 'FNT2' }, + { '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/Ojinaga', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Panama', 'EST5' }, + { 'America/Pangnirtung', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Paramaribo', 'SRT3' }, + { 'America/Phoenix', 'MST7' }, + { 'America/Port of Spain', 'AST4' }, + { 'America/Port-au-Prince', 'EST5' }, + { 'America/Porto Velho', 'AMT4' }, + { 'America/Puerto Rico', 'AST4' }, + { 'America/Rainy River', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Rankin Inlet', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Recife', 'BRT3' }, + { 'America/Regina', 'CST6' }, + { 'America/Resolute', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Rio Branco', 'AMT4' }, + { 'America/Santa Isabel', 'PST8PDT,M4.1.0,M10.5.0' }, + { 'America/Santarem', 'BRT3' }, + { 'America/Santo Domingo', 'AST4' }, + { 'America/Sao Paulo', 'BRT3BRST,M10.3.0/0,M2.3.0/0' }, + { 'America/Scoresbysund', 'EGT1EGST,M3.5.0/0,M10.5.0/1' }, + { 'America/Shiprock', 'MST7MDT,M3.2.0,M11.1.0' }, + { '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/Thunder Bay', 'EST5EDT,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', 'PST8PDT,M3.2.0,M11.1.0' }, + { 'America/Winnipeg', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Yakutat', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Yellowknife', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'Antarctica/Casey', 'WST-8' }, + { 'Antarctica/Davis', 'DAVT-7' }, + { 'Antarctica/DumontDUrville', 'DDUT-10' }, + { 'Antarctica/Macquarie', 'MIST-11' }, + { 'Antarctica/Mawson', 'MAWT-5' }, + { 'Antarctica/McMurdo', 'NZST-12NZDT,M9.5.0,M4.1.0/3' }, + { 'Antarctica/Rothera', 'ROTT3' }, + { 'Antarctica/South Pole', 'NZST-12NZDT,M9.5.0,M4.1.0/3' }, + { 'Antarctica/Syowa', 'SYOT-3' }, + { 'Antarctica/Vostok', 'VOST-6' }, + { 'Arctic/Longyearbyen', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Asia/Aden', 'AST-3' }, + { 'Asia/Almaty', 'ALMT-6' }, + { 'Asia/Anadyr', 'ANAT-12' }, + { 'Asia/Aqtau', 'AQTT-5' }, + { 'Asia/Aqtobe', 'AQTT-5' }, + { 'Asia/Ashgabat', 'TMT-5' }, + { 'Asia/Baghdad', 'AST-3' }, + { 'Asia/Bahrain', 'AST-3' }, + { 'Asia/Baku', 'AZT-4AZST,M3.5.0/4,M10.5.0/5' }, + { 'Asia/Bangkok', 'ICT-7' }, + { 'Asia/Beirut', 'EET-2EEST,M3.5.0/0,M10.5.0/0' }, + { 'Asia/Bishkek', 'KGT-6' }, + { 'Asia/Brunei', 'BNT-8' }, + { 'Asia/Choibalsan', 'CHOT-8' }, + { 'Asia/Chongqing', 'CST-8' }, + { 'Asia/Colombo', 'IST-5:30' }, + { 'Asia/Damascus', 'EET-2EEST,M4.1.5/0,M10.5.5/0' }, + { 'Asia/Dhaka', 'BDT-6' }, + { 'Asia/Dili', 'TLT-9' }, + { 'Asia/Dubai', 'GST-4' }, + { 'Asia/Dushanbe', 'TJT-5' }, + { 'Asia/Gaza', 'EET-2' }, + { 'Asia/Harbin', 'CST-8' }, + { 'Asia/Hebron', 'EET-2' }, + { 'Asia/Ho Chi Minh', 'ICT-7' }, + { 'Asia/Hong Kong', 'HKT-8' }, + { 'Asia/Hovd', 'HOVT-7' }, + { 'Asia/Irkutsk', 'IRKT-9' }, + { 'Asia/Jakarta', 'WIT-7' }, + { 'Asia/Jayapura', 'EIT-9' }, + { 'Asia/Kabul', 'AFT-4:30' }, + { 'Asia/Kamchatka', 'PETT-12' }, + { 'Asia/Karachi', 'PKT-5' }, + { 'Asia/Kashgar', 'CST-8' }, + { 'Asia/Kathmandu', 'NPT-5:45' }, + { 'Asia/Kolkata', 'IST-5:30' }, + { 'Asia/Krasnoyarsk', 'KRAT-8' }, + { 'Asia/Kuala Lumpur', 'MYT-8' }, + { 'Asia/Kuching', 'MYT-8' }, + { 'Asia/Kuwait', 'AST-3' }, + { 'Asia/Macau', 'CST-8' }, + { 'Asia/Magadan', 'MAGT-12' }, + { 'Asia/Makassar', 'CIT-8' }, + { 'Asia/Manila', 'PHT-8' }, + { 'Asia/Muscat', 'GST-4' }, + { 'Asia/Nicosia', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Asia/Novokuznetsk', 'NOVT-7' }, + { 'Asia/Novosibirsk', 'NOVT-7' }, + { 'Asia/Omsk', 'OMST-7' }, + { 'Asia/Oral', 'ORAT-5' }, + { 'Asia/Phnom Penh', 'ICT-7' }, + { 'Asia/Pontianak', 'WIT-7' }, + { 'Asia/Pyongyang', 'KST-9' }, + { 'Asia/Qatar', 'AST-3' }, + { 'Asia/Qyzylorda', 'QYZT-6' }, + { 'Asia/Rangoon', 'MMT-6:30' }, + { 'Asia/Riyadh', 'AST-3' }, + { 'Asia/Sakhalin', 'SAKT-11' }, + { 'Asia/Samarkand', 'UZT-5' }, + { 'Asia/Seoul', 'KST-9' }, + { 'Asia/Shanghai', 'CST-8' }, + { 'Asia/Singapore', 'SGT-8' }, + { 'Asia/Taipei', 'CST-8' }, + { 'Asia/Tashkent', 'UZT-5' }, + { 'Asia/Tbilisi', 'GET-4' }, + { 'Asia/Thimphu', 'BTT-6' }, + { 'Asia/Tokyo', 'JST-9' }, + { 'Asia/Ulaanbaatar', 'ULAT-8' }, + { 'Asia/Urumqi', 'CST-8' }, + { 'Asia/Vientiane', 'ICT-7' }, + { 'Asia/Vladivostok', 'VLAT-11' }, + { 'Asia/Yakutsk', 'YAKT-10' }, + { 'Asia/Yekaterinburg', 'YEKT-6' }, + { 'Asia/Yerevan', 'AMT-4AMST,M3.5.0,M10.5.0/3' }, + { 'Atlantic/Azores', 'AZOT1AZOST,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', 'CVT1' }, + { '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', 'GST2' }, + { 'Atlantic/St Helena', 'GMT0' }, + { 'Atlantic/Stanley', 'FKT4FKST,M9.1.0,M4.3.0' }, + { 'Australia/Adelaide', 'CST-9:30CST,M10.1.0,M4.1.0/3' }, + { 'Australia/Brisbane', 'EST-10' }, + { 'Australia/Broken Hill', 'CST-9:30CST,M10.1.0,M4.1.0/3' }, + { 'Australia/Currie', 'EST-10EST,M10.1.0,M4.1.0/3' }, + { 'Australia/Darwin', 'CST-9:30' }, + { 'Australia/Eucla', 'CWST-8:45' }, + { 'Australia/Hobart', 'EST-10EST,M10.1.0,M4.1.0/3' }, + { 'Australia/Lindeman', 'EST-10' }, + { 'Australia/Lord Howe', 'LHST-10:30LHST-11,M10.1.0,M4.1.0' }, + { 'Australia/Melbourne', 'EST-10EST,M10.1.0,M4.1.0/3' }, + { 'Australia/Perth', 'WST-8' }, + { 'Australia/Sydney', 'EST-10EST,M10.1.0,M4.1.0/3' }, + { 'Europe/Amsterdam', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Andorra', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { '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/Chisinau', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Copenhagen', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Dublin', 'GMT0IST,M3.5.0/1,M10.5.0' }, + { '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', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Jersey', 'GMT0BST,M3.5.0/1,M10.5.0' }, + { 'Europe/Kaliningrad', 'FET-3' }, + { 'Europe/Kiev', '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', 'FET-3' }, + { 'Europe/Monaco', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Moscow', 'MSK-4' }, + { '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', 'SAMT-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/Simferopol', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { '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/Uzhgorod', 'EET-2EEST,M3.5.0/3,M10.5.0/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', 'VOLT-4' }, + { 'Europe/Warsaw', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Zagreb', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Zaporozhye', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Zurich', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Indian/Antananarivo', 'EAT-3' }, + { 'Indian/Chagos', 'IOT-6' }, + { 'Indian/Christmas', 'CXT-7' }, + { 'Indian/Cocos', 'CCT-6:30' }, + { 'Indian/Comoro', 'EAT-3' }, + { 'Indian/Kerguelen', 'TFT-5' }, + { 'Indian/Mahe', 'SCT-4' }, + { 'Indian/Maldives', 'MVT-5' }, + { 'Indian/Mauritius', 'MUT-4' }, + { 'Indian/Mayotte', 'EAT-3' }, + { 'Indian/Reunion', 'RET-4' }, + { 'Pacific/Apia', 'WST-13' }, + { 'Pacific/Auckland', 'NZST-12NZDT,M9.5.0,M4.1.0/3' }, + { 'Pacific/Chatham', 'CHAST-12:45CHADT,M9.5.0/2:45,M4.1.0/3:45' }, + { 'Pacific/Chuuk', 'CHUT-10' }, + { 'Pacific/Efate', 'VUT-11' }, + { 'Pacific/Enderbury', 'PHOT-13' }, + { 'Pacific/Fakaofo', 'TKT10' }, + { 'Pacific/Fiji', 'FJT-12' }, + { 'Pacific/Funafuti', 'TVT-12' }, + { 'Pacific/Galapagos', 'GALT6' }, + { 'Pacific/Gambier', 'GAMT9' }, + { 'Pacific/Guadalcanal', 'SBT-11' }, + { 'Pacific/Guam', 'ChST-10' }, + { 'Pacific/Honolulu', 'HST10' }, + { 'Pacific/Johnston', 'HST10' }, + { 'Pacific/Kiritimati', 'LINT-14' }, + { 'Pacific/Kosrae', 'KOST-11' }, + { 'Pacific/Kwajalein', 'MHT-12' }, + { 'Pacific/Majuro', 'MHT-12' }, + { 'Pacific/Marquesas', 'MART9:30' }, + { 'Pacific/Midway', 'SST11' }, + { 'Pacific/Nauru', 'NRT-12' }, + { 'Pacific/Niue', 'NUT11' }, + { 'Pacific/Norfolk', 'NFT-11:30' }, + { 'Pacific/Noumea', 'NCT-11' }, + { 'Pacific/Pago Pago', 'SST11' }, + { 'Pacific/Palau', 'PWT-9' }, + { 'Pacific/Pitcairn', 'PST8' }, + { 'Pacific/Pohnpei', 'PONT-11' }, + { 'Pacific/Port Moresby', 'PGT-10' }, + { 'Pacific/Rarotonga', 'CKT10' }, + { 'Pacific/Saipan', 'ChST-10' }, + { 'Pacific/Tahiti', 'TAHT10' }, + { 'Pacific/Tarawa', 'GILT-12' }, + { 'Pacific/Tongatapu', 'TOT-13' }, + { 'Pacific/Wake', 'WAKT-12' }, + { 'Pacific/Wallis', 'WFT-12' }, +} diff --git a/modules/base/luasrc/sys/zoneinfo/tzoffset.lua b/modules/base/luasrc/sys/zoneinfo/tzoffset.lua new file mode 100644 index 0000000000..bbe75d5a47 --- /dev/null +++ b/modules/base/luasrc/sys/zoneinfo/tzoffset.lua @@ -0,0 +1,162 @@ +--[[ +LuCI - Autogenerated Zoneinfo Module + +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 + +]]-- + +module "luci.sys.zoneinfo.tzoffset" + +OFFSET = { + gmt = 0, -- GMT + eat = 10800, -- EAT + cet = 3600, -- CET + wat = 3600, -- WAT + cat = 7200, -- CAT + wet = 0, -- WET + sast = 7200, -- SAST + eet = 7200, -- EET + hast = -36000, -- HAST + hadt = -32400, -- HADT + akst = -32400, -- AKST + akdt = -28800, -- AKDT + ast = -14400, -- AST + brt = -10800, -- BRT + art = -10800, -- ART + pyt = -14400, -- PYT + pyst = -10800, -- PYST + est = -18000, -- EST + cst = -21600, -- CST + cdt = -18000, -- CDT + amt = -14400, -- AMT + cot = -18000, -- COT + mst = -25200, -- MST + mdt = -21600, -- MDT + vet = -16200, -- VET + gft = -10800, -- GFT + pst = -28800, -- PST + pdt = -25200, -- PDT + ect = -18000, -- ECT + gyt = -14400, -- GYT + bot = -14400, -- BOT + pet = -18000, -- PET + pmst = -10800, -- PMST + pmdt = -7200, -- PMDT + uyt = -10800, -- UYT + uyst = -7200, -- UYST + fnt = -7200, -- FNT + srt = -10800, -- SRT + egt = -3600, -- EGT + egst = 0, -- EGST + nst = -12600, -- NST + ndt = -9000, -- NDT + wst = 28800, -- WST + davt = 25200, -- DAVT + ddut = 36000, -- DDUT + mist = 39600, -- MIST + mawt = 18000, -- MAWT + nzst = 43200, -- NZST + nzdt = 46800, -- NZDT + rott = -10800, -- ROTT + syot = 10800, -- SYOT + vost = 21600, -- VOST + almt = 21600, -- ALMT + anat = 43200, -- ANAT + aqtt = 18000, -- AQTT + tmt = 18000, -- TMT + azt = 14400, -- AZT + azst = 18000, -- AZST + ict = 25200, -- ICT + kgt = 21600, -- KGT + bnt = 28800, -- BNT + chot = 28800, -- CHOT + ist = 19800, -- IST + bdt = 21600, -- BDT + tlt = 32400, -- TLT + gst = 14400, -- GST + tjt = 18000, -- TJT + hkt = 28800, -- HKT + hovt = 25200, -- HOVT + irkt = 32400, -- IRKT + wit = 25200, -- WIT + eit = 32400, -- EIT + aft = 16200, -- AFT + pett = 43200, -- PETT + pkt = 18000, -- PKT + npt = 20700, -- NPT + krat = 28800, -- KRAT + myt = 28800, -- MYT + magt = 43200, -- MAGT + cit = 28800, -- CIT + pht = 28800, -- PHT + novt = 25200, -- NOVT + omst = 25200, -- OMST + orat = 18000, -- ORAT + kst = 32400, -- KST + qyzt = 21600, -- QYZT + mmt = 23400, -- MMT + sakt = 39600, -- SAKT + uzt = 18000, -- UZT + sgt = 28800, -- SGT + get = 14400, -- GET + btt = 21600, -- BTT + jst = 32400, -- JST + ulat = 28800, -- ULAT + vlat = 39600, -- VLAT + yakt = 36000, -- YAKT + yekt = 21600, -- YEKT + azot = -3600, -- AZOT + azost = 0, -- AZOST + cvt = -3600, -- CVT + fkt = -14400, -- FKT + fkst = -10800, -- FKST + cwst = 31500, -- CWST + lhst = 37800, -- LHST + lhst = 39600, -- LHST + fet = 10800, -- FET + msk = 14400, -- MSK + samt = 14400, -- SAMT + volt = 14400, -- VOLT + iot = 21600, -- IOT + cxt = 25200, -- CXT + cct = 23400, -- CCT + tft = 18000, -- TFT + sct = 14400, -- SCT + mvt = 18000, -- MVT + mut = 14400, -- MUT + ret = 14400, -- RET + chast = 45900, -- CHAST + chadt = 49500, -- CHADT + chut = 36000, -- CHUT + vut = 39600, -- VUT + phot = 46800, -- PHOT + tkt = -36000, -- TKT + fjt = 43200, -- FJT + tvt = 43200, -- TVT + galt = -21600, -- GALT + gamt = -32400, -- GAMT + sbt = 39600, -- SBT + hst = -36000, -- HST + lint = 50400, -- LINT + kost = 39600, -- KOST + mht = 43200, -- MHT + mart = -34200, -- MART + sst = -39600, -- SST + nrt = 43200, -- NRT + nut = -39600, -- NUT + nft = 41400, -- NFT + nct = 39600, -- NCT + pwt = 32400, -- PWT + pont = 39600, -- PONT + pgt = 36000, -- PGT + ckt = -36000, -- CKT + taht = -36000, -- TAHT + gilt = 43200, -- GILT + tot = 46800, -- TOT + wakt = 43200, -- WAKT + wft = 43200, -- WFT +} diff --git a/modules/base/luasrc/tools/proto.lua b/modules/base/luasrc/tools/proto.lua new file mode 100644 index 0000000000..4df02696b0 --- /dev/null +++ b/modules/base/luasrc/tools/proto.lua @@ -0,0 +1,46 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2012 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +]]-- + +module("luci.tools.proto", package.seeall) + +function opt_macaddr(s, ifc, ...) + local v = luci.cbi.Value + local o = s:taboption("advanced", v, "macaddr", ...) + + o.placeholder = ifc and ifc:mac() + o.datatype = "macaddr" + + function o.cfgvalue(self, section) + local w = ifc and ifc:get_wifinet() + if w then + return w:get("macaddr") + else + return v.cfgvalue(self, section) + end + end + + function o.write(self, section, value) + local w = ifc and ifc:get_wifinet() + if w then + w:set("macaddr", value) + elseif value then + v.write(self, section, value) + else + v.remove(self, section) + end + end + + function o.remove(self, section) + self:write(section, nil) + end +end diff --git a/modules/base/luasrc/tools/status.lua b/modules/base/luasrc/tools/status.lua new file mode 100644 index 0000000000..27bc925bd2 --- /dev/null +++ b/modules/base/luasrc/tools/status.lua @@ -0,0 +1,216 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2011 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +]]-- + +module("luci.tools.status", package.seeall) + +local uci = require "luci.model.uci".cursor() + +local function dhcp_leases_common(family) + local rv = { } + local nfs = require "nixio.fs" + local leasefile = "/var/dhcp.leases" + + uci:foreach("dhcp", "dnsmasq", + function(s) + if s.leasefile and nfs.access(s.leasefile) then + leasefile = s.leasefile + return false + end + end) + + local fd = io.open(leasefile, "r") + if fd then + while true do + local ln = fd:read("*l") + if not ln then + break + else + local ts, mac, ip, name, duid = ln:match("^(%d+) (%S+) (%S+) (%S+) (%S+)") + if ts and mac and ip and name and duid then + if family == 4 and not ip:match(":") then + rv[#rv+1] = { + expires = os.difftime(tonumber(ts) or 0, os.time()), + macaddr = mac, + ipaddr = ip, + hostname = (name ~= "*") and name + } + elseif family == 6 and ip:match(":") then + rv[#rv+1] = { + expires = os.difftime(tonumber(ts) or 0, os.time()), + ip6addr = ip, + duid = (duid ~= "*") and duid, + hostname = (name ~= "*") and name + } + end + end + end + end + fd:close() + end + + local fd = io.open("/tmp/hosts/odhcpd", "r") + if fd then + while true do + local ln = fd:read("*l") + if not ln then + break + else + local iface, duid, iaid, name, ts, id, length, ip = ln:match("^# (%S+) (%S+) (%S+) (%S+) (%d+) (%S+) (%S+) (.*)") + if ip and iaid ~= "ipv4" and family == 6 then + rv[#rv+1] = { + expires = os.difftime(tonumber(ts) or 0, os.time()), + duid = duid, + ip6addr = ip, + hostname = (name ~= "-") and name + } + elseif ip and iaid == "ipv4" and family == 4 then + rv[#rv+1] = { + expires = os.difftime(tonumber(ts) or 0, os.time()), + macaddr = duid, + ipaddr = ip, + hostname = (name ~= "-") and name + } + end + end + end + fd:close() + end + + return rv +end + +function dhcp_leases() + return dhcp_leases_common(4) +end + +function dhcp6_leases() + return dhcp_leases_common(6) +end + +function wifi_networks() + local rv = { } + local ntm = require "luci.model.network".init() + + local dev + for _, dev in ipairs(ntm:get_wifidevs()) do + local rd = { + up = dev:is_up(), + device = dev:name(), + name = dev:get_i18n(), + networks = { } + } + + local net + for _, net in ipairs(dev:get_wifinets()) do + rd.networks[#rd.networks+1] = { + name = net:shortname(), + link = net:adminlink(), + up = net:is_up(), + mode = net:active_mode(), + ssid = net:active_ssid(), + bssid = net:active_bssid(), + encryption = net:active_encryption(), + frequency = net:frequency(), + channel = net:channel(), + signal = net:signal(), + quality = net:signal_percent(), + noise = net:noise(), + bitrate = net:bitrate(), + ifname = net:ifname(), + assoclist = net:assoclist(), + country = net:country(), + txpower = net:txpower(), + txpoweroff = net:txpower_offset() + } + end + + rv[#rv+1] = rd + end + + return rv +end + +function wifi_network(id) + local ntm = require "luci.model.network".init() + local net = ntm:get_wifinet(id) + if net then + local dev = net:get_device() + if dev then + return { + id = id, + name = net:shortname(), + link = net:adminlink(), + up = net:is_up(), + mode = net:active_mode(), + ssid = net:active_ssid(), + bssid = net:active_bssid(), + encryption = net:active_encryption(), + frequency = net:frequency(), + channel = net:channel(), + signal = net:signal(), + quality = net:signal_percent(), + noise = net:noise(), + bitrate = net:bitrate(), + ifname = net:ifname(), + assoclist = net:assoclist(), + country = net:country(), + txpower = net:txpower(), + txpoweroff = net:txpower_offset(), + device = { + up = dev:is_up(), + device = dev:name(), + name = dev:get_i18n() + } + } + end + end + return { } +end + +function switch_status(devs) + local dev + local switches = { } + for dev in devs:gmatch("[^%s,]+") do + local ports = { } + local swc = io.popen("swconfig dev %q show" % dev, "r") + if swc then + local l + repeat + l = swc:read("*l") + if l then + local port, up = l:match("port:(%d+) link:(%w+)") + if port then + local speed = l:match(" speed:(%d+)") + local duplex = l:match(" (%w+)-duplex") + local txflow = l:match(" (txflow)") + local rxflow = l:match(" (rxflow)") + local auto = l:match(" (auto)") + + ports[#ports+1] = { + port = tonumber(port) or 0, + speed = tonumber(speed) or 0, + link = (up == "up"), + duplex = (duplex == "full"), + rxflow = (not not rxflow), + txflow = (not not txflow), + auto = (not not auto) + } + end + end + until not l + swc:close() + end + switches[dev] = ports + end + return switches +end diff --git a/modules/base/luasrc/tools/webadmin.lua b/modules/base/luasrc/tools/webadmin.lua new file mode 100644 index 0000000000..0e09be9800 --- /dev/null +++ b/modules/base/luasrc/tools/webadmin.lua @@ -0,0 +1,173 @@ +--[[ +LuCI - Lua Configuration Interface + +Copyright 2008 Steven Barth <steven@midlink.org> +Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +module("luci.tools.webadmin", package.seeall) +local uci = require("luci.model.uci") +require("luci.sys") +require("luci.ip") + +function byte_format(byte) + local suff = {"B", "KB", "MB", "GB", "TB"} + for i=1, 5 do + if byte > 1024 and i < 5 then + byte = byte / 1024 + else + return string.format("%.2f %s", byte, suff[i]) + end + end +end + +function date_format(secs) + local suff = {"min", "h", "d"} + local mins = 0 + local hour = 0 + local days = 0 + + secs = math.floor(secs) + if secs > 60 then + mins = math.floor(secs / 60) + secs = secs % 60 + end + + if mins > 60 then + hour = math.floor(mins / 60) + mins = mins % 60 + end + + if hour > 24 then + days = math.floor(hour / 24) + hour = hour % 24 + end + + if days > 0 then + return string.format("%.0fd %02.0fh %02.0fmin %02.0fs", days, hour, mins, secs) + else + return string.format("%02.0fh %02.0fmin %02.0fs", hour, mins, secs) + end +end + +function network_get_addresses(net) + local state = uci.cursor_state() + state:load("network") + local addr = {} + local ipv4 = state:get("network", net, "ipaddr") + local mav4 = state:get("network", net, "netmask") + local ipv6 = state:get("network", net, "ip6addr") + + if ipv4 and #ipv4 > 0 then + if mav4 and #mav4 == 0 then mav4 = nil end + + ipv4 = luci.ip.IPv4(ipv4, mav4) + + if ipv4 then + table.insert(addr, ipv4:string()) + end + end + + if ipv6 then + table.insert(addr, ipv6) + end + + state:foreach("network", "alias", + function (section) + if section.interface == net then + if section.ipaddr and section.netmask then + local ipv4 = luci.ip.IPv4(section.ipaddr, section.netmask) + + if ipv4 then + table.insert(addr, ipv4:string()) + end + end + + if section.ip6addr then + table.insert(addr, section.ip6addr) + end + end + end + ) + + return addr +end + +function cbi_add_networks(field) + uci.cursor():foreach("network", "interface", + function (section) + if section[".name"] ~= "loopback" then + field:value(section[".name"]) + end + end + ) + field.titleref = luci.dispatcher.build_url("admin", "network", "network") +end + +function cbi_add_knownips(field) + for i, dataset in ipairs(luci.sys.net.arptable()) do + field:value(dataset["IP address"]) + end +end + +function network_get_zones(net) + local state = uci.cursor_state() + if not state:load("firewall") then + return nil + end + + local zones = {} + + state:foreach("firewall", "zone", + function (section) + local znet = section.network or section.name + if luci.util.contains(luci.util.split(znet, " "), net) then + table.insert(zones, section.name) + end + end + ) + + return zones +end + +function firewall_find_zone(name) + local find + + luci.model.uci.cursor():foreach("firewall", "zone", + function (section) + if section.name == name then + find = section[".name"] + end + end + ) + + return find +end + +function iface_get_network(iface) + local state = uci.cursor_state() + state:load("network") + local net + + state:foreach("network", "interface", + function (section) + local ifname = state:get( + "network", section[".name"], "ifname" + ) + + if iface == ifname then + net = section[".name"] + end + end + ) + + return net +end diff --git a/modules/base/luasrc/util.lua b/modules/base/luasrc/util.lua new file mode 100644 index 0000000000..da761e219a --- /dev/null +++ b/modules/base/luasrc/util.lua @@ -0,0 +1,791 @@ +--[[ +LuCI - Utility library + +Description: +Several common useful Lua functions + +License: +Copyright 2008 Steven Barth <steven@midlink.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +]]-- + +local io = require "io" +local math = require "math" +local table = require "table" +local debug = require "debug" +local ldebug = require "luci.debug" +local string = require "string" +local coroutine = require "coroutine" +local tparser = require "luci.template.parser" + +local getmetatable, setmetatable = getmetatable, setmetatable +local rawget, rawset, unpack = rawget, rawset, unpack +local tostring, type, assert = tostring, type, assert +local ipairs, pairs, next, loadstring = ipairs, pairs, next, loadstring +local require, pcall, xpcall = require, pcall, xpcall +local collectgarbage, get_memory_limit = collectgarbage, get_memory_limit + +--- LuCI utility functions. +module "luci.util" + +-- +-- Pythonic string formatting extension +-- +getmetatable("").__mod = function(a, b) + if not b then + return a + elseif type(b) == "table" then + for k, _ in pairs(b) do if type(b[k]) == "userdata" then b[k] = tostring(b[k]) end end + return a:format(unpack(b)) + else + if type(b) == "userdata" then b = tostring(b) end + return a:format(b) + end +end + + +-- +-- Class helper routines +-- + +-- Instantiates a class +local function _instantiate(class, ...) + local inst = setmetatable({}, {__index = class}) + + if inst.__init__ then + inst:__init__(...) + end + + return inst +end + +--- Create a Class object (Python-style object model). +-- The class object can be instantiated by calling itself. +-- Any class functions or shared parameters can be attached to this object. +-- Attaching a table to the class object makes this table shared between +-- all instances of this class. For object parameters use the __init__ function. +-- Classes can inherit member functions and values from a base class. +-- Class can be instantiated by calling them. All parameters will be passed +-- to the __init__ function of this class - if such a function exists. +-- The __init__ function must be used to set any object parameters that are not shared +-- with other objects of this class. Any return values will be ignored. +-- @param base The base class to inherit from (optional) +-- @return A class object +-- @see instanceof +-- @see clone +function class(base) + return setmetatable({}, { + __call = _instantiate, + __index = base + }) +end + +--- Test whether the given object is an instance of the given class. +-- @param object Object instance +-- @param class Class object to test against +-- @return Boolean indicating whether the object is an instance +-- @see class +-- @see clone +function instanceof(object, class) + local meta = getmetatable(object) + while meta and meta.__index do + if meta.__index == class then + return true + end + meta = getmetatable(meta.__index) + end + return false +end + + +-- +-- Scope manipulation routines +-- + +local tl_meta = { + __mode = "k", + + __index = function(self, key) + local t = rawget(self, coxpt[coroutine.running()] + or coroutine.running() or 0) + return t and t[key] + end, + + __newindex = function(self, key, value) + local c = coxpt[coroutine.running()] or coroutine.running() or 0 + if not rawget(self, c) then + rawset(self, c, { [key] = value }) + else + rawget(self, c)[key] = value + end + end +} + +--- Create a new or get an already existing thread local store associated with +-- the current active coroutine. A thread local store is private a table object +-- whose values can't be accessed from outside of the running coroutine. +-- @return Table value representing the corresponding thread local store +function threadlocal(tbl) + return setmetatable(tbl or {}, tl_meta) +end + + +-- +-- Debugging routines +-- + +--- Write given object to stderr. +-- @param obj Value to write to stderr +-- @return Boolean indicating whether the write operation was successful +function perror(obj) + return io.stderr:write(tostring(obj) .. "\n") +end + +--- Recursively dumps a table to stdout, useful for testing and debugging. +-- @param t Table value to dump +-- @param maxdepth Maximum depth +-- @return Always nil +function dumptable(t, maxdepth, i, seen) + i = i or 0 + seen = seen or setmetatable({}, {__mode="k"}) + + for k,v in pairs(t) do + perror(string.rep("\t", i) .. tostring(k) .. "\t" .. tostring(v)) + if type(v) == "table" and (not maxdepth or i < maxdepth) then + if not seen[v] then + seen[v] = true + dumptable(v, maxdepth, i+1, seen) + else + perror(string.rep("\t", i) .. "*** RECURSION ***") + end + end + end +end + + +-- +-- String and data manipulation routines +-- + +--- Create valid XML PCDATA from given string. +-- @param value String value containing the data to escape +-- @return String value containing the escaped data +function pcdata(value) + return value and tparser.pcdata(tostring(value)) +end + +--- Strip HTML tags from given string. +-- @param value String containing the HTML text +-- @return String with HTML tags stripped of +function striptags(value) + return value and tparser.striptags(tostring(value)) +end + +--- Splits given string on a defined separator sequence and return a table +-- containing the resulting substrings. The optional max parameter specifies +-- the number of bytes to process, regardless of the actual length of the given +-- string. The optional last parameter, regex, specifies whether the separator +-- sequence is interpreted as regular expression. +-- @param str String value containing the data to split up +-- @param pat String with separator pattern (optional, defaults to "\n") +-- @param max Maximum times to split (optional) +-- @param regex Boolean indicating whether to interpret the separator +-- pattern as regular expression (optional, default is false) +-- @return Table containing the resulting substrings +function split(str, pat, max, regex) + pat = pat or "\n" + max = max or #str + + local t = {} + local c = 1 + + if #str == 0 then + return {""} + end + + if #pat == 0 then + return nil + end + + if max == 0 then + return str + end + + repeat + local s, e = str:find(pat, c, not regex) + max = max - 1 + if s and max < 0 then + t[#t+1] = str:sub(c) + else + t[#t+1] = str:sub(c, s and s - 1) + end + c = e and e + 1 or #str + 1 + until not s or max < 0 + + return t +end + +--- Remove leading and trailing whitespace from given string value. +-- @param str String value containing whitespace padded data +-- @return String value with leading and trailing space removed +function trim(str) + return (str:gsub("^%s*(.-)%s*$", "%1")) +end + +--- Count the occurences of given substring in given string. +-- @param str String to search in +-- @param pattern String containing pattern to find +-- @return Number of found occurences +function cmatch(str, pat) + local count = 0 + for _ in str:gmatch(pat) do count = count + 1 end + return count +end + +--- Return a matching iterator for the given value. The iterator will return +-- one token per invocation, the tokens are separated by whitespace. If the +-- input value is a table, it is transformed into a string first. A nil value +-- will result in a valid interator which aborts with the first invocation. +-- @param val The value to scan (table, string or nil) +-- @return Iterator which returns one token per call +function imatch(v) + if type(v) == "table" then + local k = nil + return function() + k = next(v, k) + return v[k] + end + + elseif type(v) == "number" or type(v) == "boolean" then + local x = true + return function() + if x then + x = false + return tostring(v) + end + end + + elseif type(v) == "userdata" or type(v) == "string" then + return tostring(v):gmatch("%S+") + end + + return function() end +end + +--- Parse certain units from the given string and return the canonical integer +-- value or 0 if the unit is unknown. Upper- or lower case is irrelevant. +-- Recognized units are: +-- o "y" - one year (60*60*24*366) +-- o "m" - one month (60*60*24*31) +-- o "w" - one week (60*60*24*7) +-- o "d" - one day (60*60*24) +-- o "h" - one hour (60*60) +-- o "min" - one minute (60) +-- o "kb" - one kilobyte (1024) +-- o "mb" - one megabyte (1024*1024) +-- o "gb" - one gigabyte (1024*1024*1024) +-- o "kib" - one si kilobyte (1000) +-- o "mib" - one si megabyte (1000*1000) +-- o "gib" - one si gigabyte (1000*1000*1000) +-- @param ustr String containing a numerical value with trailing unit +-- @return Number containing the canonical value +function parse_units(ustr) + + local val = 0 + + -- unit map + local map = { + -- date stuff + y = 60 * 60 * 24 * 366, + m = 60 * 60 * 24 * 31, + w = 60 * 60 * 24 * 7, + d = 60 * 60 * 24, + h = 60 * 60, + min = 60, + + -- storage sizes + kb = 1024, + mb = 1024 * 1024, + gb = 1024 * 1024 * 1024, + + -- storage sizes (si) + kib = 1000, + mib = 1000 * 1000, + gib = 1000 * 1000 * 1000 + } + + -- parse input string + for spec in ustr:lower():gmatch("[0-9%.]+[a-zA-Z]*") do + + local num = spec:gsub("[^0-9%.]+$","") + local spn = spec:gsub("^[0-9%.]+", "") + + if map[spn] or map[spn:sub(1,1)] then + val = val + num * ( map[spn] or map[spn:sub(1,1)] ) + else + val = val + num + end + end + + + return val +end + +-- also register functions above in the central string class for convenience +string.pcdata = pcdata +string.striptags = striptags +string.split = split +string.trim = trim +string.cmatch = cmatch +string.parse_units = parse_units + + +--- Appends numerically indexed tables or single objects to a given table. +-- @param src Target table +-- @param ... Objects to insert +-- @return Target table +function append(src, ...) + for i, a in ipairs({...}) do + if type(a) == "table" then + for j, v in ipairs(a) do + src[#src+1] = v + end + else + src[#src+1] = a + end + end + return src +end + +--- Combines two or more numerically indexed tables and single objects into one table. +-- @param tbl1 Table value to combine +-- @param tbl2 Table value to combine +-- @param ... More tables to combine +-- @return Table value containing all values of given tables +function combine(...) + return append({}, ...) +end + +--- Checks whether the given table contains the given value. +-- @param table Table value +-- @param value Value to search within the given table +-- @return Boolean indicating whether the given value occurs within table +function contains(table, value) + for k, v in pairs(table) do + if value == v then + return k + end + end + return false +end + +--- Update values in given table with the values from the second given table. +-- Both table are - in fact - merged together. +-- @param t Table which should be updated +-- @param updates Table containing the values to update +-- @return Always nil +function update(t, updates) + for k, v in pairs(updates) do + t[k] = v + end +end + +--- Retrieve all keys of given associative table. +-- @param t Table to extract keys from +-- @return Sorted table containing the keys +function keys(t) + local keys = { } + if t then + for k, _ in kspairs(t) do + keys[#keys+1] = k + end + end + return keys +end + +--- Clones the given object and return it's copy. +-- @param object Table value to clone +-- @param deep Boolean indicating whether to do recursive cloning +-- @return Cloned table value +function clone(object, deep) + local copy = {} + + for k, v in pairs(object) do + if deep and type(v) == "table" then + v = clone(v, deep) + end + copy[k] = v + end + + return setmetatable(copy, getmetatable(object)) +end + + +--- Create a dynamic table which automatically creates subtables. +-- @return Dynamic Table +function dtable() + return setmetatable({}, { __index = + function(tbl, key) + return rawget(tbl, key) + or rawget(rawset(tbl, key, dtable()), key) + end + }) +end + + +-- Serialize the contents of a table value. +function _serialize_table(t, seen) + assert(not seen[t], "Recursion detected.") + seen[t] = true + + local data = "" + local idata = "" + local ilen = 0 + + for k, v in pairs(t) do + if type(k) ~= "number" or k < 1 or math.floor(k) ~= k or ( k - #t ) > 3 then + k = serialize_data(k, seen) + v = serialize_data(v, seen) + data = data .. ( #data > 0 and ", " or "" ) .. + '[' .. k .. '] = ' .. v + elseif k > ilen then + ilen = k + end + end + + for i = 1, ilen do + local v = serialize_data(t[i], seen) + idata = idata .. ( #idata > 0 and ", " or "" ) .. v + end + + return idata .. ( #data > 0 and #idata > 0 and ", " or "" ) .. data +end + +--- Recursively serialize given data to lua code, suitable for restoring +-- with loadstring(). +-- @param val Value containing the data to serialize +-- @return String value containing the serialized code +-- @see restore_data +-- @see get_bytecode +function serialize_data(val, seen) + seen = seen or setmetatable({}, {__mode="k"}) + + if val == nil then + return "nil" + elseif type(val) == "number" then + return val + elseif type(val) == "string" then + return "%q" % val + elseif type(val) == "boolean" then + return val and "true" or "false" + elseif type(val) == "function" then + return "loadstring(%q)" % get_bytecode(val) + elseif type(val) == "table" then + return "{ " .. _serialize_table(val, seen) .. " }" + else + return '"[unhandled data type:' .. type(val) .. ']"' + end +end + +--- Restore data previously serialized with serialize_data(). +-- @param str String containing the data to restore +-- @return Value containing the restored data structure +-- @see serialize_data +-- @see get_bytecode +function restore_data(str) + return loadstring("return " .. str)() +end + + +-- +-- Byte code manipulation routines +-- + +--- Return the current runtime bytecode of the given data. The byte code +-- will be stripped before it is returned. +-- @param val Value to return as bytecode +-- @return String value containing the bytecode of the given data +function get_bytecode(val) + local code + + if type(val) == "function" then + code = string.dump(val) + else + code = string.dump( loadstring( "return " .. serialize_data(val) ) ) + end + + return code -- and strip_bytecode(code) +end + +--- Strips unnescessary lua bytecode from given string. Information like line +-- numbers and debugging numbers will be discarded. Original version by +-- Peter Cawley (http://lua-users.org/lists/lua-l/2008-02/msg01158.html) +-- @param code String value containing the original lua byte code +-- @return String value containing the stripped lua byte code +function strip_bytecode(code) + local version, format, endian, int, size, ins, num, lnum = code:byte(5, 12) + local subint + if endian == 1 then + subint = function(code, i, l) + local val = 0 + for n = l, 1, -1 do + val = val * 256 + code:byte(i + n - 1) + end + return val, i + l + end + else + subint = function(code, i, l) + local val = 0 + for n = 1, l, 1 do + val = val * 256 + code:byte(i + n - 1) + end + return val, i + l + end + end + + local function strip_function(code) + local count, offset = subint(code, 1, size) + local stripped = { string.rep("\0", size) } + local dirty = offset + count + offset = offset + count + int * 2 + 4 + offset = offset + int + subint(code, offset, int) * ins + count, offset = subint(code, offset, int) + for n = 1, count do + local t + t, offset = subint(code, offset, 1) + if t == 1 then + offset = offset + 1 + elseif t == 4 then + offset = offset + size + subint(code, offset, size) + elseif t == 3 then + offset = offset + num + elseif t == 254 or t == 9 then + offset = offset + lnum + end + end + count, offset = subint(code, offset, int) + stripped[#stripped+1] = code:sub(dirty, offset - 1) + for n = 1, count do + local proto, off = strip_function(code:sub(offset, -1)) + stripped[#stripped+1] = proto + offset = offset + off - 1 + end + offset = offset + subint(code, offset, int) * int + int + count, offset = subint(code, offset, int) + for n = 1, count do + offset = offset + subint(code, offset, size) + size + int * 2 + end + count, offset = subint(code, offset, int) + for n = 1, count do + offset = offset + subint(code, offset, size) + size + end + stripped[#stripped+1] = string.rep("\0", int * 3) + return table.concat(stripped), offset + end + + return code:sub(1,12) .. strip_function(code:sub(13,-1)) +end + + +-- +-- Sorting iterator functions +-- + +function _sortiter( t, f ) + local keys = { } + + local k, v + for k, v in pairs(t) do + keys[#keys+1] = k + end + + local _pos = 0 + + table.sort( keys, f ) + + return function() + _pos = _pos + 1 + if _pos <= #keys then + return keys[_pos], t[keys[_pos]], _pos + end + end +end + +--- Return a key, value iterator which returns the values sorted according to +-- the provided callback function. +-- @param t The table to iterate +-- @param f A callback function to decide the order of elements +-- @return Function value containing the corresponding iterator +function spairs(t,f) + return _sortiter( t, f ) +end + +--- Return a key, value iterator for the given table. +-- The table pairs are sorted by key. +-- @param t The table to iterate +-- @return Function value containing the corresponding iterator +function kspairs(t) + return _sortiter( t ) +end + +--- Return a key, value iterator for the given table. +-- The table pairs are sorted by value. +-- @param t The table to iterate +-- @return Function value containing the corresponding iterator +function vspairs(t) + return _sortiter( t, function (a,b) return t[a] < t[b] end ) +end + + +-- +-- System utility functions +-- + +--- Test whether the current system is operating in big endian mode. +-- @return Boolean value indicating whether system is big endian +function bigendian() + return string.byte(string.dump(function() end), 7) == 0 +end + +--- Execute given commandline and gather stdout. +-- @param command String containing command to execute +-- @return String containing the command's stdout +function exec(command) + local pp = io.popen(command) + local data = pp:read("*a") + pp:close() + + return data +end + +--- Return a line-buffered iterator over the output of given command. +-- @param command String containing the command to execute +-- @return Iterator +function execi(command) + local pp = io.popen(command) + + return pp and function() + local line = pp:read() + + if not line then + pp:close() + end + + return line + end +end + +-- Deprecated +function execl(command) + local pp = io.popen(command) + local line = "" + local data = {} + + while true do + line = pp:read() + if (line == nil) then break end + data[#data+1] = line + end + pp:close() + + return data +end + +--- Returns the absolute path to LuCI base directory. +-- @return String containing the directory path +function libpath() + return require "nixio.fs".dirname(ldebug.__file__) +end + + +-- +-- Coroutine safe xpcall and pcall versions modified for Luci +-- original version: +-- coxpcall 1.13 - Copyright 2005 - Kepler Project (www.keplerproject.org) +-- +-- Copyright © 2005 Kepler Project. +-- Permission is hereby granted, free of charge, to any person obtaining a +-- copy of this software and associated documentation files (the "Software"), +-- to deal in the Software without restriction, including without limitation +-- the rights to use, copy, modify, merge, publish, distribute, sublicense, +-- and/or sell copies of the Software, and to permit persons to whom the +-- Software is furnished to do so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be +-- included in all copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +-- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +-- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +-- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +-- DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +-- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE +-- OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +local performResume, handleReturnValue +local oldpcall, oldxpcall = pcall, xpcall +coxpt = {} +setmetatable(coxpt, {__mode = "kv"}) + +-- Identity function for copcall +local function copcall_id(trace, ...) + return ... +end + +--- This is a coroutine-safe drop-in replacement for Lua's "xpcall"-function +-- @param f Lua function to be called protected +-- @param err Custom error handler +-- @param ... Parameters passed to the function +-- @return A boolean whether the function call succeeded and the return +-- values of either the function or the error handler +function coxpcall(f, err, ...) + local res, co = oldpcall(coroutine.create, f) + if not res then + local params = {...} + local newf = function() return f(unpack(params)) end + co = coroutine.create(newf) + end + local c = coroutine.running() + coxpt[co] = coxpt[c] or c or 0 + + return performResume(err, co, ...) +end + +--- This is a coroutine-safe drop-in replacement for Lua's "pcall"-function +-- @param f Lua function to be called protected +-- @param ... Parameters passed to the function +-- @return A boolean whether the function call succeeded and the returns +-- values of the function or the error object +function copcall(f, ...) + return coxpcall(f, copcall_id, ...) +end + +-- Handle return value of protected call +function handleReturnValue(err, co, status, ...) + if not status then + return false, err(debug.traceback(co, (...)), ...) + end + + if coroutine.status(co) ~= 'suspended' then + return true, ... + end + + return performResume(err, co, coroutine.yield(...)) +end + +-- Resume execution of protected function call +function performResume(err, co, ...) + return handleReturnValue(err, co, coroutine.resume(co, ...)) +end diff --git a/modules/base/luasrc/version.lua b/modules/base/luasrc/version.lua new file mode 100644 index 0000000000..9e5cb719c4 --- /dev/null +++ b/modules/base/luasrc/version.lua @@ -0,0 +1,12 @@ +--[[ +LuCI - Lua Configuration Interface +Version definition - do not edit this file +]]-- + +module "luci.version" + +distname = "Host System" +distversion = "SDK" + +luciname = "LuCI" +luciversion = "SVN" diff --git a/modules/base/luasrc/view/error404.htm b/modules/base/luasrc/view/error404.htm new file mode 100644 index 0000000000..813604d12c --- /dev/null +++ b/modules/base/luasrc/view/error404.htm @@ -0,0 +1,19 @@ +<%# +LuCI - Lua Configuration Interface +Copyright 2008 Steven Barth <steven@midlink.org> +Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +-%> +<%+header%> +<h2><a id="content" name="content">404 <%:Not Found%></a></h2> +<p><%:Sorry, the object you requested was not found.%></p> +<tt><%:Unable to dispatch%>: <%=luci.http.request.env.PATH_INFO%></tt> +<%+footer%> diff --git a/modules/base/luasrc/view/error500.htm b/modules/base/luasrc/view/error500.htm new file mode 100644 index 0000000000..14ba0410a4 --- /dev/null +++ b/modules/base/luasrc/view/error500.htm @@ -0,0 +1,19 @@ +<%# +LuCI - Lua Configuration Interface +Copyright 2008 Steven Barth <steven@midlink.org> +Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +-%> +<%+header%> +<h2><a id="content" name="content">500 <%:Internal Server Error%></a></h2> +<p><%:Sorry, the server encountered an unexpected error.%></p> +<pre class="error500"><%=message%></pre> +<%+footer%> diff --git a/modules/base/luasrc/view/footer.htm b/modules/base/luasrc/view/footer.htm new file mode 100644 index 0000000000..6c6d214210 --- /dev/null +++ b/modules/base/luasrc/view/footer.htm @@ -0,0 +1,15 @@ +<%# +LuCI - Lua Configuration Interface +Copyright 2008 Steven Barth <steven@midlink.org> +Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +-%> +<% include("themes/" .. theme .. "/footer") %>
\ No newline at end of file diff --git a/modules/base/luasrc/view/header.htm b/modules/base/luasrc/view/header.htm new file mode 100644 index 0000000000..77018b1173 --- /dev/null +++ b/modules/base/luasrc/view/header.htm @@ -0,0 +1,21 @@ +<%# +LuCI - Lua Configuration Interface +Copyright 2008 Steven Barth <steven@midlink.org> +Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +-%> + +<% + if not luci.dispatcher.context.template_header_sent then + include("themes/" .. theme .. "/header") + luci.dispatcher.context.template_header_sent = true + end +%> diff --git a/modules/base/luasrc/view/indexer.htm b/modules/base/luasrc/view/indexer.htm new file mode 100644 index 0000000000..c628289711 --- /dev/null +++ b/modules/base/luasrc/view/indexer.htm @@ -0,0 +1,15 @@ +<%# +LuCI - Lua Configuration Interface +Copyright 2008 Steven Barth <steven@midlink.org> +Copyright 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ + +-%> +<% include("themes/" .. theme .. "/indexer") %>
\ No newline at end of file diff --git a/modules/base/luasrc/view/sysauth.htm b/modules/base/luasrc/view/sysauth.htm new file mode 100644 index 0000000000..7c39f0da51 --- /dev/null +++ b/modules/base/luasrc/view/sysauth.htm @@ -0,0 +1,80 @@ +<%# +LuCI - Lua Configuration Interface +Copyright 2008 Steven Barth <steven@midlink.org> +Copyright 2008-2012 Jo-Philipp Wich <xm@subsignal.org> + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +-%> + +<%+header%> + +<form method="post" action="<%=pcdata(luci.http.getenv("REQUEST_URI"))%>"> + <div class="cbi-map"> + <h2><a id="content" name="content"><%:Authorization Required%></a></h2> + <div class="cbi-map-descr"> + <%:Please enter your username and password.%> + <%- if fuser then %> + <div class="error"><%:Invalid username and/or password! Please try again.%></div> + <br /> + <% end -%> + </div> + <fieldset class="cbi-section"><fieldset class="cbi-section-node"> + <div class="cbi-value"> + <label class="cbi-value-title"><%:Username%></label> + <div class="cbi-value-field"> + <input class="cbi-input-user" type="text" name="username" value="<%=duser%>" /> + </div> + </div> + <div class="cbi-value cbi-value-last"> + <label class="cbi-value-title"><%:Password%></label> + <div class="cbi-value-field"> + <input id="focus_password" class="cbi-input-password" type="password" name="password" /> + </div> + </div> + </fieldset></fieldset> + </div> + + <div> + <input type="submit" value="<%:Login%>" class="cbi-button cbi-button-apply" /> + <input type="reset" value="<%:Reset%>" class="cbi-button cbi-button-reset" /> + </div> +</form> +<script type="text/javascript">//<![CDATA[ + var input = document.getElementById('focus_password'); + 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%>/cbi/up.gif?' + Math.random();; + setTimeout(function(){ + img.src='' + }, 5000); + } +//]]></script> + +<% end %> + +<%+footer%> diff --git a/modules/base/root/etc/config/ucitrack b/modules/base/root/etc/config/ucitrack new file mode 100644 index 0000000000..04467f4fdf --- /dev/null +++ b/modules/base/root/etc/config/ucitrack @@ -0,0 +1,53 @@ +config network + option init network + list affects dhcp + list affects radvd + +config wireless + list affects network + +config firewall + option init firewall + list affects luci-splash + list affects qos + list affects miniupnpd + +config olsr + option init olsrd + +config dhcp + option init dnsmasq + +config dropbear + option init dropbear + +config httpd + option init httpd + +config fstab + option init fstab + +config qos + option init qos + +config system + option init led + list affects luci_statistics + +config luci_splash + option init luci_splash + +config upnpd + option init miniupnpd + +config ntpclient + option init ntpclient + +config samba + option init samba + +config tinyproxy + option init tinyproxy + +config 6relayd + option init 6relayd diff --git a/modules/base/root/root/etc/config/luci b/modules/base/root/root/etc/config/luci new file mode 100644 index 0000000000..c503a8f1e0 --- /dev/null +++ b/modules/base/root/root/etc/config/luci @@ -0,0 +1,24 @@ +config core main + option lang auto + option mediaurlbase /luci-static/openwrt.org + option resourcebase /luci-static/resources + +config extern flash_keep + option uci "/etc/config/" + option dropbear "/etc/dropbear/" + option openvpn "/etc/openvpn/" + option passwd "/etc/passwd" + option opkg "/etc/opkg.conf" + option firewall "/etc/firewall.user" + option uploads "/lib/uci/upload/" + +config internal languages + +config internal sauth + option sessionpath "/tmp/luci-sessions" + option sessiontime 3600 + +config internal ccache + option enable 1 + +config internal themes diff --git a/modules/base/root/root/lib/uci/upload/.gitignore b/modules/base/root/root/lib/uci/upload/.gitignore new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/modules/base/root/root/lib/uci/upload/.gitignore diff --git a/modules/base/root/sbin/luci-reload b/modules/base/root/sbin/luci-reload new file mode 100755 index 0000000000..cc41da2bb7 --- /dev/null +++ b/modules/base/root/sbin/luci-reload @@ -0,0 +1,45 @@ +#!/bin/sh +. /lib/functions.sh + +apply_config() { + config_get init "$1" init + config_get exec "$1" exec + config_get test "$1" test + + echo "$2" > "/var/run/luci-reload-status" + + [ -n "$init" ] && reload_init "$2" "$init" "$test" + [ -n "$exec" ] && reload_exec "$2" "$exec" "$test" +} + +reload_exec() { + local service="$1" + local ok="$3" + set -- $2 + local cmd="$1"; shift + + [ -x "$cmd" ] && { + echo "Reloading $service... " + ( $cmd "$@" ) 2>/dev/null 1>&2 + [ -n "$ok" -a "$?" != "$ok" ] && echo '!!! Failed to reload' $service '!!!' + } +} + +reload_init() { + [ -x /etc/init.d/$2 ] && /etc/init.d/$2 enabled && { + echo "Reloading $1... " + /etc/init.d/$2 reload >/dev/null 2>&1 + [ -n "$3" -a "$?" != "$3" ] && echo '!!! Failed to reload' $1 '!!!' + } +} + +lock "/var/run/luci-reload" + +config_load ucitrack + +for i in $*; do + config_foreach apply_config $i $i +done + +rm -f "/var/run/luci-reload-status" +lock -u "/var/run/luci-reload" diff --git a/modules/base/root/www/index.html b/modules/base/root/www/index.html new file mode 100644 index 0000000000..0a7238b556 --- /dev/null +++ b/modules/base/root/www/index.html @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +<meta http-equiv="refresh" content="0; URL=/cgi-bin/luci" /> +</head> +<body style="background-color: black"> +<a style="color: white; text-decoration: none" href="/cgi-bin/luci">LuCI - Lua Configuration Interface</a> +</body> +</html> diff --git a/modules/base/src/po2lmo.c b/modules/base/src/po2lmo.c new file mode 100644 index 0000000000..0da792b680 --- /dev/null +++ b/modules/base/src/po2lmo.c @@ -0,0 +1,247 @@ +/* + * lmo - Lua Machine Objects - PO to LMO conversion tool + * + * Copyright (C) 2009-2012 Jo-Philipp Wich <xm@subsignal.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "template_lmo.h" + +static void die(const char *msg) +{ + fprintf(stderr, "Error: %s\n", msg); + exit(1); +} + +static void usage(const char *name) +{ + fprintf(stderr, "Usage: %s input.po output.lmo\n", name); + exit(1); +} + +static void print(const void *ptr, size_t size, size_t nmemb, FILE *stream) +{ + if( fwrite(ptr, size, nmemb, stream) == 0 ) + die("Failed to write stdout"); +} + +static int extract_string(const char *src, char *dest, int len) +{ + int pos = 0; + int esc = 0; + int off = -1; + + for( pos = 0; (pos < strlen(src)) && (pos < len); pos++ ) + { + if( (off == -1) && (src[pos] == '"') ) + { + off = pos + 1; + } + else if( off >= 0 ) + { + if( esc == 1 ) + { + switch (src[pos]) + { + case '"': + case '\\': + off++; + break; + } + dest[pos-off] = src[pos]; + esc = 0; + } + else if( src[pos] == '\\' ) + { + dest[pos-off] = src[pos]; + esc = 1; + } + else if( src[pos] != '"' ) + { + dest[pos-off] = src[pos]; + } + else + { + dest[pos-off] = '\0'; + break; + } + } + } + + return (off > -1) ? strlen(dest) : -1; +} + +static int cmp_index(const void *a, const void *b) +{ + uint32_t x = ((const lmo_entry_t *)a)->key_id; + uint32_t y = ((const lmo_entry_t *)b)->key_id; + + if (x < y) + return -1; + else if (x > y) + return 1; + + return 0; +} + +static void print_uint32(uint32_t x, FILE *out) +{ + uint32_t y = htonl(x); + print(&y, sizeof(uint32_t), 1, out); +} + +static void print_index(void *array, int n, FILE *out) +{ + lmo_entry_t *e; + + qsort(array, n, sizeof(*e), cmp_index); + + for (e = array; n > 0; n--, e++) + { + print_uint32(e->key_id, out); + print_uint32(e->val_id, out); + print_uint32(e->offset, out); + print_uint32(e->length, out); + } +} + +int main(int argc, char *argv[]) +{ + char line[4096]; + char key[4096]; + char val[4096]; + char tmp[4096]; + int state = 0; + int offset = 0; + int length = 0; + int n_entries = 0; + void *array = NULL; + lmo_entry_t *entry = NULL; + uint32_t key_id, val_id; + + FILE *in; + FILE *out; + + if( (argc != 3) || ((in = fopen(argv[1], "r")) == NULL) || ((out = fopen(argv[2], "w")) == NULL) ) + usage(argv[0]); + + memset(line, 0, sizeof(key)); + memset(key, 0, sizeof(val)); + memset(val, 0, sizeof(val)); + + while( (NULL != fgets(line, sizeof(line), in)) || (state >= 2 && feof(in)) ) + { + if( state == 0 && strstr(line, "msgid \"") == line ) + { + switch(extract_string(line, key, sizeof(key))) + { + case -1: + die("Syntax error in msgid"); + case 0: + state = 1; + break; + default: + state = 2; + } + } + else if( state == 1 || state == 2 ) + { + if( strstr(line, "msgstr \"") == line || state == 2 ) + { + switch(extract_string(line, val, sizeof(val))) + { + case -1: + state = 4; + break; + default: + state = 3; + } + } + else + { + switch(extract_string(line, tmp, sizeof(tmp))) + { + case -1: + state = 2; + break; + default: + strcat(key, tmp); + } + } + } + else if( state == 3 ) + { + switch(extract_string(line, tmp, sizeof(tmp))) + { + case -1: + state = 4; + break; + default: + strcat(val, tmp); + } + } + + if( state == 4 ) + { + if( strlen(key) > 0 && strlen(val) > 0 ) + { + key_id = sfh_hash(key, strlen(key)); + val_id = sfh_hash(val, strlen(val)); + + if( key_id != val_id ) + { + n_entries++; + array = realloc(array, n_entries * sizeof(lmo_entry_t)); + entry = (lmo_entry_t *)array + n_entries - 1; + + if (!array) + die("Out of memory"); + + entry->key_id = key_id; + entry->val_id = val_id; + entry->offset = offset; + entry->length = strlen(val); + + length = strlen(val) + ((4 - (strlen(val) % 4)) % 4); + + print(val, length, 1, out); + offset += length; + } + } + + state = 0; + memset(key, 0, sizeof(key)); + memset(val, 0, sizeof(val)); + } + + memset(line, 0, sizeof(line)); + } + + print_index(array, n_entries, out); + + if( offset > 0 ) + { + print_uint32(offset, out); + fsync(fileno(out)); + fclose(out); + } + else + { + fclose(out); + unlink(argv[2]); + } + + fclose(in); + return(0); +} diff --git a/modules/base/src/template_lmo.c b/modules/base/src/template_lmo.c new file mode 100644 index 0000000000..27205a7228 --- /dev/null +++ b/modules/base/src/template_lmo.c @@ -0,0 +1,328 @@ +/* + * lmo - Lua Machine Objects - Base functions + * + * Copyright (C) 2009-2010 Jo-Philipp Wich <xm@subsignal.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "template_lmo.h" + +/* + * Hash function from http://www.azillionmonkeys.com/qed/hash.html + * Copyright (C) 2004-2008 by Paul Hsieh + */ + +uint32_t sfh_hash(const char *data, int len) +{ + uint32_t hash = len, tmp; + int rem; + + if (len <= 0 || data == NULL) return 0; + + rem = len & 3; + len >>= 2; + + /* Main loop */ + for (;len > 0; len--) { + hash += sfh_get16(data); + tmp = (sfh_get16(data+2) << 11) ^ hash; + hash = (hash << 16) ^ tmp; + data += 2*sizeof(uint16_t); + hash += hash >> 11; + } + + /* Handle end cases */ + switch (rem) { + case 3: hash += sfh_get16(data); + hash ^= hash << 16; + hash ^= data[sizeof(uint16_t)] << 18; + hash += hash >> 11; + break; + case 2: hash += sfh_get16(data); + hash ^= hash << 11; + hash += hash >> 17; + break; + case 1: hash += *data; + hash ^= hash << 10; + hash += hash >> 1; + } + + /* Force "avalanching" of final 127 bits */ + hash ^= hash << 3; + hash += hash >> 5; + hash ^= hash << 4; + hash += hash >> 17; + hash ^= hash << 25; + hash += hash >> 6; + + return hash; +} + +uint32_t lmo_canon_hash(const char *str, int len) +{ + char res[4096]; + char *ptr, prev; + int off; + + if (!str || len >= sizeof(res)) + return 0; + + for (prev = ' ', ptr = res, off = 0; off < len; prev = *str, off++, str++) + { + if (isspace(*str)) + { + if (!isspace(prev)) + *ptr++ = ' '; + } + else + { + *ptr++ = *str; + } + } + + if ((ptr > res) && isspace(*(ptr-1))) + ptr--; + + return sfh_hash(res, ptr - res); +} + +lmo_archive_t * lmo_open(const char *file) +{ + int in = -1; + uint32_t idx_offset = 0; + struct stat s; + + lmo_archive_t *ar = NULL; + + if (stat(file, &s) == -1) + goto err; + + if ((in = open(file, O_RDONLY)) == -1) + goto err; + + if ((ar = (lmo_archive_t *)malloc(sizeof(*ar))) != NULL) + { + memset(ar, 0, sizeof(*ar)); + + ar->fd = in; + ar->size = s.st_size; + + fcntl(ar->fd, F_SETFD, fcntl(ar->fd, F_GETFD) | FD_CLOEXEC); + + if ((ar->mmap = mmap(NULL, ar->size, PROT_READ, MAP_SHARED, ar->fd, 0)) == MAP_FAILED) + goto err; + + idx_offset = ntohl(*((const uint32_t *) + (ar->mmap + ar->size - sizeof(uint32_t)))); + + if (idx_offset >= ar->size) + goto err; + + ar->index = (lmo_entry_t *)(ar->mmap + idx_offset); + ar->length = (ar->size - idx_offset - sizeof(uint32_t)) / sizeof(lmo_entry_t); + ar->end = ar->mmap + ar->size; + + return ar; + } + +err: + if (in > -1) + close(in); + + if (ar != NULL) + { + if ((ar->mmap != NULL) && (ar->mmap != MAP_FAILED)) + munmap(ar->mmap, ar->size); + + free(ar); + } + + return NULL; +} + +void lmo_close(lmo_archive_t *ar) +{ + if (ar != NULL) + { + if ((ar->mmap != NULL) && (ar->mmap != MAP_FAILED)) + munmap(ar->mmap, ar->size); + + close(ar->fd); + free(ar); + + ar = NULL; + } +} + + +lmo_catalog_t *_lmo_catalogs = NULL; +lmo_catalog_t *_lmo_active_catalog = NULL; + +int lmo_load_catalog(const char *lang, const char *dir) +{ + DIR *dh = NULL; + char pattern[16]; + char path[PATH_MAX]; + struct dirent *de = NULL; + + lmo_archive_t *ar = NULL; + lmo_catalog_t *cat = NULL; + + if (!lmo_change_catalog(lang)) + return 0; + + if (!dir || !(dh = opendir(dir))) + goto err; + + if (!(cat = malloc(sizeof(*cat)))) + goto err; + + memset(cat, 0, sizeof(*cat)); + + snprintf(cat->lang, sizeof(cat->lang), "%s", lang); + snprintf(pattern, sizeof(pattern), "*.%s.lmo", lang); + + while ((de = readdir(dh)) != NULL) + { + if (!fnmatch(pattern, de->d_name, 0)) + { + snprintf(path, sizeof(path), "%s/%s", dir, de->d_name); + ar = lmo_open(path); + + if (ar) + { + ar->next = cat->archives; + cat->archives = ar; + } + } + } + + closedir(dh); + + cat->next = _lmo_catalogs; + _lmo_catalogs = cat; + + if (!_lmo_active_catalog) + _lmo_active_catalog = cat; + + return 0; + +err: + if (dh) closedir(dh); + if (cat) free(cat); + + return -1; +} + +int lmo_change_catalog(const char *lang) +{ + lmo_catalog_t *cat; + + for (cat = _lmo_catalogs; cat; cat = cat->next) + { + if (!strncmp(cat->lang, lang, sizeof(cat->lang))) + { + _lmo_active_catalog = cat; + return 0; + } + } + + return -1; +} + +static lmo_entry_t * lmo_find_entry(lmo_archive_t *ar, uint32_t hash) +{ + unsigned int m, l, r; + uint32_t k; + + l = 0; + r = ar->length - 1; + + while (1) + { + m = l + ((r - l) / 2); + + if (r < l) + break; + + k = ntohl(ar->index[m].key_id); + + if (k == hash) + return &ar->index[m]; + + if (k > hash) + { + if (!m) + break; + + r = m - 1; + } + else + { + l = m + 1; + } + } + + return NULL; +} + +int lmo_translate(const char *key, int keylen, char **out, int *outlen) +{ + uint32_t hash; + lmo_entry_t *e; + lmo_archive_t *ar; + + if (!key || !_lmo_active_catalog) + return -2; + + hash = lmo_canon_hash(key, keylen); + + for (ar = _lmo_active_catalog->archives; ar; ar = ar->next) + { + if ((e = lmo_find_entry(ar, hash)) != NULL) + { + *out = ar->mmap + ntohl(e->offset); + *outlen = ntohl(e->length); + return 0; + } + } + + return -1; +} + +void lmo_close_catalog(const char *lang) +{ + lmo_archive_t *ar, *next; + lmo_catalog_t *cat, *prev; + + for (prev = NULL, cat = _lmo_catalogs; cat; prev = cat, cat = cat->next) + { + if (!strncmp(cat->lang, lang, sizeof(cat->lang))) + { + if (prev) + prev->next = cat->next; + else + _lmo_catalogs = cat->next; + + for (ar = cat->archives; ar; ar = next) + { + next = ar->next; + lmo_close(ar); + } + + free(cat); + break; + } + } +} diff --git a/modules/base/src/template_lmo.h b/modules/base/src/template_lmo.h new file mode 100644 index 0000000000..57f59aa56b --- /dev/null +++ b/modules/base/src/template_lmo.h @@ -0,0 +1,92 @@ +/* + * lmo - Lua Machine Objects - General header + * + * Copyright (C) 2009-2012 Jo-Philipp Wich <xm@subsignal.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _TEMPLATE_LMO_H_ +#define _TEMPLATE_LMO_H_ + +#include <stdlib.h> +#include <stdio.h> +#include <stdint.h> +#include <string.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <sys/mman.h> +#include <arpa/inet.h> +#include <unistd.h> +#include <errno.h> +#include <fnmatch.h> +#include <dirent.h> +#include <ctype.h> +#include <limits.h> + +#if (defined(__GNUC__) && defined(__i386__)) +#define sfh_get16(d) (*((const uint16_t *) (d))) +#else +#define sfh_get16(d) ((((uint32_t)(((const uint8_t *)(d))[1])) << 8)\ + +(uint32_t)(((const uint8_t *)(d))[0]) ) +#endif + + +struct lmo_entry { + uint32_t key_id; + uint32_t val_id; + uint32_t offset; + uint32_t length; +} __attribute__((packed)); + +typedef struct lmo_entry lmo_entry_t; + + +struct lmo_archive { + int fd; + int length; + uint32_t size; + lmo_entry_t *index; + char *mmap; + char *end; + struct lmo_archive *next; +}; + +typedef struct lmo_archive lmo_archive_t; + + +struct lmo_catalog { + char lang[6]; + struct lmo_archive *archives; + struct lmo_catalog *next; +}; + +typedef struct lmo_catalog lmo_catalog_t; + + +uint32_t sfh_hash(const char *data, int len); +uint32_t lmo_canon_hash(const char *data, int len); + +lmo_archive_t * lmo_open(const char *file); +void lmo_close(lmo_archive_t *ar); + + +extern lmo_catalog_t *_lmo_catalogs; +extern lmo_catalog_t *_lmo_active_catalog; + +int lmo_load_catalog(const char *lang, const char *dir); +int lmo_change_catalog(const char *lang); +int lmo_translate(const char *key, int keylen, char **out, int *outlen); +void lmo_close_catalog(const char *lang); + +#endif diff --git a/modules/base/src/template_lualib.c b/modules/base/src/template_lualib.c new file mode 100644 index 0000000000..0d43641041 --- /dev/null +++ b/modules/base/src/template_lualib.c @@ -0,0 +1,163 @@ +/* + * LuCI Template - Lua binding + * + * Copyright (C) 2009 Jo-Philipp Wich <xm@subsignal.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "template_lualib.h" + +int template_L_parse(lua_State *L) +{ + const char *file = luaL_checkstring(L, 1); + struct template_parser *parser = template_open(file); + int lua_status, rv; + + if (!parser) + { + lua_pushnil(L); + lua_pushinteger(L, errno); + lua_pushstring(L, strerror(errno)); + return 3; + } + + lua_status = lua_load(L, template_reader, parser, file); + + if (lua_status == 0) + rv = 1; + else + rv = template_error(L, parser); + + template_close(parser); + + return rv; +} + +int template_L_utf8(lua_State *L) +{ + size_t len = 0; + const char *str = luaL_checklstring(L, 1, &len); + char *res = utf8(str, len); + + if (res != NULL) + { + lua_pushstring(L, res); + free(res); + + return 1; + } + + return 0; +} + +int template_L_pcdata(lua_State *L) +{ + size_t len = 0; + const char *str = luaL_checklstring(L, 1, &len); + char *res = pcdata(str, len); + + if (res != NULL) + { + lua_pushstring(L, res); + free(res); + + return 1; + } + + return 0; +} + +int template_L_striptags(lua_State *L) +{ + size_t len = 0; + const char *str = luaL_checklstring(L, 1, &len); + char *res = striptags(str, len); + + if (res != NULL) + { + lua_pushstring(L, res); + free(res); + + return 1; + } + + return 0; +} + +static int template_L_load_catalog(lua_State *L) { + const char *lang = luaL_optstring(L, 1, "en"); + const char *dir = luaL_optstring(L, 2, NULL); + lua_pushboolean(L, !lmo_load_catalog(lang, dir)); + return 1; +} + +static int template_L_close_catalog(lua_State *L) { + const char *lang = luaL_optstring(L, 1, "en"); + lmo_close_catalog(lang); + return 0; +} + +static int template_L_change_catalog(lua_State *L) { + const char *lang = luaL_optstring(L, 1, "en"); + lua_pushboolean(L, !lmo_change_catalog(lang)); + return 1; +} + +static int template_L_translate(lua_State *L) { + size_t len; + char *tr; + int trlen; + const char *key = luaL_checklstring(L, 1, &len); + + switch (lmo_translate(key, len, &tr, &trlen)) + { + case 0: + lua_pushlstring(L, tr, trlen); + return 1; + + case -1: + return 0; + } + + lua_pushnil(L); + lua_pushstring(L, "no catalog loaded"); + return 2; +} + +static int template_L_hash(lua_State *L) { + size_t len; + const char *key = luaL_checklstring(L, 1, &len); + lua_pushinteger(L, sfh_hash(key, len)); + return 1; +} + + +/* module table */ +static const luaL_reg R[] = { + { "parse", template_L_parse }, + { "utf8", template_L_utf8 }, + { "pcdata", template_L_pcdata }, + { "striptags", template_L_striptags }, + { "load_catalog", template_L_load_catalog }, + { "close_catalog", template_L_close_catalog }, + { "change_catalog", template_L_change_catalog }, + { "translate", template_L_translate }, + { "hash", template_L_hash }, + { NULL, NULL } +}; + +LUALIB_API int luaopen_luci_template_parser(lua_State *L) { + luaL_register(L, TEMPLATE_LUALIB_META, R); + return 1; +} diff --git a/modules/base/src/template_lualib.h b/modules/base/src/template_lualib.h new file mode 100644 index 0000000000..1b659be126 --- /dev/null +++ b/modules/base/src/template_lualib.h @@ -0,0 +1,30 @@ +/* + * LuCI Template - Lua library header + * + * Copyright (C) 2009 Jo-Philipp Wich <xm@subsignal.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _TEMPLATE_LUALIB_H_ +#define _TEMPLATE_LUALIB_H_ + +#include "template_parser.h" +#include "template_utils.h" +#include "template_lmo.h" + +#define TEMPLATE_LUALIB_META "template.parser" + +LUALIB_API int luaopen_luci_template_parser(lua_State *L); + +#endif diff --git a/modules/base/src/template_parser.c b/modules/base/src/template_parser.c new file mode 100644 index 0000000000..6054451315 --- /dev/null +++ b/modules/base/src/template_parser.c @@ -0,0 +1,386 @@ +/* + * LuCI Template - Parser implementation + * + * Copyright (C) 2009-2012 Jo-Philipp Wich <xm@subsignal.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "template_parser.h" +#include "template_utils.h" +#include "template_lmo.h" + + +/* leading and trailing code for different types */ +const char *gen_code[9][2] = { + { NULL, NULL }, + { "write(\"", "\")" }, + { NULL, NULL }, + { "write(tostring(", " or \"\"))" }, + { "include(\"", "\")" }, + { "write(\"", "\")" }, + { "write(\"", "\")" }, + { NULL, " " }, + { NULL, NULL }, +}; + +/* Simple strstr() like function that takes len arguments for both haystack and needle. */ +static char *strfind(char *haystack, int hslen, const char *needle, int ndlen) +{ + int match = 0; + int i, j; + + for( i = 0; i < hslen; i++ ) + { + if( haystack[i] == needle[0] ) + { + match = ((ndlen == 1) || ((i + ndlen) <= hslen)); + + for( j = 1; (j < ndlen) && ((i + j) < hslen); j++ ) + { + if( haystack[i+j] != needle[j] ) + { + match = 0; + break; + } + } + + if( match ) + return &haystack[i]; + } + } + + return NULL; +} + +struct template_parser * template_open(const char *file) +{ + struct stat s; + static struct template_parser *parser; + + if (!(parser = malloc(sizeof(*parser)))) + goto err; + + memset(parser, 0, sizeof(*parser)); + parser->fd = -1; + parser->file = file; + + if (stat(file, &s)) + goto err; + + if ((parser->fd = open(file, O_RDONLY)) < 0) + goto err; + + parser->size = s.st_size; + parser->mmap = mmap(NULL, parser->size, PROT_READ, MAP_PRIVATE, + parser->fd, 0); + + if (parser->mmap != MAP_FAILED) + { + parser->off = parser->mmap; + parser->cur_chunk.type = T_TYPE_INIT; + parser->cur_chunk.s = parser->mmap; + parser->cur_chunk.e = parser->mmap; + + return parser; + } + +err: + template_close(parser); + return NULL; +} + +void template_close(struct template_parser *parser) +{ + if (!parser) + return; + + if (parser->gc != NULL) + free(parser->gc); + + if ((parser->mmap != NULL) && (parser->mmap != MAP_FAILED)) + munmap(parser->mmap, parser->size); + + if (parser->fd >= 0) + close(parser->fd); + + free(parser); +} + +void template_text(struct template_parser *parser, const char *e) +{ + const char *s = parser->off; + + if (s < (parser->mmap + parser->size)) + { + if (parser->strip_after) + { + while ((s <= e) && isspace(*s)) + s++; + } + + parser->cur_chunk.type = T_TYPE_TEXT; + } + else + { + parser->cur_chunk.type = T_TYPE_EOF; + } + + parser->cur_chunk.line = parser->line; + parser->cur_chunk.s = s; + parser->cur_chunk.e = e; +} + +void template_code(struct template_parser *parser, const char *e) +{ + const char *s = parser->off; + + parser->strip_before = 0; + parser->strip_after = 0; + + if (*s == '-') + { + parser->strip_before = 1; + for (s++; (s <= e) && (*s == ' ' || *s == '\t'); s++); + } + + if (*(e-1) == '-') + { + parser->strip_after = 1; + for (e--; (e >= s) && (*e == ' ' || *e == '\t'); e--); + } + + switch (*s) + { + /* comment */ + case '#': + s++; + parser->cur_chunk.type = T_TYPE_COMMENT; + break; + + /* include */ + case '+': + s++; + parser->cur_chunk.type = T_TYPE_INCLUDE; + break; + + /* translate */ + case ':': + s++; + parser->cur_chunk.type = T_TYPE_I18N; + break; + + /* translate raw */ + case '_': + s++; + parser->cur_chunk.type = T_TYPE_I18N_RAW; + break; + + /* expr */ + case '=': + s++; + parser->cur_chunk.type = T_TYPE_EXPR; + break; + + /* code */ + default: + parser->cur_chunk.type = T_TYPE_CODE; + break; + } + + parser->cur_chunk.line = parser->line; + parser->cur_chunk.s = s; + parser->cur_chunk.e = e; +} + +static const char * +template_format_chunk(struct template_parser *parser, size_t *sz) +{ + const char *s, *p; + const char *head, *tail; + struct template_chunk *c = &parser->prv_chunk; + struct template_buffer *buf; + + *sz = 0; + s = parser->gc = NULL; + + if (parser->strip_before && c->type == T_TYPE_TEXT) + { + while ((c->e > c->s) && isspace(*(c->e - 1))) + c->e--; + } + + /* empty chunk */ + if (c->s == c->e) + { + if (c->type == T_TYPE_EOF) + { + *sz = 0; + s = NULL; + } + else + { + *sz = 1; + s = " "; + } + } + + /* format chunk */ + else if ((buf = buf_init(c->e - c->s)) != NULL) + { + if ((head = gen_code[c->type][0]) != NULL) + buf_append(buf, head, strlen(head)); + + switch (c->type) + { + case T_TYPE_TEXT: + luastr_escape(buf, c->s, c->e - c->s, 0); + break; + + case T_TYPE_EXPR: + buf_append(buf, c->s, c->e - c->s); + for (p = c->s; p < c->e; p++) + parser->line += (*p == '\n'); + break; + + case T_TYPE_INCLUDE: + luastr_escape(buf, c->s, c->e - c->s, 0); + break; + + case T_TYPE_I18N: + luastr_translate(buf, c->s, c->e - c->s, 1); + break; + + case T_TYPE_I18N_RAW: + luastr_translate(buf, c->s, c->e - c->s, 0); + break; + + case T_TYPE_CODE: + buf_append(buf, c->s, c->e - c->s); + for (p = c->s; p < c->e; p++) + parser->line += (*p == '\n'); + break; + } + + if ((tail = gen_code[c->type][1]) != NULL) + buf_append(buf, tail, strlen(tail)); + + *sz = buf_length(buf); + s = parser->gc = buf_destroy(buf); + + if (!*sz) + { + *sz = 1; + s = " "; + } + } + + return s; +} + +const char *template_reader(lua_State *L, void *ud, size_t *sz) +{ + struct template_parser *parser = ud; + int rem = parser->size - (parser->off - parser->mmap); + char *tag; + + parser->prv_chunk = parser->cur_chunk; + + /* free previous string */ + if (parser->gc) + { + free(parser->gc); + parser->gc = NULL; + } + + /* before tag */ + if (!parser->in_expr) + { + if ((tag = strfind(parser->off, rem, "<%", 2)) != NULL) + { + template_text(parser, tag); + parser->off = tag + 2; + parser->in_expr = 1; + } + else + { + template_text(parser, parser->mmap + parser->size); + parser->off = parser->mmap + parser->size; + } + } + + /* inside tag */ + else + { + if ((tag = strfind(parser->off, rem, "%>", 2)) != NULL) + { + template_code(parser, tag); + parser->off = tag + 2; + parser->in_expr = 0; + } + else + { + /* unexpected EOF */ + template_code(parser, parser->mmap + parser->size); + + *sz = 1; + return "\033"; + } + } + + return template_format_chunk(parser, sz); +} + +int template_error(lua_State *L, struct template_parser *parser) +{ + const char *err = luaL_checkstring(L, -1); + const char *off = parser->prv_chunk.s; + const char *ptr; + char msg[1024]; + int line = 0; + int chunkline = 0; + + if ((ptr = strfind((char *)err, strlen(err), "]:", 2)) != NULL) + { + chunkline = atoi(ptr + 2) - parser->prv_chunk.line; + + while (*ptr) + { + if (*ptr++ == ' ') + { + err = ptr; + break; + } + } + } + + if (strfind((char *)err, strlen(err), "'char(27)'", 10) != NULL) + { + off = parser->mmap + parser->size; + err = "'%>' expected before end of file"; + chunkline = 0; + } + + for (ptr = parser->mmap; ptr < off; ptr++) + if (*ptr == '\n') + line++; + + snprintf(msg, sizeof(msg), "Syntax error in %s:%d: %s", + parser->file, line + chunkline, err ? err : "(unknown error)"); + + lua_pushnil(L); + lua_pushinteger(L, line + chunkline); + lua_pushstring(L, msg); + + return 3; +} diff --git a/modules/base/src/template_parser.h b/modules/base/src/template_parser.h new file mode 100644 index 0000000000..d1c606272e --- /dev/null +++ b/modules/base/src/template_parser.h @@ -0,0 +1,79 @@ +/* + * LuCI Template - Parser header + * + * Copyright (C) 2009 Jo-Philipp Wich <xm@subsignal.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _TEMPLATE_PARSER_H_ +#define _TEMPLATE_PARSER_H_ + +#include <stdlib.h> +#include <stdio.h> +#include <stdint.h> +#include <unistd.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <sys/mman.h> +#include <string.h> +#include <ctype.h> +#include <errno.h> + +#include <lua.h> +#include <lualib.h> +#include <lauxlib.h> + + +/* code types */ +#define T_TYPE_INIT 0 +#define T_TYPE_TEXT 1 +#define T_TYPE_COMMENT 2 +#define T_TYPE_EXPR 3 +#define T_TYPE_INCLUDE 4 +#define T_TYPE_I18N 5 +#define T_TYPE_I18N_RAW 6 +#define T_TYPE_CODE 7 +#define T_TYPE_EOF 8 + + +struct template_chunk { + const char *s; + const char *e; + int type; + int line; +}; + +/* parser state */ +struct template_parser { + int fd; + uint32_t size; + char *mmap; + char *off; + char *gc; + int line; + int in_expr; + int strip_before; + int strip_after; + struct template_chunk prv_chunk; + struct template_chunk cur_chunk; + const char *file; +}; + +struct template_parser * template_open(const char *file); +void template_close(struct template_parser *parser); + +const char *template_reader(lua_State *L, void *ud, size_t *sz); +int template_error(lua_State *L, struct template_parser *parser); + +#endif diff --git a/modules/base/src/template_utils.c b/modules/base/src/template_utils.c new file mode 100644 index 0000000000..80542bd4f3 --- /dev/null +++ b/modules/base/src/template_utils.c @@ -0,0 +1,494 @@ +/* + * LuCI Template - Utility functions + * + * Copyright (C) 2010 Jo-Philipp Wich <xm@subsignal.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "template_utils.h" +#include "template_lmo.h" + +/* initialize a buffer object */ +struct template_buffer * buf_init(int size) +{ + struct template_buffer *buf; + + if (size <= 0) + size = 1024; + + buf = (struct template_buffer *)malloc(sizeof(struct template_buffer)); + + if (buf != NULL) + { + buf->fill = 0; + buf->size = size; + buf->data = malloc(buf->size); + + if (buf->data != NULL) + { + buf->dptr = buf->data; + buf->data[0] = 0; + + return buf; + } + + free(buf); + } + + return NULL; +} + +/* grow buffer */ +int buf_grow(struct template_buffer *buf, int size) +{ + unsigned int off = (buf->dptr - buf->data); + char *data; + + if (size <= 0) + size = 1024; + + data = realloc(buf->data, buf->size + size); + + if (data != NULL) + { + buf->data = data; + buf->dptr = data + off; + buf->size += size; + + return buf->size; + } + + return 0; +} + +/* put one char into buffer object */ +int buf_putchar(struct template_buffer *buf, char c) +{ + if( ((buf->fill + 1) >= buf->size) && !buf_grow(buf, 0) ) + return 0; + + *(buf->dptr++) = c; + *(buf->dptr) = 0; + + buf->fill++; + return 1; +} + +/* append data to buffer */ +int buf_append(struct template_buffer *buf, const char *s, int len) +{ + if ((buf->fill + len + 1) >= buf->size) + { + if (!buf_grow(buf, len + 1)) + return 0; + } + + memcpy(buf->dptr, s, len); + buf->fill += len; + buf->dptr += len; + + *(buf->dptr) = 0; + + return len; +} + +/* read buffer length */ +int buf_length(struct template_buffer *buf) +{ + return buf->fill; +} + +/* destroy buffer object and return pointer to data */ +char * buf_destroy(struct template_buffer *buf) +{ + char *data = buf->data; + + free(buf); + return data; +} + + +/* calculate the number of expected continuation chars */ +static inline int mb_num_chars(unsigned char c) +{ + if ((c & 0xE0) == 0xC0) + return 2; + else if ((c & 0xF0) == 0xE0) + return 3; + else if ((c & 0xF8) == 0xF0) + return 4; + else if ((c & 0xFC) == 0xF8) + return 5; + else if ((c & 0xFE) == 0xFC) + return 6; + + return 1; +} + +/* test whether the given byte is a valid continuation char */ +static inline int mb_is_cont(unsigned char c) +{ + return ((c >= 0x80) && (c <= 0xBF)); +} + +/* test whether the byte sequence at the given pointer with the given + * length is the shortest possible representation of the code point */ +static inline int mb_is_shortest(unsigned char *s, int n) +{ + switch (n) + { + case 2: + /* 1100000x (10xxxxxx) */ + return !(((*s >> 1) == 0x60) && + ((*(s+1) >> 6) == 0x02)); + + case 3: + /* 11100000 100xxxxx (10xxxxxx) */ + return !((*s == 0xE0) && + ((*(s+1) >> 5) == 0x04) && + ((*(s+2) >> 6) == 0x02)); + + case 4: + /* 11110000 1000xxxx (10xxxxxx 10xxxxxx) */ + return !((*s == 0xF0) && + ((*(s+1) >> 4) == 0x08) && + ((*(s+2) >> 6) == 0x02) && + ((*(s+3) >> 6) == 0x02)); + + case 5: + /* 11111000 10000xxx (10xxxxxx 10xxxxxx 10xxxxxx) */ + return !((*s == 0xF8) && + ((*(s+1) >> 3) == 0x10) && + ((*(s+2) >> 6) == 0x02) && + ((*(s+3) >> 6) == 0x02) && + ((*(s+4) >> 6) == 0x02)); + + case 6: + /* 11111100 100000xx (10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx) */ + return !((*s == 0xF8) && + ((*(s+1) >> 2) == 0x20) && + ((*(s+2) >> 6) == 0x02) && + ((*(s+3) >> 6) == 0x02) && + ((*(s+4) >> 6) == 0x02) && + ((*(s+5) >> 6) == 0x02)); + } + + return 1; +} + +/* test whether the byte sequence at the given pointer with the given + * length is an UTF-16 surrogate */ +static inline int mb_is_surrogate(unsigned char *s, int n) +{ + return ((n == 3) && (*s == 0xED) && (*(s+1) >= 0xA0) && (*(s+1) <= 0xBF)); +} + +/* test whether the byte sequence at the given pointer with the given + * length is an illegal UTF-8 code point */ +static inline int mb_is_illegal(unsigned char *s, int n) +{ + return ((n == 3) && (*s == 0xEF) && (*(s+1) == 0xBF) && + (*(s+2) >= 0xBE) && (*(s+2) <= 0xBF)); +} + + +/* scan given source string, validate UTF-8 sequence and store result + * in given buffer object */ +static int _validate_utf8(unsigned char **s, int l, struct template_buffer *buf) +{ + unsigned char *ptr = *s; + unsigned int o = 0, v, n; + + /* ascii byte without null */ + if ((*(ptr+0) >= 0x01) && (*(ptr+0) <= 0x7F)) + { + if (!buf_putchar(buf, *ptr++)) + return 0; + + o = 1; + } + + /* multi byte sequence */ + else if ((n = mb_num_chars(*ptr)) > 1) + { + /* count valid chars */ + for (v = 1; (v <= n) && ((o+v) < l) && mb_is_cont(*(ptr+v)); v++); + + switch (n) + { + case 6: + case 5: + /* five and six byte sequences are always invalid */ + if (!buf_putchar(buf, '?')) + return 0; + + break; + + default: + /* if the number of valid continuation bytes matches the + * expected number and if the sequence is legal, copy + * the bytes to the destination buffer */ + if ((v == n) && mb_is_shortest(ptr, n) && + !mb_is_surrogate(ptr, n) && !mb_is_illegal(ptr, n)) + { + /* copy sequence */ + if (!buf_append(buf, (char *)ptr, n)) + return 0; + } + + /* the found sequence is illegal, skip it */ + else + { + /* invalid sequence */ + if (!buf_putchar(buf, '?')) + return 0; + } + + break; + } + + /* advance beyound the last found valid continuation char */ + o = v; + ptr += v; + } + + /* invalid byte (0x00) */ + else + { + if (!buf_putchar(buf, '?')) /* or 0xEF, 0xBF, 0xBD */ + return 0; + + o = 1; + ptr++; + } + + *s = ptr; + return o; +} + +/* sanitize given string and replace all invalid UTF-8 sequences with "?" */ +char * utf8(const char *s, unsigned int l) +{ + struct template_buffer *buf = buf_init(l); + unsigned char *ptr = (unsigned char *)s; + unsigned int v, o; + + if (!buf) + return NULL; + + for (o = 0; o < l; o++) + { + /* ascii char */ + if ((*ptr >= 0x01) && (*ptr <= 0x7F)) + { + if (!buf_putchar(buf, (char)*ptr++)) + break; + } + + /* invalid byte or multi byte sequence */ + else + { + if (!(v = _validate_utf8(&ptr, l - o, buf))) + break; + + o += (v - 1); + } + } + + return buf_destroy(buf); +} + +/* Sanitize given string and strip all invalid XML bytes + * Validate UTF-8 sequences + * Escape XML control chars */ +char * pcdata(const char *s, unsigned int l) +{ + struct template_buffer *buf = buf_init(l); + unsigned char *ptr = (unsigned char *)s; + unsigned int o, v; + char esq[8]; + int esl; + + if (!buf) + return NULL; + + for (o = 0; o < l; o++) + { + /* Invalid XML bytes */ + if (((*ptr >= 0x00) && (*ptr <= 0x08)) || + ((*ptr >= 0x0B) && (*ptr <= 0x0C)) || + ((*ptr >= 0x0E) && (*ptr <= 0x1F)) || + (*ptr == 0x7F)) + { + ptr++; + } + + /* Escapes */ + else if ((*ptr == 0x26) || + (*ptr == 0x27) || + (*ptr == 0x22) || + (*ptr == 0x3C) || + (*ptr == 0x3E)) + { + esl = snprintf(esq, sizeof(esq), "&#%i;", *ptr); + + if (!buf_append(buf, esq, esl)) + break; + + ptr++; + } + + /* ascii char */ + else if (*ptr <= 0x7F) + { + buf_putchar(buf, (char)*ptr++); + } + + /* multi byte sequence */ + else + { + if (!(v = _validate_utf8(&ptr, l - o, buf))) + break; + + o += (v - 1); + } + } + + return buf_destroy(buf); +} + +char * striptags(const char *s, unsigned int l) +{ + struct template_buffer *buf = buf_init(l); + unsigned char *ptr = (unsigned char *)s; + unsigned char *end = ptr + l; + unsigned char *tag; + unsigned char prev; + char esq[8]; + int esl; + + for (prev = ' '; ptr < end; ptr++) + { + if ((*ptr == '<') && ((ptr + 2) < end) && + ((*(ptr + 1) == '/') || isalpha(*(ptr + 1)))) + { + for (tag = ptr; tag < end; tag++) + { + if (*tag == '>') + { + if (!isspace(prev)) + buf_putchar(buf, ' '); + + ptr = tag; + prev = ' '; + break; + } + } + } + else if (isspace(*ptr)) + { + if (!isspace(prev)) + buf_putchar(buf, *ptr); + + prev = *ptr; + } + else + { + switch(*ptr) + { + case '"': + case '\'': + case '<': + case '>': + case '&': + esl = snprintf(esq, sizeof(esq), "&#%i;", *ptr); + buf_append(buf, esq, esl); + break; + + default: + buf_putchar(buf, *ptr); + break; + } + + prev = *ptr; + } + } + + return buf_destroy(buf); +} + +void luastr_escape(struct template_buffer *out, const char *s, unsigned int l, + int escape_xml) +{ + int esl; + char esq[8]; + char *ptr; + + for (ptr = (char *)s; ptr < (s + l); ptr++) + { + switch (*ptr) + { + case '\\': + buf_append(out, "\\\\", 2); + break; + + case '"': + if (escape_xml) + buf_append(out, """, 5); + else + buf_append(out, "\\\"", 2); + break; + + case '\n': + buf_append(out, "\\n", 2); + break; + + case '\'': + case '&': + case '<': + case '>': + if (escape_xml) + { + esl = snprintf(esq, sizeof(esq), "&#%i;", *ptr); + buf_append(out, esq, esl); + break; + } + + default: + buf_putchar(out, *ptr); + } + } +} + +void luastr_translate(struct template_buffer *out, const char *s, unsigned int l, + int escape_xml) +{ + char *tr; + int trlen; + + switch (lmo_translate(s, l, &tr, &trlen)) + { + case 0: + luastr_escape(out, tr, trlen, escape_xml); + break; + + case -1: + luastr_escape(out, s, l, escape_xml); + break; + + default: + /* no catalog loaded */ + break; + } +} diff --git a/modules/base/src/template_utils.h b/modules/base/src/template_utils.h new file mode 100644 index 0000000000..c54af757c5 --- /dev/null +++ b/modules/base/src/template_utils.h @@ -0,0 +1,49 @@ +/* + * LuCI Template - Utility header + * + * Copyright (C) 2010-2012 Jo-Philipp Wich <xm@subsignal.org> + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef _TEMPLATE_UTILS_H_ +#define _TEMPLATE_UTILS_H_ + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> + + +/* buffer object */ +struct template_buffer { + char *data; + char *dptr; + unsigned int size; + unsigned int fill; +}; + +struct template_buffer * buf_init(int size); +int buf_grow(struct template_buffer *buf, int size); +int buf_putchar(struct template_buffer *buf, char c); +int buf_append(struct template_buffer *buf, const char *s, int len); +int buf_length(struct template_buffer *buf); +char * buf_destroy(struct template_buffer *buf); + +char * utf8(const char *s, unsigned int l); +char * pcdata(const char *s, unsigned int l); +char * striptags(const char *s, unsigned int l); + +void luastr_escape(struct template_buffer *out, const char *s, unsigned int l, int escape_xml); +void luastr_translate(struct template_buffer *out, const char *s, unsigned int l, int escape_xml); + +#endif diff --git a/modules/base/standalone.mk b/modules/base/standalone.mk new file mode 100644 index 0000000000..66a0e5a2e7 --- /dev/null +++ b/modules/base/standalone.mk @@ -0,0 +1,56 @@ +LUAC = luac +LUAC_OPTIONS = -s +LUA_TARGET ?= source + +LUA_MODULEDIR = /usr/local/share/lua/5.1 +LUA_LIBRARYDIR = /usr/local/lib/lua/5.1 + +OS ?= $(shell uname) + +LUA_SHLIBS = $(shell pkg-config --silence-errors --libs lua5.1 || pkg-config --silence-errors --libs lua-5.1 || pkg-config --silence-errors --libs lua) +LUA_LIBS = $(if $(LUA_SHLIBS),$(LUA_SHLIBS),$(firstword $(wildcard /usr/lib/liblua.a /usr/local/lib/liblua.a /opt/local/lib/liblua.a))) +LUA_CFLAGS = $(shell pkg-config --silence-errors --cflags lua5.1 || pkg-config --silence-errors --cflags lua-5.1 || pkg-config --silence-errors --cflags lua) + +CC = gcc +AR = ar +RANLIB = ranlib +CFLAGS = -O2 +FPIC = -fPIC +EXTRA_CFLAGS = --std=gnu99 +WFLAGS = -Wall -Werror -pedantic +CPPFLAGS = +COMPILE = $(CC) $(CPPFLAGS) $(CFLAGS) $(EXTRA_CFLAGS) $(WFLAGS) +ifeq ($(OS),Darwin) + SHLIB_FLAGS = -bundle -undefined dynamic_lookup +else + SHLIB_FLAGS = -shared +endif +LINK = $(CC) $(LDFLAGS) + +.PHONY: all build compile luacompile luasource clean luaclean + +all: build + +build: luabuild gccbuild + +luabuild: lua$(LUA_TARGET) + +gccbuild: compile +compile: + +clean: luaclean + +luasource: + mkdir -p dist$(LUA_MODULEDIR) + cp -pR root/* dist 2>/dev/null || true + cp -pR lua/* dist$(LUA_MODULEDIR) 2>/dev/null || true + for i in $$(find dist -name .svn); do rm -rf $$i || true; done + +luastrip: luasource + for i in $$(find dist -type f -name '*.lua'); do perl -e 'undef $$/; open( F, "< $$ARGV[0]" ) || die $$!; $$src = <F>; close F; $$src =~ s/--\[\[.*?\]\](--)?//gs; $$src =~ s/^\s*--.*?\n//gm; open( F, "> $$ARGV[0]" ) || die $$!; print F $$src; close F' $$i; done + +luacompile: luasource + for i in $$(find dist -name *.lua -not -name debug.lua); do $(LUAC) $(LUAC_OPTIONS) -o $$i $$i; done + +luaclean: + rm -rf dist |