").append( jQuery.parseHTML( responseText ) ).find( selector ) :
+
+ // Otherwise use the full result
+ responseText );
+
+ }).complete( callback && function( jqXHR, status ) {
+ self.each( callback, response || [ jqXHR.responseText, status, jqXHR ] );
+ });
+ }
+
+ return this;
+};
+
+// Attach a bunch of functions for handling common AJAX events
+jQuery.each( [ "ajaxStart", "ajaxStop", "ajaxComplete", "ajaxError", "ajaxSuccess", "ajaxSend" ], function( i, type ){
+ jQuery.fn[ type ] = function( fn ){
+ return this.on( type, fn );
+ };
+});
+
+jQuery.each( [ "get", "post" ], function( i, method ) {
+ jQuery[ method ] = function( url, data, callback, type ) {
+ // shift arguments if data argument was omitted
+ if ( jQuery.isFunction( data ) ) {
+ type = type || callback;
+ callback = data;
+ data = undefined;
+ }
+
+ return jQuery.ajax({
+ url: url,
+ type: method,
+ dataType: type,
+ data: data,
+ success: callback
+ });
+ };
+});
+
+jQuery.extend({
+
+ // Counter for holding the number of active queries
+ active: 0,
+
+ // Last-Modified header cache for next request
+ lastModified: {},
+ etag: {},
+
+ ajaxSettings: {
+ url: ajaxLocation,
+ type: "GET",
+ isLocal: rlocalProtocol.test( ajaxLocParts[ 1 ] ),
+ global: true,
+ processData: true,
+ async: true,
+ contentType: "application/x-www-form-urlencoded; charset=UTF-8",
+ /*
+ timeout: 0,
+ data: null,
+ dataType: null,
+ username: null,
+ password: null,
+ cache: null,
+ throws: false,
+ traditional: false,
+ headers: {},
+ */
+
+ accepts: {
+ "*": allTypes,
+ text: "text/plain",
+ html: "text/html",
+ xml: "application/xml, text/xml",
+ json: "application/json, text/javascript"
+ },
+
+ contents: {
+ xml: /xml/,
+ html: /html/,
+ json: /json/
+ },
+
+ responseFields: {
+ xml: "responseXML",
+ text: "responseText"
+ },
+
+ // Data converters
+ // Keys separate source (or catchall "*") and destination types with a single space
+ converters: {
+
+ // Convert anything to text
+ "* text": window.String,
+
+ // Text to html (true = no transformation)
+ "text html": true,
+
+ // Evaluate text as a json expression
+ "text json": jQuery.parseJSON,
+
+ // Parse text as xml
+ "text xml": jQuery.parseXML
+ },
+
+ // For options that shouldn't be deep extended:
+ // you can add your own custom options here if
+ // and when you create one that shouldn't be
+ // deep extended (see ajaxExtend)
+ flatOptions: {
+ url: true,
+ context: true
+ }
+ },
+
+ // Creates a full fledged settings object into target
+ // with both ajaxSettings and settings fields.
+ // If target is omitted, writes into ajaxSettings.
+ ajaxSetup: function( target, settings ) {
+ return settings ?
+
+ // Building a settings object
+ ajaxExtend( ajaxExtend( target, jQuery.ajaxSettings ), settings ) :
+
+ // Extending ajaxSettings
+ ajaxExtend( jQuery.ajaxSettings, target );
+ },
+
+ ajaxPrefilter: addToPrefiltersOrTransports( prefilters ),
+ ajaxTransport: addToPrefiltersOrTransports( transports ),
+
+ // Main method
+ ajax: function( url, options ) {
+
+ // If url is an object, simulate pre-1.5 signature
+ if ( typeof url === "object" ) {
+ options = url;
+ url = undefined;
+ }
+
+ // Force options to be an object
+ options = options || {};
+
+ var // Cross-domain detection vars
+ parts,
+ // Loop variable
+ i,
+ // URL without anti-cache param
+ cacheURL,
+ // Response headers as string
+ responseHeadersString,
+ // timeout handle
+ timeoutTimer,
+
+ // To know if global events are to be dispatched
+ fireGlobals,
+
+ transport,
+ // Response headers
+ responseHeaders,
+ // Create the final options object
+ s = jQuery.ajaxSetup( {}, options ),
+ // Callbacks context
+ callbackContext = s.context || s,
+ // Context for global events is callbackContext if it is a DOM node or jQuery collection
+ globalEventContext = s.context && ( callbackContext.nodeType || callbackContext.jquery ) ?
+ jQuery( callbackContext ) :
+ jQuery.event,
+ // Deferreds
+ deferred = jQuery.Deferred(),
+ completeDeferred = jQuery.Callbacks("once memory"),
+ // Status-dependent callbacks
+ statusCode = s.statusCode || {},
+ // Headers (they are sent all at once)
+ requestHeaders = {},
+ requestHeadersNames = {},
+ // The jqXHR state
+ state = 0,
+ // Default abort message
+ strAbort = "canceled",
+ // Fake xhr
+ jqXHR = {
+ readyState: 0,
+
+ // Builds headers hashtable if needed
+ getResponseHeader: function( key ) {
+ var match;
+ if ( state === 2 ) {
+ if ( !responseHeaders ) {
+ responseHeaders = {};
+ while ( (match = rheaders.exec( responseHeadersString )) ) {
+ responseHeaders[ match[1].toLowerCase() ] = match[ 2 ];
+ }
+ }
+ match = responseHeaders[ key.toLowerCase() ];
+ }
+ return match == null ? null : match;
+ },
+
+ // Raw string
+ getAllResponseHeaders: function() {
+ return state === 2 ? responseHeadersString : null;
+ },
+
+ // Caches the header
+ setRequestHeader: function( name, value ) {
+ var lname = name.toLowerCase();
+ if ( !state ) {
+ name = requestHeadersNames[ lname ] = requestHeadersNames[ lname ] || name;
+ requestHeaders[ name ] = value;
+ }
+ return this;
+ },
+
+ // Overrides response content-type header
+ overrideMimeType: function( type ) {
+ if ( !state ) {
+ s.mimeType = type;
+ }
+ return this;
+ },
+
+ // Status-dependent callbacks
+ statusCode: function( map ) {
+ var code;
+ if ( map ) {
+ if ( state < 2 ) {
+ for ( code in map ) {
+ // Lazy-add the new callback in a way that preserves old ones
+ statusCode[ code ] = [ statusCode[ code ], map[ code ] ];
+ }
+ } else {
+ // Execute the appropriate callbacks
+ jqXHR.always( map[ jqXHR.status ] );
+ }
+ }
+ return this;
+ },
+
+ // Cancel the request
+ abort: function( statusText ) {
+ var finalText = statusText || strAbort;
+ if ( transport ) {
+ transport.abort( finalText );
+ }
+ done( 0, finalText );
+ return this;
+ }
+ };
+
+ // Attach deferreds
+ deferred.promise( jqXHR ).complete = completeDeferred.add;
+ jqXHR.success = jqXHR.done;
+ jqXHR.error = jqXHR.fail;
+
+ // Remove hash character (#7531: and string promotion)
+ // Add protocol if not provided (#5866: IE7 issue with protocol-less urls)
+ // Handle falsy url in the settings object (#10093: consistency with old signature)
+ // We also use the url parameter if available
+ s.url = ( ( url || s.url || ajaxLocation ) + "" ).replace( rhash, "" ).replace( rprotocol, ajaxLocParts[ 1 ] + "//" );
+
+ // Alias method option to type as per ticket #12004
+ s.type = options.method || options.type || s.method || s.type;
+
+ // Extract dataTypes list
+ s.dataTypes = jQuery.trim( s.dataType || "*" ).toLowerCase().match( core_rnotwhite ) || [""];
+
+ // A cross-domain request is in order when we have a protocol:host:port mismatch
+ if ( s.crossDomain == null ) {
+ parts = rurl.exec( s.url.toLowerCase() );
+ s.crossDomain = !!( parts &&
+ ( parts[ 1 ] !== ajaxLocParts[ 1 ] || parts[ 2 ] !== ajaxLocParts[ 2 ] ||
+ ( parts[ 3 ] || ( parts[ 1 ] === "http:" ? 80 : 443 ) ) !=
+ ( ajaxLocParts[ 3 ] || ( ajaxLocParts[ 1 ] === "http:" ? 80 : 443 ) ) )
+ );
+ }
+
+ // Convert data if not already a string
+ if ( s.data && s.processData && typeof s.data !== "string" ) {
+ s.data = jQuery.param( s.data, s.traditional );
+ }
+
+ // Apply prefilters
+ inspectPrefiltersOrTransports( prefilters, s, options, jqXHR );
+
+ // If request was aborted inside a prefilter, stop there
+ if ( state === 2 ) {
+ return jqXHR;
+ }
+
+ // We can fire global events as of now if asked to
+ fireGlobals = s.global;
+
+ // Watch for a new set of requests
+ if ( fireGlobals && jQuery.active++ === 0 ) {
+ jQuery.event.trigger("ajaxStart");
+ }
+
+ // Uppercase the type
+ s.type = s.type.toUpperCase();
+
+ // Determine if request has content
+ s.hasContent = !rnoContent.test( s.type );
+
+ // Save the URL in case we're toying with the If-Modified-Since
+ // and/or If-None-Match header later on
+ cacheURL = s.url;
+
+ // More options handling for requests with no content
+ if ( !s.hasContent ) {
+
+ // If data is available, append data to url
+ if ( s.data ) {
+ cacheURL = ( s.url += ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + s.data );
+ // #9682: remove data so that it's not used in an eventual retry
+ delete s.data;
+ }
+
+ // Add anti-cache in url if needed
+ if ( s.cache === false ) {
+ s.url = rts.test( cacheURL ) ?
+
+ // If there is already a '_' parameter, set its value
+ cacheURL.replace( rts, "$1_=" + ajax_nonce++ ) :
+
+ // Otherwise add one to the end
+ cacheURL + ( ajax_rquery.test( cacheURL ) ? "&" : "?" ) + "_=" + ajax_nonce++;
+ }
+ }
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ if ( jQuery.lastModified[ cacheURL ] ) {
+ jqXHR.setRequestHeader( "If-Modified-Since", jQuery.lastModified[ cacheURL ] );
+ }
+ if ( jQuery.etag[ cacheURL ] ) {
+ jqXHR.setRequestHeader( "If-None-Match", jQuery.etag[ cacheURL ] );
+ }
+ }
+
+ // Set the correct header, if data is being sent
+ if ( s.data && s.hasContent && s.contentType !== false || options.contentType ) {
+ jqXHR.setRequestHeader( "Content-Type", s.contentType );
+ }
+
+ // Set the Accepts header for the server, depending on the dataType
+ jqXHR.setRequestHeader(
+ "Accept",
+ s.dataTypes[ 0 ] && s.accepts[ s.dataTypes[0] ] ?
+ s.accepts[ s.dataTypes[0] ] + ( s.dataTypes[ 0 ] !== "*" ? ", " + allTypes + "; q=0.01" : "" ) :
+ s.accepts[ "*" ]
+ );
+
+ // Check for headers option
+ for ( i in s.headers ) {
+ jqXHR.setRequestHeader( i, s.headers[ i ] );
+ }
+
+ // Allow custom headers/mimetypes and early abort
+ if ( s.beforeSend && ( s.beforeSend.call( callbackContext, jqXHR, s ) === false || state === 2 ) ) {
+ // Abort if not done already and return
+ return jqXHR.abort();
+ }
+
+ // aborting is no longer a cancellation
+ strAbort = "abort";
+
+ // Install callbacks on deferreds
+ for ( i in { success: 1, error: 1, complete: 1 } ) {
+ jqXHR[ i ]( s[ i ] );
+ }
+
+ // Get transport
+ transport = inspectPrefiltersOrTransports( transports, s, options, jqXHR );
+
+ // If no transport, we auto-abort
+ if ( !transport ) {
+ done( -1, "No Transport" );
+ } else {
+ jqXHR.readyState = 1;
+
+ // Send global event
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxSend", [ jqXHR, s ] );
+ }
+ // Timeout
+ if ( s.async && s.timeout > 0 ) {
+ timeoutTimer = setTimeout(function() {
+ jqXHR.abort("timeout");
+ }, s.timeout );
+ }
+
+ try {
+ state = 1;
+ transport.send( requestHeaders, done );
+ } catch ( e ) {
+ // Propagate exception as error if not done
+ if ( state < 2 ) {
+ done( -1, e );
+ // Simply rethrow otherwise
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ // Callback for when everything is done
+ function done( status, nativeStatusText, responses, headers ) {
+ var isSuccess, success, error, response, modified,
+ statusText = nativeStatusText;
+
+ // Called once
+ if ( state === 2 ) {
+ return;
+ }
+
+ // State is "done" now
+ state = 2;
+
+ // Clear timeout if it exists
+ if ( timeoutTimer ) {
+ clearTimeout( timeoutTimer );
+ }
+
+ // Dereference transport for early garbage collection
+ // (no matter how long the jqXHR object will be used)
+ transport = undefined;
+
+ // Cache response headers
+ responseHeadersString = headers || "";
+
+ // Set readyState
+ jqXHR.readyState = status > 0 ? 4 : 0;
+
+ // Get response data
+ if ( responses ) {
+ response = ajaxHandleResponses( s, jqXHR, responses );
+ }
+
+ // If successful, handle type chaining
+ if ( status >= 200 && status < 300 || status === 304 ) {
+
+ // Set the If-Modified-Since and/or If-None-Match header, if in ifModified mode.
+ if ( s.ifModified ) {
+ modified = jqXHR.getResponseHeader("Last-Modified");
+ if ( modified ) {
+ jQuery.lastModified[ cacheURL ] = modified;
+ }
+ modified = jqXHR.getResponseHeader("etag");
+ if ( modified ) {
+ jQuery.etag[ cacheURL ] = modified;
+ }
+ }
+
+ // if no content
+ if ( status === 204 ) {
+ isSuccess = true;
+ statusText = "nocontent";
+
+ // if not modified
+ } else if ( status === 304 ) {
+ isSuccess = true;
+ statusText = "notmodified";
+
+ // If we have data, let's convert it
+ } else {
+ isSuccess = ajaxConvert( s, response );
+ statusText = isSuccess.state;
+ success = isSuccess.data;
+ error = isSuccess.error;
+ isSuccess = !error;
+ }
+ } else {
+ // We extract error from statusText
+ // then normalize statusText and status for non-aborts
+ error = statusText;
+ if ( status || !statusText ) {
+ statusText = "error";
+ if ( status < 0 ) {
+ status = 0;
+ }
+ }
+ }
+
+ // Set data for the fake xhr object
+ jqXHR.status = status;
+ jqXHR.statusText = ( nativeStatusText || statusText ) + "";
+
+ // Success/Error
+ if ( isSuccess ) {
+ deferred.resolveWith( callbackContext, [ success, statusText, jqXHR ] );
+ } else {
+ deferred.rejectWith( callbackContext, [ jqXHR, statusText, error ] );
+ }
+
+ // Status-dependent callbacks
+ jqXHR.statusCode( statusCode );
+ statusCode = undefined;
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( isSuccess ? "ajaxSuccess" : "ajaxError",
+ [ jqXHR, s, isSuccess ? success : error ] );
+ }
+
+ // Complete
+ completeDeferred.fireWith( callbackContext, [ jqXHR, statusText ] );
+
+ if ( fireGlobals ) {
+ globalEventContext.trigger( "ajaxComplete", [ jqXHR, s ] );
+ // Handle the global AJAX counter
+ if ( !( --jQuery.active ) ) {
+ jQuery.event.trigger("ajaxStop");
+ }
+ }
+ }
+
+ return jqXHR;
+ },
+
+ getScript: function( url, callback ) {
+ return jQuery.get( url, undefined, callback, "script" );
+ },
+
+ getJSON: function( url, data, callback ) {
+ return jQuery.get( url, data, callback, "json" );
+ }
+});
+
+/* Handles responses to an ajax request:
+ * - sets all responseXXX fields accordingly
+ * - finds the right dataType (mediates between content-type and expected dataType)
+ * - returns the corresponding response
+ */
+function ajaxHandleResponses( s, jqXHR, responses ) {
+ var firstDataType, ct, finalDataType, type,
+ contents = s.contents,
+ dataTypes = s.dataTypes,
+ responseFields = s.responseFields;
+
+ // Fill responseXXX fields
+ for ( type in responseFields ) {
+ if ( type in responses ) {
+ jqXHR[ responseFields[type] ] = responses[ type ];
+ }
+ }
+
+ // Remove auto dataType and get content-type in the process
+ while( dataTypes[ 0 ] === "*" ) {
+ dataTypes.shift();
+ if ( ct === undefined ) {
+ ct = s.mimeType || jqXHR.getResponseHeader("Content-Type");
+ }
+ }
+
+ // Check if we're dealing with a known content-type
+ if ( ct ) {
+ for ( type in contents ) {
+ if ( contents[ type ] && contents[ type ].test( ct ) ) {
+ dataTypes.unshift( type );
+ break;
+ }
+ }
+ }
+
+ // Check to see if we have a response for the expected dataType
+ if ( dataTypes[ 0 ] in responses ) {
+ finalDataType = dataTypes[ 0 ];
+ } else {
+ // Try convertible dataTypes
+ for ( type in responses ) {
+ if ( !dataTypes[ 0 ] || s.converters[ type + " " + dataTypes[0] ] ) {
+ finalDataType = type;
+ break;
+ }
+ if ( !firstDataType ) {
+ firstDataType = type;
+ }
+ }
+ // Or just use first one
+ finalDataType = finalDataType || firstDataType;
+ }
+
+ // If we found a dataType
+ // We add the dataType to the list if needed
+ // and return the corresponding response
+ if ( finalDataType ) {
+ if ( finalDataType !== dataTypes[ 0 ] ) {
+ dataTypes.unshift( finalDataType );
+ }
+ return responses[ finalDataType ];
+ }
+}
+
+// Chain conversions given the request and the original response
+function ajaxConvert( s, response ) {
+ var conv2, current, conv, tmp,
+ converters = {},
+ i = 0,
+ // Work with a copy of dataTypes in case we need to modify it for conversion
+ dataTypes = s.dataTypes.slice(),
+ prev = dataTypes[ 0 ];
+
+ // Apply the dataFilter if provided
+ if ( s.dataFilter ) {
+ response = s.dataFilter( response, s.dataType );
+ }
+
+ // Create converters map with lowercased keys
+ if ( dataTypes[ 1 ] ) {
+ for ( conv in s.converters ) {
+ converters[ conv.toLowerCase() ] = s.converters[ conv ];
+ }
+ }
+
+ // Convert to each sequential dataType, tolerating list modification
+ for ( ; (current = dataTypes[++i]); ) {
+
+ // There's only work to do if current dataType is non-auto
+ if ( current !== "*" ) {
+
+ // Convert response if prev dataType is non-auto and differs from current
+ if ( prev !== "*" && prev !== current ) {
+
+ // Seek a direct converter
+ conv = converters[ prev + " " + current ] || converters[ "* " + current ];
+
+ // If none found, seek a pair
+ if ( !conv ) {
+ for ( conv2 in converters ) {
+
+ // If conv2 outputs current
+ tmp = conv2.split(" ");
+ if ( tmp[ 1 ] === current ) {
+
+ // If prev can be converted to accepted input
+ conv = converters[ prev + " " + tmp[ 0 ] ] ||
+ converters[ "* " + tmp[ 0 ] ];
+ if ( conv ) {
+ // Condense equivalence converters
+ if ( conv === true ) {
+ conv = converters[ conv2 ];
+
+ // Otherwise, insert the intermediate dataType
+ } else if ( converters[ conv2 ] !== true ) {
+ current = tmp[ 0 ];
+ dataTypes.splice( i--, 0, current );
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Apply converter (if not an equivalence)
+ if ( conv !== true ) {
+
+ // Unless errors are allowed to bubble, catch and return them
+ if ( conv && s["throws"] ) {
+ response = conv( response );
+ } else {
+ try {
+ response = conv( response );
+ } catch ( e ) {
+ return { state: "parsererror", error: conv ? e : "No conversion from " + prev + " to " + current };
+ }
+ }
+ }
+ }
+
+ // Update prev for next iteration
+ prev = current;
+ }
+ }
+
+ return { state: "success", data: response };
+}
+// Install script dataType
+jQuery.ajaxSetup({
+ accepts: {
+ script: "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"
+ },
+ contents: {
+ script: /(?:java|ecma)script/
+ },
+ converters: {
+ "text script": function( text ) {
+ jQuery.globalEval( text );
+ return text;
+ }
+ }
+});
+
+// Handle cache's special case and global
+jQuery.ajaxPrefilter( "script", function( s ) {
+ if ( s.cache === undefined ) {
+ s.cache = false;
+ }
+ if ( s.crossDomain ) {
+ s.type = "GET";
+ s.global = false;
+ }
+});
+
+// Bind script tag hack transport
+jQuery.ajaxTransport( "script", function(s) {
+
+ // This transport only deals with cross domain requests
+ if ( s.crossDomain ) {
+
+ var script,
+ head = document.head || jQuery("head")[0] || document.documentElement;
+
+ return {
+
+ send: function( _, callback ) {
+
+ script = document.createElement("script");
+
+ script.async = true;
+
+ if ( s.scriptCharset ) {
+ script.charset = s.scriptCharset;
+ }
+
+ script.src = s.url;
+
+ // Attach handlers for all browsers
+ script.onload = script.onreadystatechange = function( _, isAbort ) {
+
+ if ( isAbort || !script.readyState || /loaded|complete/.test( script.readyState ) ) {
+
+ // Handle memory leak in IE
+ script.onload = script.onreadystatechange = null;
+
+ // Remove the script
+ if ( script.parentNode ) {
+ script.parentNode.removeChild( script );
+ }
+
+ // Dereference the script
+ script = null;
+
+ // Callback if not abort
+ if ( !isAbort ) {
+ callback( 200, "success" );
+ }
+ }
+ };
+
+ // Circumvent IE6 bugs with base elements (#2709 and #4378) by prepending
+ // Use native DOM manipulation to avoid our domManip AJAX trickery
+ head.insertBefore( script, head.firstChild );
+ },
+
+ abort: function() {
+ if ( script ) {
+ script.onload( undefined, true );
+ }
+ }
+ };
+ }
+});
+var oldCallbacks = [],
+ rjsonp = /(=)\?(?=&|$)|\?\?/;
+
+// Default jsonp settings
+jQuery.ajaxSetup({
+ jsonp: "callback",
+ jsonpCallback: function() {
+ var callback = oldCallbacks.pop() || ( jQuery.expando + "_" + ( ajax_nonce++ ) );
+ this[ callback ] = true;
+ return callback;
+ }
+});
+
+// Detect, normalize options and install callbacks for jsonp requests
+jQuery.ajaxPrefilter( "json jsonp", function( s, originalSettings, jqXHR ) {
+
+ var callbackName, overwritten, responseContainer,
+ jsonProp = s.jsonp !== false && ( rjsonp.test( s.url ) ?
+ "url" :
+ typeof s.data === "string" && !( s.contentType || "" ).indexOf("application/x-www-form-urlencoded") && rjsonp.test( s.data ) && "data"
+ );
+
+ // Handle iff the expected data type is "jsonp" or we have a parameter to set
+ if ( jsonProp || s.dataTypes[ 0 ] === "jsonp" ) {
+
+ // Get callback name, remembering preexisting value associated with it
+ callbackName = s.jsonpCallback = jQuery.isFunction( s.jsonpCallback ) ?
+ s.jsonpCallback() :
+ s.jsonpCallback;
+
+ // Insert callback into url or form data
+ if ( jsonProp ) {
+ s[ jsonProp ] = s[ jsonProp ].replace( rjsonp, "$1" + callbackName );
+ } else if ( s.jsonp !== false ) {
+ s.url += ( ajax_rquery.test( s.url ) ? "&" : "?" ) + s.jsonp + "=" + callbackName;
+ }
+
+ // Use data converter to retrieve json after script execution
+ s.converters["script json"] = function() {
+ if ( !responseContainer ) {
+ jQuery.error( callbackName + " was not called" );
+ }
+ return responseContainer[ 0 ];
+ };
+
+ // force json dataType
+ s.dataTypes[ 0 ] = "json";
+
+ // Install callback
+ overwritten = window[ callbackName ];
+ window[ callbackName ] = function() {
+ responseContainer = arguments;
+ };
+
+ // Clean-up function (fires after converters)
+ jqXHR.always(function() {
+ // Restore preexisting value
+ window[ callbackName ] = overwritten;
+
+ // Save back as free
+ if ( s[ callbackName ] ) {
+ // make sure that re-using the options doesn't screw things around
+ s.jsonpCallback = originalSettings.jsonpCallback;
+
+ // save the callback name for future use
+ oldCallbacks.push( callbackName );
+ }
+
+ // Call if it was a function and we have a response
+ if ( responseContainer && jQuery.isFunction( overwritten ) ) {
+ overwritten( responseContainer[ 0 ] );
+ }
+
+ responseContainer = overwritten = undefined;
+ });
+
+ // Delegate to script
+ return "script";
+ }
+});
+var xhrCallbacks, xhrSupported,
+ xhrId = 0,
+ // #5280: Internet Explorer will keep connections alive if we don't abort on unload
+ xhrOnUnloadAbort = window.ActiveXObject && function() {
+ // Abort all pending requests
+ var key;
+ for ( key in xhrCallbacks ) {
+ xhrCallbacks[ key ]( undefined, true );
+ }
+ };
+
+// Functions to create xhrs
+function createStandardXHR() {
+ try {
+ return new window.XMLHttpRequest();
+ } catch( e ) {}
+}
+
+function createActiveXHR() {
+ try {
+ return new window.ActiveXObject("Microsoft.XMLHTTP");
+ } catch( e ) {}
+}
+
+// Create the request object
+// (This is still attached to ajaxSettings for backward compatibility)
+jQuery.ajaxSettings.xhr = window.ActiveXObject ?
+ /* Microsoft failed to properly
+ * implement the XMLHttpRequest in IE7 (can't request local files),
+ * so we use the ActiveXObject when it is available
+ * Additionally XMLHttpRequest can be disabled in IE7/IE8 so
+ * we need a fallback.
+ */
+ function() {
+ return !this.isLocal && createStandardXHR() || createActiveXHR();
+ } :
+ // For all other browsers, use the standard XMLHttpRequest object
+ createStandardXHR;
+
+// Determine support properties
+xhrSupported = jQuery.ajaxSettings.xhr();
+jQuery.support.cors = !!xhrSupported && ( "withCredentials" in xhrSupported );
+xhrSupported = jQuery.support.ajax = !!xhrSupported;
+
+// Create transport if the browser can provide an xhr
+if ( xhrSupported ) {
+
+ jQuery.ajaxTransport(function( s ) {
+ // Cross domain only allowed if supported through XMLHttpRequest
+ if ( !s.crossDomain || jQuery.support.cors ) {
+
+ var callback;
+
+ return {
+ send: function( headers, complete ) {
+
+ // Get a new xhr
+ var handle, i,
+ xhr = s.xhr();
+
+ // Open the socket
+ // Passing null username, generates a login popup on Opera (#2865)
+ if ( s.username ) {
+ xhr.open( s.type, s.url, s.async, s.username, s.password );
+ } else {
+ xhr.open( s.type, s.url, s.async );
+ }
+
+ // Apply custom fields if provided
+ if ( s.xhrFields ) {
+ for ( i in s.xhrFields ) {
+ xhr[ i ] = s.xhrFields[ i ];
+ }
+ }
+
+ // Override mime type if needed
+ if ( s.mimeType && xhr.overrideMimeType ) {
+ xhr.overrideMimeType( s.mimeType );
+ }
+
+ // X-Requested-With header
+ // For cross-domain requests, seeing as conditions for a preflight are
+ // akin to a jigsaw puzzle, we simply never set it to be sure.
+ // (it can always be set on a per-request basis or even using ajaxSetup)
+ // For same-domain requests, won't change header if already provided.
+ if ( !s.crossDomain && !headers["X-Requested-With"] ) {
+ headers["X-Requested-With"] = "XMLHttpRequest";
+ }
+
+ // Need an extra try/catch for cross domain requests in Firefox 3
+ try {
+ for ( i in headers ) {
+ xhr.setRequestHeader( i, headers[ i ] );
+ }
+ } catch( err ) {}
+
+ // Do send the request
+ // This may raise an exception which is actually
+ // handled in jQuery.ajax (so no try/catch here)
+ xhr.send( ( s.hasContent && s.data ) || null );
+
+ // Listener
+ callback = function( _, isAbort ) {
+ var status, responseHeaders, statusText, responses;
+
+ // Firefox throws exceptions when accessing properties
+ // of an xhr when a network error occurred
+ // http://helpful.knobs-dials.com/index.php/Component_returned_failure_code:_0x80040111_(NS_ERROR_NOT_AVAILABLE)
+ try {
+
+ // Was never called and is aborted or complete
+ if ( callback && ( isAbort || xhr.readyState === 4 ) ) {
+
+ // Only called once
+ callback = undefined;
+
+ // Do not keep as active anymore
+ if ( handle ) {
+ xhr.onreadystatechange = jQuery.noop;
+ if ( xhrOnUnloadAbort ) {
+ delete xhrCallbacks[ handle ];
+ }
+ }
+
+ // If it's an abort
+ if ( isAbort ) {
+ // Abort it manually if needed
+ if ( xhr.readyState !== 4 ) {
+ xhr.abort();
+ }
+ } else {
+ responses = {};
+ status = xhr.status;
+ responseHeaders = xhr.getAllResponseHeaders();
+
+ // When requesting binary data, IE6-9 will throw an exception
+ // on any attempt to access responseText (#11426)
+ if ( typeof xhr.responseText === "string" ) {
+ responses.text = xhr.responseText;
+ }
+
+ // Firefox throws an exception when accessing
+ // statusText for faulty cross-domain requests
+ try {
+ statusText = xhr.statusText;
+ } catch( e ) {
+ // We normalize with Webkit giving an empty statusText
+ statusText = "";
+ }
+
+ // Filter status for non standard behaviors
+
+ // If the request is local and we have data: assume a success
+ // (success with no data won't get notified, that's the best we
+ // can do given current implementations)
+ if ( !status && s.isLocal && !s.crossDomain ) {
+ status = responses.text ? 200 : 404;
+ // IE - #1450: sometimes returns 1223 when it should be 204
+ } else if ( status === 1223 ) {
+ status = 204;
+ }
+ }
+ }
+ } catch( firefoxAccessException ) {
+ if ( !isAbort ) {
+ complete( -1, firefoxAccessException );
+ }
+ }
+
+ // Call complete if needed
+ if ( responses ) {
+ complete( status, statusText, responses, responseHeaders );
+ }
+ };
+
+ if ( !s.async ) {
+ // if we're in sync mode we fire the callback
+ callback();
+ } else if ( xhr.readyState === 4 ) {
+ // (IE6 & IE7) if it's in cache and has been
+ // retrieved directly we need to fire the callback
+ setTimeout( callback );
+ } else {
+ handle = ++xhrId;
+ if ( xhrOnUnloadAbort ) {
+ // Create the active xhrs callbacks list if needed
+ // and attach the unload handler
+ if ( !xhrCallbacks ) {
+ xhrCallbacks = {};
+ jQuery( window ).unload( xhrOnUnloadAbort );
+ }
+ // Add to list of active xhrs callbacks
+ xhrCallbacks[ handle ] = callback;
+ }
+ xhr.onreadystatechange = callback;
+ }
+ },
+
+ abort: function() {
+ if ( callback ) {
+ callback( undefined, true );
+ }
+ }
+ };
+ }
+ });
+}
+var fxNow, timerId,
+ rfxtypes = /^(?:toggle|show|hide)$/,
+ rfxnum = new RegExp( "^(?:([+-])=|)(" + core_pnum + ")([a-z%]*)$", "i" ),
+ rrun = /queueHooks$/,
+ animationPrefilters = [ defaultPrefilter ],
+ tweeners = {
+ "*": [function( prop, value ) {
+ var end, unit,
+ tween = this.createTween( prop, value ),
+ parts = rfxnum.exec( value ),
+ target = tween.cur(),
+ start = +target || 0,
+ scale = 1,
+ maxIterations = 20;
+
+ if ( parts ) {
+ end = +parts[2];
+ unit = parts[3] || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+
+ // We need to compute starting value
+ if ( unit !== "px" && start ) {
+ // Iteratively approximate from a nonzero starting point
+ // Prefer the current property, because this process will be trivial if it uses the same units
+ // Fallback to end or a simple constant
+ start = jQuery.css( tween.elem, prop, true ) || end || 1;
+
+ do {
+ // If previous iteration zeroed out, double until we get *something*
+ // Use a string for doubling factor so we don't accidentally see scale as unchanged below
+ scale = scale || ".5";
+
+ // Adjust and apply
+ start = start / scale;
+ jQuery.style( tween.elem, prop, start + unit );
+
+ // Update scale, tolerating zero or NaN from tween.cur()
+ // And breaking the loop if scale is unchanged or perfect, or if we've just had enough
+ } while ( scale !== (scale = tween.cur() / target) && scale !== 1 && --maxIterations );
+ }
+
+ tween.unit = unit;
+ tween.start = start;
+ // If a +=/-= token was provided, we're doing a relative animation
+ tween.end = parts[1] ? start + ( parts[1] + 1 ) * end : end;
+ }
+ return tween;
+ }]
+ };
+
+// Animations created synchronously will run synchronously
+function createFxNow() {
+ setTimeout(function() {
+ fxNow = undefined;
+ });
+ return ( fxNow = jQuery.now() );
+}
+
+function createTweens( animation, props ) {
+ jQuery.each( props, function( prop, value ) {
+ var collection = ( tweeners[ prop ] || [] ).concat( tweeners[ "*" ] ),
+ index = 0,
+ length = collection.length;
+ for ( ; index < length; index++ ) {
+ if ( collection[ index ].call( animation, prop, value ) ) {
+
+ // we're done with this property
+ return;
+ }
+ }
+ });
+}
+
+function Animation( elem, properties, options ) {
+ var result,
+ stopped,
+ index = 0,
+ length = animationPrefilters.length,
+ deferred = jQuery.Deferred().always( function() {
+ // don't match elem in the :animated selector
+ delete tick.elem;
+ }),
+ tick = function() {
+ if ( stopped ) {
+ return false;
+ }
+ var currentTime = fxNow || createFxNow(),
+ remaining = Math.max( 0, animation.startTime + animation.duration - currentTime ),
+ // archaic crash bug won't allow us to use 1 - ( 0.5 || 0 ) (#12497)
+ temp = remaining / animation.duration || 0,
+ percent = 1 - temp,
+ index = 0,
+ length = animation.tweens.length;
+
+ for ( ; index < length ; index++ ) {
+ animation.tweens[ index ].run( percent );
+ }
+
+ deferred.notifyWith( elem, [ animation, percent, remaining ]);
+
+ if ( percent < 1 && length ) {
+ return remaining;
+ } else {
+ deferred.resolveWith( elem, [ animation ] );
+ return false;
+ }
+ },
+ animation = deferred.promise({
+ elem: elem,
+ props: jQuery.extend( {}, properties ),
+ opts: jQuery.extend( true, { specialEasing: {} }, options ),
+ originalProperties: properties,
+ originalOptions: options,
+ startTime: fxNow || createFxNow(),
+ duration: options.duration,
+ tweens: [],
+ createTween: function( prop, end ) {
+ var tween = jQuery.Tween( elem, animation.opts, prop, end,
+ animation.opts.specialEasing[ prop ] || animation.opts.easing );
+ animation.tweens.push( tween );
+ return tween;
+ },
+ stop: function( gotoEnd ) {
+ var index = 0,
+ // if we are going to the end, we want to run all the tweens
+ // otherwise we skip this part
+ length = gotoEnd ? animation.tweens.length : 0;
+ if ( stopped ) {
+ return this;
+ }
+ stopped = true;
+ for ( ; index < length ; index++ ) {
+ animation.tweens[ index ].run( 1 );
+ }
+
+ // resolve when we played the last frame
+ // otherwise, reject
+ if ( gotoEnd ) {
+ deferred.resolveWith( elem, [ animation, gotoEnd ] );
+ } else {
+ deferred.rejectWith( elem, [ animation, gotoEnd ] );
+ }
+ return this;
+ }
+ }),
+ props = animation.props;
+
+ propFilter( props, animation.opts.specialEasing );
+
+ for ( ; index < length ; index++ ) {
+ result = animationPrefilters[ index ].call( animation, elem, props, animation.opts );
+ if ( result ) {
+ return result;
+ }
+ }
+
+ createTweens( animation, props );
+
+ if ( jQuery.isFunction( animation.opts.start ) ) {
+ animation.opts.start.call( elem, animation );
+ }
+
+ jQuery.fx.timer(
+ jQuery.extend( tick, {
+ elem: elem,
+ anim: animation,
+ queue: animation.opts.queue
+ })
+ );
+
+ // attach callbacks from options
+ return animation.progress( animation.opts.progress )
+ .done( animation.opts.done, animation.opts.complete )
+ .fail( animation.opts.fail )
+ .always( animation.opts.always );
+}
+
+function propFilter( props, specialEasing ) {
+ var value, name, index, easing, hooks;
+
+ // camelCase, specialEasing and expand cssHook pass
+ for ( index in props ) {
+ name = jQuery.camelCase( index );
+ easing = specialEasing[ name ];
+ value = props[ index ];
+ if ( jQuery.isArray( value ) ) {
+ easing = value[ 1 ];
+ value = props[ index ] = value[ 0 ];
+ }
+
+ if ( index !== name ) {
+ props[ name ] = value;
+ delete props[ index ];
+ }
+
+ hooks = jQuery.cssHooks[ name ];
+ if ( hooks && "expand" in hooks ) {
+ value = hooks.expand( value );
+ delete props[ name ];
+
+ // not quite $.extend, this wont overwrite keys already present.
+ // also - reusing 'index' from above because we have the correct "name"
+ for ( index in value ) {
+ if ( !( index in props ) ) {
+ props[ index ] = value[ index ];
+ specialEasing[ index ] = easing;
+ }
+ }
+ } else {
+ specialEasing[ name ] = easing;
+ }
+ }
+}
+
+jQuery.Animation = jQuery.extend( Animation, {
+
+ tweener: function( props, callback ) {
+ if ( jQuery.isFunction( props ) ) {
+ callback = props;
+ props = [ "*" ];
+ } else {
+ props = props.split(" ");
+ }
+
+ var prop,
+ index = 0,
+ length = props.length;
+
+ for ( ; index < length ; index++ ) {
+ prop = props[ index ];
+ tweeners[ prop ] = tweeners[ prop ] || [];
+ tweeners[ prop ].unshift( callback );
+ }
+ },
+
+ prefilter: function( callback, prepend ) {
+ if ( prepend ) {
+ animationPrefilters.unshift( callback );
+ } else {
+ animationPrefilters.push( callback );
+ }
+ }
+});
+
+function defaultPrefilter( elem, props, opts ) {
+ /*jshint validthis:true */
+ var prop, index, length,
+ value, dataShow, toggle,
+ tween, hooks, oldfire,
+ anim = this,
+ style = elem.style,
+ orig = {},
+ handled = [],
+ hidden = elem.nodeType && isHidden( elem );
+
+ // handle queue: false promises
+ if ( !opts.queue ) {
+ hooks = jQuery._queueHooks( elem, "fx" );
+ if ( hooks.unqueued == null ) {
+ hooks.unqueued = 0;
+ oldfire = hooks.empty.fire;
+ hooks.empty.fire = function() {
+ if ( !hooks.unqueued ) {
+ oldfire();
+ }
+ };
+ }
+ hooks.unqueued++;
+
+ anim.always(function() {
+ // doing this makes sure that the complete handler will be called
+ // before this completes
+ anim.always(function() {
+ hooks.unqueued--;
+ if ( !jQuery.queue( elem, "fx" ).length ) {
+ hooks.empty.fire();
+ }
+ });
+ });
+ }
+
+ // height/width overflow pass
+ if ( elem.nodeType === 1 && ( "height" in props || "width" in props ) ) {
+ // Make sure that nothing sneaks out
+ // Record all 3 overflow attributes because IE does not
+ // change the overflow attribute when overflowX and
+ // overflowY are set to the same value
+ opts.overflow = [ style.overflow, style.overflowX, style.overflowY ];
+
+ // Set display property to inline-block for height/width
+ // animations on inline elements that are having width/height animated
+ if ( jQuery.css( elem, "display" ) === "inline" &&
+ jQuery.css( elem, "float" ) === "none" ) {
+
+ // inline-level elements accept inline-block;
+ // block-level elements need to be inline with layout
+ if ( !jQuery.support.inlineBlockNeedsLayout || css_defaultDisplay( elem.nodeName ) === "inline" ) {
+ style.display = "inline-block";
+
+ } else {
+ style.zoom = 1;
+ }
+ }
+ }
+
+ if ( opts.overflow ) {
+ style.overflow = "hidden";
+ if ( !jQuery.support.shrinkWrapBlocks ) {
+ anim.always(function() {
+ style.overflow = opts.overflow[ 0 ];
+ style.overflowX = opts.overflow[ 1 ];
+ style.overflowY = opts.overflow[ 2 ];
+ });
+ }
+ }
+
+
+ // show/hide pass
+ for ( index in props ) {
+ value = props[ index ];
+ if ( rfxtypes.exec( value ) ) {
+ delete props[ index ];
+ toggle = toggle || value === "toggle";
+ if ( value === ( hidden ? "hide" : "show" ) ) {
+ continue;
+ }
+ handled.push( index );
+ }
+ }
+
+ length = handled.length;
+ if ( length ) {
+ dataShow = jQuery._data( elem, "fxshow" ) || jQuery._data( elem, "fxshow", {} );
+ if ( "hidden" in dataShow ) {
+ hidden = dataShow.hidden;
+ }
+
+ // store state if its toggle - enables .stop().toggle() to "reverse"
+ if ( toggle ) {
+ dataShow.hidden = !hidden;
+ }
+ if ( hidden ) {
+ jQuery( elem ).show();
+ } else {
+ anim.done(function() {
+ jQuery( elem ).hide();
+ });
+ }
+ anim.done(function() {
+ var prop;
+ jQuery._removeData( elem, "fxshow" );
+ for ( prop in orig ) {
+ jQuery.style( elem, prop, orig[ prop ] );
+ }
+ });
+ for ( index = 0 ; index < length ; index++ ) {
+ prop = handled[ index ];
+ tween = anim.createTween( prop, hidden ? dataShow[ prop ] : 0 );
+ orig[ prop ] = dataShow[ prop ] || jQuery.style( elem, prop );
+
+ if ( !( prop in dataShow ) ) {
+ dataShow[ prop ] = tween.start;
+ if ( hidden ) {
+ tween.end = tween.start;
+ tween.start = prop === "width" || prop === "height" ? 1 : 0;
+ }
+ }
+ }
+ }
+}
+
+function Tween( elem, options, prop, end, easing ) {
+ return new Tween.prototype.init( elem, options, prop, end, easing );
+}
+jQuery.Tween = Tween;
+
+Tween.prototype = {
+ constructor: Tween,
+ init: function( elem, options, prop, end, easing, unit ) {
+ this.elem = elem;
+ this.prop = prop;
+ this.easing = easing || "swing";
+ this.options = options;
+ this.start = this.now = this.cur();
+ this.end = end;
+ this.unit = unit || ( jQuery.cssNumber[ prop ] ? "" : "px" );
+ },
+ cur: function() {
+ var hooks = Tween.propHooks[ this.prop ];
+
+ return hooks && hooks.get ?
+ hooks.get( this ) :
+ Tween.propHooks._default.get( this );
+ },
+ run: function( percent ) {
+ var eased,
+ hooks = Tween.propHooks[ this.prop ];
+
+ if ( this.options.duration ) {
+ this.pos = eased = jQuery.easing[ this.easing ](
+ percent, this.options.duration * percent, 0, 1, this.options.duration
+ );
+ } else {
+ this.pos = eased = percent;
+ }
+ this.now = ( this.end - this.start ) * eased + this.start;
+
+ if ( this.options.step ) {
+ this.options.step.call( this.elem, this.now, this );
+ }
+
+ if ( hooks && hooks.set ) {
+ hooks.set( this );
+ } else {
+ Tween.propHooks._default.set( this );
+ }
+ return this;
+ }
+};
+
+Tween.prototype.init.prototype = Tween.prototype;
+
+Tween.propHooks = {
+ _default: {
+ get: function( tween ) {
+ var result;
+
+ if ( tween.elem[ tween.prop ] != null &&
+ (!tween.elem.style || tween.elem.style[ tween.prop ] == null) ) {
+ return tween.elem[ tween.prop ];
+ }
+
+ // passing an empty string as a 3rd parameter to .css will automatically
+ // attempt a parseFloat and fallback to a string if the parse fails
+ // so, simple values such as "10px" are parsed to Float.
+ // complex values such as "rotate(1rad)" are returned as is.
+ result = jQuery.css( tween.elem, tween.prop, "" );
+ // Empty strings, null, undefined and "auto" are converted to 0.
+ return !result || result === "auto" ? 0 : result;
+ },
+ set: function( tween ) {
+ // use step hook for back compat - use cssHook if its there - use .style if its
+ // available and use plain properties where available
+ if ( jQuery.fx.step[ tween.prop ] ) {
+ jQuery.fx.step[ tween.prop ]( tween );
+ } else if ( tween.elem.style && ( tween.elem.style[ jQuery.cssProps[ tween.prop ] ] != null || jQuery.cssHooks[ tween.prop ] ) ) {
+ jQuery.style( tween.elem, tween.prop, tween.now + tween.unit );
+ } else {
+ tween.elem[ tween.prop ] = tween.now;
+ }
+ }
+ }
+};
+
+// Remove in 2.0 - this supports IE8's panic based approach
+// to setting things on disconnected nodes
+
+Tween.propHooks.scrollTop = Tween.propHooks.scrollLeft = {
+ set: function( tween ) {
+ if ( tween.elem.nodeType && tween.elem.parentNode ) {
+ tween.elem[ tween.prop ] = tween.now;
+ }
+ }
+};
+
+jQuery.each([ "toggle", "show", "hide" ], function( i, name ) {
+ var cssFn = jQuery.fn[ name ];
+ jQuery.fn[ name ] = function( speed, easing, callback ) {
+ return speed == null || typeof speed === "boolean" ?
+ cssFn.apply( this, arguments ) :
+ this.animate( genFx( name, true ), speed, easing, callback );
+ };
+});
+
+jQuery.fn.extend({
+ fadeTo: function( speed, to, easing, callback ) {
+
+ // show any hidden elements after setting opacity to 0
+ return this.filter( isHidden ).css( "opacity", 0 ).show()
+
+ // animate to the value specified
+ .end().animate({ opacity: to }, speed, easing, callback );
+ },
+ animate: function( prop, speed, easing, callback ) {
+ var empty = jQuery.isEmptyObject( prop ),
+ optall = jQuery.speed( speed, easing, callback ),
+ doAnimation = function() {
+ // Operate on a copy of prop so per-property easing won't be lost
+ var anim = Animation( this, jQuery.extend( {}, prop ), optall );
+ doAnimation.finish = function() {
+ anim.stop( true );
+ };
+ // Empty animations, or finishing resolves immediately
+ if ( empty || jQuery._data( this, "finish" ) ) {
+ anim.stop( true );
+ }
+ };
+ doAnimation.finish = doAnimation;
+
+ return empty || optall.queue === false ?
+ this.each( doAnimation ) :
+ this.queue( optall.queue, doAnimation );
+ },
+ stop: function( type, clearQueue, gotoEnd ) {
+ var stopQueue = function( hooks ) {
+ var stop = hooks.stop;
+ delete hooks.stop;
+ stop( gotoEnd );
+ };
+
+ if ( typeof type !== "string" ) {
+ gotoEnd = clearQueue;
+ clearQueue = type;
+ type = undefined;
+ }
+ if ( clearQueue && type !== false ) {
+ this.queue( type || "fx", [] );
+ }
+
+ return this.each(function() {
+ var dequeue = true,
+ index = type != null && type + "queueHooks",
+ timers = jQuery.timers,
+ data = jQuery._data( this );
+
+ if ( index ) {
+ if ( data[ index ] && data[ index ].stop ) {
+ stopQueue( data[ index ] );
+ }
+ } else {
+ for ( index in data ) {
+ if ( data[ index ] && data[ index ].stop && rrun.test( index ) ) {
+ stopQueue( data[ index ] );
+ }
+ }
+ }
+
+ for ( index = timers.length; index--; ) {
+ if ( timers[ index ].elem === this && (type == null || timers[ index ].queue === type) ) {
+ timers[ index ].anim.stop( gotoEnd );
+ dequeue = false;
+ timers.splice( index, 1 );
+ }
+ }
+
+ // start the next in the queue if the last step wasn't forced
+ // timers currently will call their complete callbacks, which will dequeue
+ // but only if they were gotoEnd
+ if ( dequeue || !gotoEnd ) {
+ jQuery.dequeue( this, type );
+ }
+ });
+ },
+ finish: function( type ) {
+ if ( type !== false ) {
+ type = type || "fx";
+ }
+ return this.each(function() {
+ var index,
+ data = jQuery._data( this ),
+ queue = data[ type + "queue" ],
+ hooks = data[ type + "queueHooks" ],
+ timers = jQuery.timers,
+ length = queue ? queue.length : 0;
+
+ // enable finishing flag on private data
+ data.finish = true;
+
+ // empty the queue first
+ jQuery.queue( this, type, [] );
+
+ if ( hooks && hooks.cur && hooks.cur.finish ) {
+ hooks.cur.finish.call( this );
+ }
+
+ // look for any active animations, and finish them
+ for ( index = timers.length; index--; ) {
+ if ( timers[ index ].elem === this && timers[ index ].queue === type ) {
+ timers[ index ].anim.stop( true );
+ timers.splice( index, 1 );
+ }
+ }
+
+ // look for any animations in the old queue and finish them
+ for ( index = 0; index < length; index++ ) {
+ if ( queue[ index ] && queue[ index ].finish ) {
+ queue[ index ].finish.call( this );
+ }
+ }
+
+ // turn off finishing flag
+ delete data.finish;
+ });
+ }
+});
+
+// Generate parameters to create a standard animation
+function genFx( type, includeWidth ) {
+ var which,
+ attrs = { height: type },
+ i = 0;
+
+ // if we include width, step value is 1 to do all cssExpand values,
+ // if we don't include width, step value is 2 to skip over Left and Right
+ includeWidth = includeWidth? 1 : 0;
+ for( ; i < 4 ; i += 2 - includeWidth ) {
+ which = cssExpand[ i ];
+ attrs[ "margin" + which ] = attrs[ "padding" + which ] = type;
+ }
+
+ if ( includeWidth ) {
+ attrs.opacity = attrs.width = type;
+ }
+
+ return attrs;
+}
+
+// Generate shortcuts for custom animations
+jQuery.each({
+ slideDown: genFx("show"),
+ slideUp: genFx("hide"),
+ slideToggle: genFx("toggle"),
+ fadeIn: { opacity: "show" },
+ fadeOut: { opacity: "hide" },
+ fadeToggle: { opacity: "toggle" }
+}, function( name, props ) {
+ jQuery.fn[ name ] = function( speed, easing, callback ) {
+ return this.animate( props, speed, easing, callback );
+ };
+});
+
+jQuery.speed = function( speed, easing, fn ) {
+ var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
+ complete: fn || !fn && easing ||
+ jQuery.isFunction( speed ) && speed,
+ duration: speed,
+ easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
+ };
+
+ opt.duration = jQuery.fx.off ? 0 : typeof opt.duration === "number" ? opt.duration :
+ opt.duration in jQuery.fx.speeds ? jQuery.fx.speeds[ opt.duration ] : jQuery.fx.speeds._default;
+
+ // normalize opt.queue - true/undefined/null -> "fx"
+ if ( opt.queue == null || opt.queue === true ) {
+ opt.queue = "fx";
+ }
+
+ // Queueing
+ opt.old = opt.complete;
+
+ opt.complete = function() {
+ if ( jQuery.isFunction( opt.old ) ) {
+ opt.old.call( this );
+ }
+
+ if ( opt.queue ) {
+ jQuery.dequeue( this, opt.queue );
+ }
+ };
+
+ return opt;
+};
+
+jQuery.easing = {
+ linear: function( p ) {
+ return p;
+ },
+ swing: function( p ) {
+ return 0.5 - Math.cos( p*Math.PI ) / 2;
+ }
+};
+
+jQuery.timers = [];
+jQuery.fx = Tween.prototype.init;
+jQuery.fx.tick = function() {
+ var timer,
+ timers = jQuery.timers,
+ i = 0;
+
+ fxNow = jQuery.now();
+
+ for ( ; i < timers.length; i++ ) {
+ timer = timers[ i ];
+ // Checks the timer has not already been removed
+ if ( !timer() && timers[ i ] === timer ) {
+ timers.splice( i--, 1 );
+ }
+ }
+
+ if ( !timers.length ) {
+ jQuery.fx.stop();
+ }
+ fxNow = undefined;
+};
+
+jQuery.fx.timer = function( timer ) {
+ if ( timer() && jQuery.timers.push( timer ) ) {
+ jQuery.fx.start();
+ }
+};
+
+jQuery.fx.interval = 13;
+
+jQuery.fx.start = function() {
+ if ( !timerId ) {
+ timerId = setInterval( jQuery.fx.tick, jQuery.fx.interval );
+ }
+};
+
+jQuery.fx.stop = function() {
+ clearInterval( timerId );
+ timerId = null;
+};
+
+jQuery.fx.speeds = {
+ slow: 600,
+ fast: 200,
+ // Default speed
+ _default: 400
+};
+
+// Back Compat <1.8 extension point
+jQuery.fx.step = {};
+
+if ( jQuery.expr && jQuery.expr.filters ) {
+ jQuery.expr.filters.animated = function( elem ) {
+ return jQuery.grep(jQuery.timers, function( fn ) {
+ return elem === fn.elem;
+ }).length;
+ };
+}
+jQuery.fn.offset = function( options ) {
+ if ( arguments.length ) {
+ return options === undefined ?
+ this :
+ this.each(function( i ) {
+ jQuery.offset.setOffset( this, options, i );
+ });
+ }
+
+ var docElem, win,
+ box = { top: 0, left: 0 },
+ elem = this[ 0 ],
+ doc = elem && elem.ownerDocument;
+
+ if ( !doc ) {
+ return;
+ }
+
+ docElem = doc.documentElement;
+
+ // Make sure it's not a disconnected DOM node
+ if ( !jQuery.contains( docElem, elem ) ) {
+ return box;
+ }
+
+ // If we don't have gBCR, just use 0,0 rather than error
+ // BlackBerry 5, iOS 3 (original iPhone)
+ if ( typeof elem.getBoundingClientRect !== core_strundefined ) {
+ box = elem.getBoundingClientRect();
+ }
+ win = getWindow( doc );
+ return {
+ top: box.top + ( win.pageYOffset || docElem.scrollTop ) - ( docElem.clientTop || 0 ),
+ left: box.left + ( win.pageXOffset || docElem.scrollLeft ) - ( docElem.clientLeft || 0 )
+ };
+};
+
+jQuery.offset = {
+
+ setOffset: function( elem, options, i ) {
+ var position = jQuery.css( elem, "position" );
+
+ // set position first, in-case top/left are set even on static elem
+ if ( position === "static" ) {
+ elem.style.position = "relative";
+ }
+
+ var curElem = jQuery( elem ),
+ curOffset = curElem.offset(),
+ curCSSTop = jQuery.css( elem, "top" ),
+ curCSSLeft = jQuery.css( elem, "left" ),
+ calculatePosition = ( position === "absolute" || position === "fixed" ) && jQuery.inArray("auto", [curCSSTop, curCSSLeft]) > -1,
+ props = {}, curPosition = {}, curTop, curLeft;
+
+ // need to be able to calculate position if either top or left is auto and position is either absolute or fixed
+ if ( calculatePosition ) {
+ curPosition = curElem.position();
+ curTop = curPosition.top;
+ curLeft = curPosition.left;
+ } else {
+ curTop = parseFloat( curCSSTop ) || 0;
+ curLeft = parseFloat( curCSSLeft ) || 0;
+ }
+
+ if ( jQuery.isFunction( options ) ) {
+ options = options.call( elem, i, curOffset );
+ }
+
+ if ( options.top != null ) {
+ props.top = ( options.top - curOffset.top ) + curTop;
+ }
+ if ( options.left != null ) {
+ props.left = ( options.left - curOffset.left ) + curLeft;
+ }
+
+ if ( "using" in options ) {
+ options.using.call( elem, props );
+ } else {
+ curElem.css( props );
+ }
+ }
+};
+
+
+jQuery.fn.extend({
+
+ position: function() {
+ if ( !this[ 0 ] ) {
+ return;
+ }
+
+ var offsetParent, offset,
+ parentOffset = { top: 0, left: 0 },
+ elem = this[ 0 ];
+
+ // fixed elements are offset from window (parentOffset = {top:0, left: 0}, because it is it's only offset parent
+ if ( jQuery.css( elem, "position" ) === "fixed" ) {
+ // we assume that getBoundingClientRect is available when computed position is fixed
+ offset = elem.getBoundingClientRect();
+ } else {
+ // Get *real* offsetParent
+ offsetParent = this.offsetParent();
+
+ // Get correct offsets
+ offset = this.offset();
+ if ( !jQuery.nodeName( offsetParent[ 0 ], "html" ) ) {
+ parentOffset = offsetParent.offset();
+ }
+
+ // Add offsetParent borders
+ parentOffset.top += jQuery.css( offsetParent[ 0 ], "borderTopWidth", true );
+ parentOffset.left += jQuery.css( offsetParent[ 0 ], "borderLeftWidth", true );
+ }
+
+ // Subtract parent offsets and element margins
+ // note: when an element has margin: auto the offsetLeft and marginLeft
+ // are the same in Safari causing offset.left to incorrectly be 0
+ return {
+ top: offset.top - parentOffset.top - jQuery.css( elem, "marginTop", true ),
+ left: offset.left - parentOffset.left - jQuery.css( elem, "marginLeft", true)
+ };
+ },
+
+ offsetParent: function() {
+ return this.map(function() {
+ var offsetParent = this.offsetParent || document.documentElement;
+ while ( offsetParent && ( !jQuery.nodeName( offsetParent, "html" ) && jQuery.css( offsetParent, "position") === "static" ) ) {
+ offsetParent = offsetParent.offsetParent;
+ }
+ return offsetParent || document.documentElement;
+ });
+ }
+});
+
+
+// Create scrollLeft and scrollTop methods
+jQuery.each( {scrollLeft: "pageXOffset", scrollTop: "pageYOffset"}, function( method, prop ) {
+ var top = /Y/.test( prop );
+
+ jQuery.fn[ method ] = function( val ) {
+ return jQuery.access( this, function( elem, method, val ) {
+ var win = getWindow( elem );
+
+ if ( val === undefined ) {
+ return win ? (prop in win) ? win[ prop ] :
+ win.document.documentElement[ method ] :
+ elem[ method ];
+ }
+
+ if ( win ) {
+ win.scrollTo(
+ !top ? val : jQuery( win ).scrollLeft(),
+ top ? val : jQuery( win ).scrollTop()
+ );
+
+ } else {
+ elem[ method ] = val;
+ }
+ }, method, val, arguments.length, null );
+ };
+});
+
+function getWindow( elem ) {
+ return jQuery.isWindow( elem ) ?
+ elem :
+ elem.nodeType === 9 ?
+ elem.defaultView || elem.parentWindow :
+ false;
+}
+// Create innerHeight, innerWidth, height, width, outerHeight and outerWidth methods
+jQuery.each( { Height: "height", Width: "width" }, function( name, type ) {
+ jQuery.each( { padding: "inner" + name, content: type, "": "outer" + name }, function( defaultExtra, funcName ) {
+ // margin is only for outerHeight, outerWidth
+ jQuery.fn[ funcName ] = function( margin, value ) {
+ var chainable = arguments.length && ( defaultExtra || typeof margin !== "boolean" ),
+ extra = defaultExtra || ( margin === true || value === true ? "margin" : "border" );
+
+ return jQuery.access( this, function( elem, type, value ) {
+ var doc;
+
+ if ( jQuery.isWindow( elem ) ) {
+ // As of 5/8/2012 this will yield incorrect results for Mobile Safari, but there
+ // isn't a whole lot we can do. See pull request at this URL for discussion:
+ // https://github.com/jquery/jquery/pull/764
+ return elem.document.documentElement[ "client" + name ];
+ }
+
+ // Get document width or height
+ if ( elem.nodeType === 9 ) {
+ doc = elem.documentElement;
+
+ // Either scroll[Width/Height] or offset[Width/Height] or client[Width/Height], whichever is greatest
+ // unfortunately, this causes bug #3838 in IE6/8 only, but there is currently no good, small way to fix it.
+ return Math.max(
+ elem.body[ "scroll" + name ], doc[ "scroll" + name ],
+ elem.body[ "offset" + name ], doc[ "offset" + name ],
+ doc[ "client" + name ]
+ );
+ }
+
+ return value === undefined ?
+ // Get width or height on the element, requesting but not forcing parseFloat
+ jQuery.css( elem, type, extra ) :
+
+ // Set width or height on the element
+ jQuery.style( elem, type, value, extra );
+ }, type, chainable ? margin : undefined, chainable, null );
+ };
+ });
+});
+// Limit scope pollution from any deprecated API
+// (function() {
+
+// })();
+// Expose jQuery to the global object
+window.jQuery = window.$ = jQuery;
+
+// Expose jQuery as an AMD module, but only for AMD loaders that
+// understand the issues with loading multiple versions of jQuery
+// in a page that all might call define(). The loader will indicate
+// they have special allowances for multiple jQuery versions by
+// specifying define.amd.jQuery = true. Register as a named module,
+// since jQuery can be concatenated with other files that may use define,
+// but not use a proper concatenation script that understands anonymous
+// AMD modules. A named AMD is safest and most robust way to register.
+// Lowercase jquery is used because AMD module names are derived from
+// file names, and jQuery is normally delivered in a lowercase file name.
+// Do this after creating the global so that if an AMD module wants to call
+// noConflict to hide this version of jQuery, it will work.
+if ( typeof define === "function" && define.amd && define.amd.jQuery ) {
+ define( "jquery", [], function () { return jQuery; } );
+}
+
+})( window );
diff --git a/luci2/luci2/htdocs/luci2/jquery.peity.js b/luci2/luci2/htdocs/luci2/jquery.peity.js
new file mode 100644
index 000000000..05fa8c8a8
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/jquery.peity.js
@@ -0,0 +1,261 @@
+// Peity jQuery plugin version 1.2.0
+// (c) 2013 Ben Pickles
+//
+// http://benpickles.github.com/peity
+//
+// Released under MIT license.
+(function($, document, Math, devicePixelRatio) {
+ var canvasSupported = document.createElement("canvas").getContext
+
+ var peity = $.fn.peity = function(type, options) {
+ if (canvasSupported) {
+ this.each(function() {
+ var $this = $(this)
+ var chart = $this.data("peity")
+
+ if (chart) {
+ if (type) chart.type = type
+ $.extend(chart.opts, options)
+ chart.draw()
+ } else {
+ var defaults = peity.defaults[type]
+ var data = {}
+
+ $.each($this.data(), function(name, value) {
+ if (name in defaults) data[name] = value
+ })
+
+ var opts = $.extend({}, defaults, data, options)
+ var chart = new Peity($this, type, opts)
+ chart.draw()
+
+ $this
+ .change(function() { chart.draw() })
+ .data("peity", chart)
+ }
+ });
+ }
+
+ return this;
+ };
+
+ var Peity = function($el, type, opts) {
+ this.$el = $el
+ this.type = type
+ this.opts = opts
+ }
+
+ var PeityPrototype = Peity.prototype
+
+ PeityPrototype.colours = function() {
+ var colours = this.opts.colours
+ var func = colours
+
+ if (!$.isFunction(func)) {
+ func = function(_, i) {
+ return colours[i % colours.length]
+ }
+ }
+
+ return func
+ }
+
+ PeityPrototype.draw = function() {
+ peity.graphers[this.type].call(this, this.opts)
+ }
+
+ PeityPrototype.prepareCanvas = function(width, height) {
+ var canvas = this.canvas
+ var $canvas
+
+ if (canvas) {
+ this.context.clearRect(0, 0, canvas.width, canvas.height)
+ $canvas = $(canvas)
+ } else {
+ $canvas = $("
").css({
+ height: height,
+ width: width
+ }).addClass("peity").data("peity", this)
+
+ this.canvas = canvas = $canvas[0]
+ this.context = canvas.getContext("2d")
+ this.$el.hide().after(canvas)
+ }
+
+ canvas.height = $canvas.height() * devicePixelRatio
+ canvas.width = $canvas.width() * devicePixelRatio
+
+ return canvas
+ }
+
+ PeityPrototype.values = function() {
+ return $.map(this.$el.text().split(this.opts.delimiter), function(value) {
+ return parseFloat(value)
+ })
+ }
+
+ peity.defaults = {}
+ peity.graphers = {}
+
+ peity.register = function(type, defaults, grapher) {
+ this.defaults[type] = defaults
+ this.graphers[type] = grapher
+ }
+
+ peity.register(
+ 'pie',
+ {
+ colours: ["#ff9900", "#fff4dd", "#ffc66e"],
+ delimiter: null,
+ diameter: 16
+ },
+ function(opts) {
+ if (!opts.delimiter) {
+ var delimiter = this.$el.text().match(/[^0-9\.]/)
+ opts.delimiter = delimiter ? delimiter[0] : ","
+ }
+
+ var values = this.values()
+
+ if (opts.delimiter == "/") {
+ var v1 = values[0]
+ var v2 = values[1]
+ values = [v1, v2 - v1]
+ }
+
+ var i = 0
+ var length = values.length
+ var sum = 0
+
+ for (; i < length; i++) {
+ sum += values[i]
+ }
+
+ var canvas = this.prepareCanvas(opts.width || opts.diameter, opts.height || opts.diameter)
+ var context = this.context
+ var width = canvas.width
+ var height = canvas.height
+ var radius = Math.min(width, height) / 2
+ var pi = Math.PI
+ var colours = this.colours()
+
+ context.save()
+ context.translate(width / 2, height / 2)
+ context.rotate(-pi / 2)
+
+ for (i = 0; i < length; i++) {
+ var value = values[i]
+ var slice = (value / sum) * pi * 2
+
+ context.beginPath()
+ context.moveTo(0, 0)
+ context.arc(0, 0, radius, 0, slice, false)
+ context.fillStyle = colours.call(this, value, i, values)
+ context.fill()
+ context.rotate(slice)
+ }
+
+ context.restore()
+ }
+ )
+
+ peity.register(
+ "line",
+ {
+ colour: "#c6d9fd",
+ strokeColour: "#4d89f9",
+ strokeWidth: 1,
+ delimiter: ",",
+ height: 16,
+ max: null,
+ min: 0,
+ width: 32
+ },
+ function(opts) {
+ var values = this.values()
+ if (values.length == 1) values.push(values[0])
+ var max = Math.max.apply(Math, values.concat([opts.max]));
+ var min = Math.min.apply(Math, values.concat([opts.min]))
+
+ var canvas = this.prepareCanvas(opts.width, opts.height)
+ var context = this.context
+ var width = canvas.width
+ var height = canvas.height
+ var xQuotient = width / (values.length - 1)
+ var yQuotient = height / (max - min)
+
+ var coords = [];
+ var i;
+
+ context.beginPath();
+ context.moveTo(0, height + (min * yQuotient))
+
+ for (i = 0; i < values.length; i++) {
+ var x = i * xQuotient
+ var y = height - (yQuotient * (values[i] - min))
+
+ coords.push({ x: x, y: y });
+ context.lineTo(x, y);
+ }
+
+ context.lineTo(width, height + (min * yQuotient))
+ context.fillStyle = opts.colour;
+ context.fill();
+
+ if (opts.strokeWidth) {
+ context.beginPath();
+ context.moveTo(0, coords[0].y);
+ for (i = 0; i < coords.length; i++) {
+ context.lineTo(coords[i].x, coords[i].y);
+ }
+ context.lineWidth = opts.strokeWidth * devicePixelRatio;
+ context.strokeStyle = opts.strokeColour;
+ context.stroke();
+ }
+ }
+ );
+
+ peity.register(
+ 'bar',
+ {
+ colours: ["#4D89F9"],
+ delimiter: ",",
+ height: 16,
+ max: null,
+ min: 0,
+ spacing: devicePixelRatio,
+ width: 32
+ },
+ function(opts) {
+ var values = this.values()
+ var max = Math.max.apply(Math, values.concat([opts.max]));
+ var min = Math.min.apply(Math, values.concat([opts.min]))
+
+ var canvas = this.prepareCanvas(opts.width, opts.height)
+ var context = this.context
+
+ var width = canvas.width
+ var height = canvas.height
+ var yQuotient = height / (max - min)
+ var space = opts.spacing
+ var xQuotient = (width + space) / values.length
+ var colours = this.colours()
+
+ for (var i = 0; i < values.length; i++) {
+ var value = values[i]
+ var y = height - (yQuotient * (value - min))
+ var h
+
+ if (value == 0) {
+ if (min >= 0 || max > 0) y -= 1
+ h = 1
+ } else {
+ h = yQuotient * values[i]
+ }
+
+ context.fillStyle = colours.call(this, value, i, values)
+ context.fillRect(i * xQuotient, y, xQuotient - space, h)
+ }
+ }
+ );
+})(jQuery, document, Math, window.devicePixelRatio || 1);
diff --git a/luci2/luci2/htdocs/luci2/luci2.js b/luci2/luci2/htdocs/luci2/luci2.js
new file mode 100644
index 000000000..3e5af4a75
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/luci2.js
@@ -0,0 +1,705 @@
+/*
+ LuCI2 - OpenWrt Web Interface
+
+ Copyright 2013-2014 Jo-Philipp Wich
+
+ 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
+*/
+
+String.prototype.format = function()
+{
+ var html_esc = [/&/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)) != null)
+ {
+ 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)
+ ? '%dd %dh %dm %ds'.format(td, th, tm, ts)
+ : '%dh %dm %ds'.format(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;
+ }
+
+ subst = (typeof(subst) == 'undefined') ? '' : subst.toString();
+
+ if (minLength > 0 && pad.length > 0)
+ for (var i = 0; i < (minLength - subst.length); i++)
+ subst = justifyRight ? (pad + subst) : (subst + pad);
+ }
+ }
+
+ out += leftpart + subst;
+ str = str.substr(m.length);
+ }
+
+ return out + str;
+}
+
+if (!window.location.origin)
+ window.location.origin = '%s//%s%s'.format(
+ window.location.protocol,
+ window.location.hostname,
+ (window.location.port ? ':' + window.location.port : '')
+ );
+
+function LuCI2()
+{
+ var L = this;
+
+ var Class = function() { };
+
+ Class.extend = function(properties)
+ {
+ Class.initializing = true;
+
+ var prototype = new this();
+ var superprot = this.prototype;
+
+ Class.initializing = false;
+
+ $.extend(prototype, properties, {
+ callSuper: function() {
+ var args = [ ];
+ var meth = arguments[0];
+
+ if (typeof(superprot[meth]) != 'function')
+ return undefined;
+
+ for (var i = 1; i < arguments.length; i++)
+ args.push(arguments[i]);
+
+ return superprot[meth].apply(this, args);
+ }
+ });
+
+ function _class()
+ {
+ this.options = arguments[0] || { };
+
+ if (!Class.initializing && typeof(this.init) == 'function')
+ this.init.apply(this, arguments);
+ }
+
+ _class.prototype = prototype;
+ _class.prototype.constructor = _class;
+
+ _class.extend = Class.extend;
+
+ return _class;
+ };
+
+ Class.require = function(name)
+ {
+ var path = '/' + name.replace(/\./g, '/') + '.js';
+
+ return $.ajax(path, {
+ method: 'GET',
+ async: false,
+ cache: true,
+ dataType: 'text'
+ }).then(function(text) {
+ var code = '%s\n\n//@ sourceURL=%s/%s'.format(text, window.location.origin, path);
+ var construct = eval(code);
+
+ var parts = name.split(/\./);
+ var cparent = L.Class || (L.Class = { });
+
+ for (var i = 1; i < parts.length - 1; i++)
+ {
+ cparent = cparent[parts[i]];
+
+ if (!cparent)
+ throw "Missing parent class";
+ }
+
+ cparent[parts[i]] = construct;
+ });
+ };
+
+ Class.instantiate = function(name)
+ {
+ Class.require(name).then(function() {
+ var parts = name.split(/\./);
+ var iparent = L;
+ var construct = L.Class;
+
+ for (var i = 1; i < parts.length - 1; i++)
+ {
+ iparent = iparent[parts[i]];
+ construct = construct[parts[i]];
+
+ if (!iparent)
+ throw "Missing parent class";
+ }
+
+ if (construct[parts[i]])
+ iparent[parts[i]] = new construct[parts[i]]();
+ });
+ };
+
+ this.defaults = function(obj, def)
+ {
+ for (var key in def)
+ if (typeof(obj[key]) == 'undefined')
+ obj[key] = def[key];
+
+ return obj;
+ };
+
+ this.isDeferred = function(x)
+ {
+ return (typeof(x) == 'object' &&
+ typeof(x.then) == 'function' &&
+ typeof(x.promise) == 'function');
+ };
+
+ this.deferrable = function()
+ {
+ if (this.isDeferred(arguments[0]))
+ return arguments[0];
+
+ var d = $.Deferred();
+ d.resolve.apply(d, arguments);
+
+ return d.promise();
+ };
+
+ this.i18n = {
+
+ loaded: false,
+ catalog: { },
+ plural: function(n) { return 0 + (n != 1) },
+
+ init: function() {
+ if (L.i18n.loaded)
+ return;
+
+ var lang = (navigator.userLanguage || navigator.language || 'en').toLowerCase();
+ var langs = (lang.indexOf('-') > -1) ? [ lang, lang.split(/-/)[0] ] : [ lang ];
+
+ for (var i = 0; i < langs.length; i++)
+ $.ajax('%s/i18n/base.%s.json'.format(L.globals.resource, langs[i]), {
+ async: false,
+ cache: true,
+ dataType: 'json',
+ success: function(data) {
+ $.extend(L.i18n.catalog, data);
+
+ var pe = L.i18n.catalog[''];
+ if (pe)
+ {
+ delete L.i18n.catalog[''];
+ try {
+ var pf = new Function('n', 'return 0 + (' + pe + ')');
+ L.i18n.plural = pf;
+ } catch (e) { };
+ }
+ }
+ });
+
+ L.i18n.loaded = true;
+ }
+
+ };
+
+ this.tr = function(msgid)
+ {
+ L.i18n.init();
+
+ var msgstr = L.i18n.catalog[msgid];
+
+ if (typeof(msgstr) == 'undefined')
+ return msgid;
+ else if (typeof(msgstr) == 'string')
+ return msgstr;
+ else
+ return msgstr[0];
+ };
+
+ this.trp = function(msgid, msgid_plural, count)
+ {
+ L.i18n.init();
+
+ var msgstr = L.i18n.catalog[msgid];
+
+ if (typeof(msgstr) == 'undefined')
+ return (count == 1) ? msgid : msgid_plural;
+ else if (typeof(msgstr) == 'string')
+ return msgstr;
+ else
+ return msgstr[L.i18n.plural(count)];
+ };
+
+ this.trc = function(msgctx, msgid)
+ {
+ L.i18n.init();
+
+ var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx];
+
+ if (typeof(msgstr) == 'undefined')
+ return msgid;
+ else if (typeof(msgstr) == 'string')
+ return msgstr;
+ else
+ return msgstr[0];
+ };
+
+ this.trcp = function(msgctx, msgid, msgid_plural, count)
+ {
+ L.i18n.init();
+
+ var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx];
+
+ if (typeof(msgstr) == 'undefined')
+ return (count == 1) ? msgid : msgid_plural;
+ else if (typeof(msgstr) == 'string')
+ return msgstr;
+ else
+ return msgstr[L.i18n.plural(count)];
+ };
+
+ this.setHash = function(key, value)
+ {
+ var h = '';
+ var data = this.getHash(undefined);
+
+ if (typeof(value) == 'undefined')
+ delete data[key];
+ else
+ data[key] = value;
+
+ var keys = [ ];
+ for (var k in data)
+ keys.push(k);
+
+ keys.sort();
+
+ for (var i = 0; i < keys.length; i++)
+ {
+ if (i > 0)
+ h += ',';
+
+ h += keys[i] + ':' + data[keys[i]];
+ }
+
+ if (h.length)
+ location.hash = '#' + h;
+ else
+ location.hash = '';
+ };
+
+ this.getHash = function(key)
+ {
+ var data = { };
+ var tuples = (location.hash || '#').substring(1).split(/,/);
+
+ for (var i = 0; i < tuples.length; i++)
+ {
+ var tuple = tuples[i].split(/:/);
+ if (tuple.length == 2)
+ data[tuple[0]] = tuple[1];
+ }
+
+ if (typeof(key) != 'undefined')
+ return data[key];
+
+ return data;
+ };
+
+ this.toArray = function(x)
+ {
+ switch (typeof(x))
+ {
+ case 'number':
+ case 'boolean':
+ return [ x ];
+
+ case 'string':
+ var r = [ ];
+ var l = x.split(/\s+/);
+ for (var i = 0; i < l.length; i++)
+ if (l[i].length > 0)
+ r.push(l[i]);
+ return r;
+
+ case 'object':
+ if ($.isArray(x))
+ {
+ var r = [ ];
+ for (var i = 0; i < x.length; i++)
+ r.push(x[i]);
+ return r;
+ }
+ else if ($.isPlainObject(x))
+ {
+ var r = [ ];
+ for (var k in x)
+ if (x.hasOwnProperty(k))
+ r.push(k);
+ return r.sort();
+ }
+ }
+
+ return [ ];
+ };
+
+ this.toObject = function(x)
+ {
+ switch (typeof(x))
+ {
+ case 'number':
+ case 'boolean':
+ return { x: true };
+
+ case 'string':
+ var r = { };
+ var l = x.split(/\x+/);
+ for (var i = 0; i < l.length; i++)
+ if (l[i].length > 0)
+ r[l[i]] = true;
+ return r;
+
+ case 'object':
+ if ($.isArray(x))
+ {
+ var r = { };
+ for (var i = 0; i < x.length; i++)
+ r[x[i]] = true;
+ return r;
+ }
+ else if ($.isPlainObject(x))
+ {
+ return x;
+ }
+ }
+
+ return { };
+ };
+
+ this.filterArray = function(array, item)
+ {
+ if (!$.isArray(array))
+ return [ ];
+
+ for (var i = 0; i < array.length; i++)
+ if (array[i] === item)
+ array.splice(i--, 1);
+
+ return array;
+ };
+
+ this.toClassName = function(str, suffix)
+ {
+ var n = '';
+ var l = str.split(/[\/.]/);
+
+ for (var i = 0; i < l.length; i++)
+ if (l[i].length > 0)
+ n += l[i].charAt(0).toUpperCase() + l[i].substr(1).toLowerCase();
+
+ if (typeof(suffix) == 'string')
+ n += suffix;
+
+ return n;
+ };
+
+ this.toColor = function(str)
+ {
+ if (typeof(str) != 'string' || str.length == 0)
+ return '#CCCCCC';
+
+ if (str == 'wan')
+ return '#F09090';
+ else if (str == 'lan')
+ return '#90F090';
+
+ var i = 0, hash = 0;
+
+ while (i < str.length)
+ hash = str.charCodeAt(i++) + ((hash << 5) - hash);
+
+ var r = (hash & 0xFF) % 128;
+ var g = ((hash >> 8) & 0xFF) % 128;
+
+ var min = 0;
+ var max = 128;
+
+ if ((r + g) < 128)
+ min = 128 - r - g;
+ else
+ max = 255 - r - g;
+
+ var b = min + (((hash >> 16) & 0xFF) % (max - min));
+
+ return '#%02X%02X%02X'.format(0xFF - r, 0xFF - g, 0xFF - b);
+ };
+
+ this.parseIPv4 = function(str)
+ {
+ if ((typeof(str) != 'string' && !(str instanceof String)) ||
+ !str.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
+ return undefined;
+
+ var num = [ ];
+ var parts = str.split(/\./);
+
+ for (var i = 0; i < parts.length; i++)
+ {
+ var n = parseInt(parts[i], 10);
+ if (isNaN(n) || n > 255)
+ return undefined;
+
+ num.push(n);
+ }
+
+ return num;
+ };
+
+ this.parseIPv6 = function(str)
+ {
+ if ((typeof(str) != 'string' && !(str instanceof String)) ||
+ !str.match(/^[a-fA-F0-9:]+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/))
+ return undefined;
+
+ var parts = str.split(/::/);
+ if (parts.length == 0 || parts.length > 2)
+ return undefined;
+
+ var lnum = [ ];
+ if (parts[0].length > 0)
+ {
+ var left = parts[0].split(/:/);
+ for (var i = 0; i < left.length; i++)
+ {
+ var n = parseInt(left[i], 16);
+ if (isNaN(n))
+ return undefined;
+
+ lnum.push((n / 256) >> 0);
+ lnum.push(n % 256);
+ }
+ }
+
+ var rnum = [ ];
+ if (parts.length > 1 && parts[1].length > 0)
+ {
+ var right = parts[1].split(/:/);
+
+ for (var i = 0; i < right.length; i++)
+ {
+ if (right[i].indexOf('.') > 0)
+ {
+ var addr = L.parseIPv4(right[i]);
+ if (!addr)
+ return undefined;
+
+ rnum.push.apply(rnum, addr);
+ continue;
+ }
+
+ var n = parseInt(right[i], 16);
+ if (isNaN(n))
+ return undefined;
+
+ rnum.push((n / 256) >> 0);
+ rnum.push(n % 256);
+ }
+ }
+
+ if (rnum.length > 0 && (lnum.length + rnum.length) > 15)
+ return undefined;
+
+ var num = [ ];
+
+ num.push.apply(num, lnum);
+
+ for (var i = 0; i < (16 - lnum.length - rnum.length); i++)
+ num.push(0);
+
+ num.push.apply(num, rnum);
+
+ if (num.length > 16)
+ return undefined;
+
+ return num;
+ };
+
+ this.isNetmask = function(addr)
+ {
+ if (!$.isArray(addr))
+ return false;
+
+ var c;
+
+ for (c = 0; (c < addr.length) && (addr[c] == 255); c++);
+
+ if (c == addr.length)
+ return true;
+
+ if ((addr[c] == 254) || (addr[c] == 252) || (addr[c] == 248) ||
+ (addr[c] == 240) || (addr[c] == 224) || (addr[c] == 192) ||
+ (addr[c] == 128) || (addr[c] == 0))
+ {
+ for (c++; (c < addr.length) && (addr[c] == 0); c++);
+
+ if (c == addr.length)
+ return true;
+ }
+
+ return false;
+ };
+
+ this.globals = {
+ timeout: 15000,
+ resource: '/luci2',
+ sid: '00000000000000000000000000000000'
+ };
+
+ Class.instantiate('luci2.rpc');
+ Class.instantiate('luci2.uci');
+ Class.instantiate('luci2.network');
+ Class.instantiate('luci2.wireless');
+ Class.instantiate('luci2.firewall');
+ Class.instantiate('luci2.system');
+ Class.instantiate('luci2.session');
+ Class.instantiate('luci2.ui');
+ Class.instantiate('luci2.cbi');
+};
diff --git a/luci2/luci2/htdocs/luci2/network.js b/luci2/luci2/htdocs/luci2/network.js
new file mode 100644
index 000000000..64e32e5ef
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/network.js
@@ -0,0 +1,1447 @@
+(function() {
+ var network_class = {
+ deviceBlacklist: [
+ /^gre[0-9]+$/,
+ /^gretap[0-9]+$/,
+ /^ifb[0-9]+$/,
+ /^ip6tnl[0-9]+$/,
+ /^sit[0-9]+$/,
+ /^wlan[0-9]+\.sta[0-9]+$/,
+ /^tunl[0-9]+$/,
+ /^ip6gre[0-9]+$/
+ ],
+
+ rpcCacheFunctions: [
+ 'protolist', 0, L.rpc.declare({
+ object: 'network',
+ method: 'get_proto_handlers',
+ expect: { '': { } }
+ }),
+ 'ifstate', 1, L.rpc.declare({
+ object: 'network.interface',
+ method: 'dump',
+ expect: { 'interface': [ ] }
+ }),
+ 'devstate', 2, L.rpc.declare({
+ object: 'network.device',
+ method: 'status',
+ expect: { '': { } }
+ }),
+ 'wifistate', 0, L.rpc.declare({
+ object: 'network.wireless',
+ method: 'status',
+ expect: { '': { } }
+ }),
+ 'bwstate', 2, L.rpc.declare({
+ object: 'luci2.network.bwmon',
+ method: 'statistics',
+ expect: { 'statistics': { } }
+ }),
+ 'devlist', 2, L.rpc.declare({
+ object: 'luci2.network',
+ method: 'device_list',
+ expect: { 'devices': [ ] }
+ }),
+ 'swlist', 0, L.rpc.declare({
+ object: 'luci2.network',
+ method: 'switch_list',
+ expect: { 'switches': [ ] }
+ })
+ ],
+
+ loadProtocolHandler: function(proto)
+ {
+ var url = L.globals.resource + '/proto/' + proto + '.js';
+ var self = L.network;
+
+ var def = $.Deferred();
+
+ $.ajax(url, {
+ method: 'GET',
+ cache: true,
+ dataType: 'text'
+ }).then(function(data) {
+ try {
+ var protoConstructorSource = (
+ '(function(L, $) { ' +
+ 'return %s' +
+ '})(L, $);\n\n' +
+ '//@ sourceURL=%s/%s'
+ ).format(data, window.location.origin, url);
+
+ var protoClass = eval(protoConstructorSource);
+
+ self.protocolHandlers[proto] = new protoClass();
+ }
+ catch(e) {
+ alert('Unable to instantiate proto "%s": %s'.format(url, e));
+ };
+
+ def.resolve();
+ }).fail(function() {
+ def.resolve();
+ });
+
+ return def;
+ },
+
+ loadProtocolHandlers: function()
+ {
+ var self = L.network;
+ var deferreds = [
+ self.loadProtocolHandler('none')
+ ];
+
+ for (var proto in self.rpcCache.protolist)
+ deferreds.push(self.loadProtocolHandler(proto));
+
+ return $.when.apply($, deferreds);
+ },
+
+ callSwitchInfo: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'switch_info',
+ params: [ 'switch' ],
+ expect: { 'info': { } }
+ }),
+
+ callSwitchInfoCallback: function(responses) {
+ var self = L.network;
+ var swlist = self.rpcCache.swlist;
+ var swstate = self.rpcCache.swstate = { };
+
+ for (var i = 0; i < responses.length; i++)
+ swstate[swlist[i]] = responses[i];
+ },
+
+ loadCacheCallback: function(level)
+ {
+ var self = L.network;
+ var name = '_fetch_cache_cb_' + level;
+
+ return self[name] || (
+ self[name] = function(responses)
+ {
+ for (var i = 0; i < self.rpcCacheFunctions.length; i += 3)
+ if (!level || self.rpcCacheFunctions[i + 1] == level)
+ self.rpcCache[self.rpcCacheFunctions[i]] = responses.shift();
+
+ if (!level)
+ {
+ L.rpc.batch();
+
+ for (var i = 0; i < self.rpcCache.swlist.length; i++)
+ self.callSwitchInfo(self.rpcCache.swlist[i]);
+
+ return L.rpc.flush().then(self.callSwitchInfoCallback);
+ }
+
+ return L.deferrable();
+ }
+ );
+ },
+
+ loadCache: function(level)
+ {
+ var self = L.network;
+
+ return L.uci.load(['network', 'wireless']).then(function() {
+ L.rpc.batch();
+
+ for (var i = 0; i < self.rpcCacheFunctions.length; i += 3)
+ if (!level || self.rpcCacheFunctions[i + 1] == level)
+ self.rpcCacheFunctions[i + 2]();
+
+ return L.rpc.flush().then(self.loadCacheCallback(level || 0));
+ });
+ },
+
+ isBlacklistedDevice: function(dev)
+ {
+ for (var i = 0; i < this.deviceBlacklist.length; i++)
+ if (dev.match(this.deviceBlacklist[i]))
+ return true;
+
+ return false;
+ },
+
+ sortDevicesCallback: function(a, b)
+ {
+ if (a.options.kind < b.options.kind)
+ return -1;
+ else if (a.options.kind > b.options.kind)
+ return 1;
+
+ if (a.options.name < b.options.name)
+ return -1;
+ else if (a.options.name > b.options.name)
+ return 1;
+
+ return 0;
+ },
+
+ getDeviceObject: function(ifname)
+ {
+ var alias = (ifname.charAt(0) == '@');
+ return this.deviceObjects[ifname] || (
+ this.deviceObjects[ifname] = {
+ ifname: ifname,
+ kind: alias ? 'alias' : 'ethernet',
+ type: alias ? 0 : 1,
+ up: false,
+ changed: { }
+ }
+ );
+ },
+
+ getInterfaceObject: function(name)
+ {
+ return this.interfaceObjects[name] || (
+ this.interfaceObjects[name] = {
+ name: name,
+ proto: this.protocolHandlers.none,
+ changed: { }
+ }
+ );
+ },
+
+ loadDevicesCallback: function()
+ {
+ var self = L.network;
+ var wificount = { };
+
+ for (var ifname in self.rpcCache.devstate)
+ {
+ if (self.isBlacklistedDevice(ifname))
+ continue;
+
+ var dev = self.rpcCache.devstate[ifname];
+ var entry = self.getDeviceObject(ifname);
+
+ entry.up = dev.up;
+
+ switch (dev.type)
+ {
+ case 'IP tunnel':
+ entry.kind = 'tunnel';
+ break;
+
+ case 'Bridge':
+ entry.kind = 'bridge';
+ //entry.ports = dev['bridge-members'].sort();
+ break;
+ }
+ }
+
+ for (var i = 0; i < self.rpcCache.devlist.length; i++)
+ {
+ var dev = self.rpcCache.devlist[i];
+
+ if (self.isBlacklistedDevice(dev.device))
+ continue;
+
+ var entry = self.getDeviceObject(dev.device);
+
+ entry.up = dev.is_up;
+ entry.type = dev.type;
+
+ switch (dev.type)
+ {
+ case 1: /* Ethernet */
+ if (dev.is_bridge)
+ entry.kind = 'bridge';
+ else if (dev.is_tuntap)
+ entry.kind = 'tunnel';
+ else if (dev.is_wireless)
+ entry.kind = 'wifi';
+ break;
+
+ case 512: /* PPP */
+ case 768: /* IP-IP Tunnel */
+ case 769: /* IP6-IP6 Tunnel */
+ case 776: /* IPv6-in-IPv4 */
+ case 778: /* GRE over IP */
+ entry.kind = 'tunnel';
+ break;
+ }
+ }
+
+ var net = L.uci.sections('network');
+ for (var i = 0; i < net.length; i++)
+ {
+ var s = net[i];
+ var sid = s['.name'];
+
+ if (s['.type'] == 'device' && s.name)
+ {
+ var entry = self.getDeviceObject(s.name);
+
+ switch (s.type)
+ {
+ case 'macvlan':
+ case 'tunnel':
+ entry.kind = 'tunnel';
+ break;
+ }
+
+ entry.sid = sid;
+ }
+ else if (s['.type'] == 'interface' && !s['.anonymous'] && s.ifname)
+ {
+ var ifnames = L.toArray(s.ifname);
+
+ for (var j = 0; j < ifnames.length; j++)
+ self.getDeviceObject(ifnames[j]);
+
+ if (s['.name'] != 'loopback')
+ {
+ var entry = self.getDeviceObject('@%s'.format(s['.name']));
+
+ entry.type = 0;
+ entry.kind = 'alias';
+ entry.sid = sid;
+ }
+ }
+ else if (s['.type'] == 'switch_vlan' && s.device)
+ {
+ var sw = self.rpcCache.swstate[s.device];
+ var vid = parseInt(s.vid || s.vlan);
+ var ports = L.toArray(s.ports);
+
+ if (!sw || !ports.length || isNaN(vid))
+ continue;
+
+ var ifname = undefined;
+
+ for (var j = 0; j < ports.length; j++)
+ {
+ var port = parseInt(ports[j]);
+ var tag = (ports[j].replace(/[^tu]/g, '') == 't');
+
+ if (port == sw.cpu_port)
+ {
+ // XXX: need a way to map switch to netdev
+ if (tag)
+ ifname = 'eth0.%d'.format(vid);
+ else
+ ifname = 'eth0';
+
+ break;
+ }
+ }
+
+ if (!ifname)
+ continue;
+
+ var entry = self.getDeviceObject(ifname);
+
+ entry.kind = 'vlan';
+ entry.sid = sid;
+ entry.vsw = sw;
+ entry.vid = vid;
+ }
+ }
+
+ var wifi = L.uci.sections('wireless');
+ for (var i = 0, c = 0; i < wifi.length; i++)
+ {
+ var s = wifi[i];
+
+ if (s['.type'] == 'wifi-iface')
+ {
+ var sid = '@wifi-iface[%d]'.format(c++);
+
+ if (!s.device)
+ continue;
+
+ var r = parseInt(s.device.replace(/^[^0-9]+/, ''));
+ var n = wificount[s.device] = (wificount[s.device] || 0) + 1;
+ var id = 'radio%d.network%d'.format(r, n);
+ var ifname = id;
+
+ if (self.rpcCache.wifistate[s.device])
+ {
+ var ifcs = self.rpcCache.wifistate[s.device].interfaces;
+ for (var ifc in ifcs)
+ {
+ if (ifcs[ifc].section == sid && ifcs[ifc].ifname)
+ {
+ ifname = ifcs[ifc].ifname;
+ break;
+ }
+ }
+ }
+
+ var entry = self.getDeviceObject(ifname);
+
+ entry.kind = 'wifi';
+ entry.sid = s['.name'];
+ entry.wid = id;
+ entry.wdev = s.device;
+ entry.wmode = s.mode;
+ entry.wssid = s.ssid;
+ entry.wbssid = s.bssid;
+ }
+ }
+
+ for (var i = 0; i < net.length; i++)
+ {
+ var s = net[i];
+ var sid = s['.name'];
+
+ if (s['.type'] == 'interface' && !s['.anonymous'] && s.type == 'bridge')
+ {
+ var ifnames = L.toArray(s.ifname);
+
+ for (var ifname in self.deviceObjects)
+ {
+ var dev = self.deviceObjects[ifname];
+
+ if (dev.kind != 'wifi')
+ continue;
+
+ var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network'));
+ if ($.inArray(sid, wnets) > -1)
+ ifnames.push(ifname);
+ }
+
+ entry = self.getDeviceObject('br-%s'.format(s['.name']));
+ entry.type = 1;
+ entry.kind = 'bridge';
+ entry.sid = sid;
+ entry.ports = ifnames.sort();
+ }
+ }
+ },
+
+ loadInterfacesCallback: function()
+ {
+ var self = L.network;
+ var net = L.uci.sections('network');
+
+ for (var i = 0; i < net.length; i++)
+ {
+ var s = net[i];
+ var sid = s['.name'];
+
+ if (s['.type'] == 'interface' && !s['.anonymous'] && s.proto)
+ {
+ var entry = self.getInterfaceObject(s['.name']);
+ var proto = self.protocolHandlers[s.proto] || self.protocolHandlers.none;
+
+ var l3dev = undefined;
+ var l2dev = undefined;
+
+ var ifnames = L.toArray(s.ifname);
+
+ for (var ifname in self.deviceObjects)
+ {
+ var dev = self.deviceObjects[ifname];
+
+ if (dev.kind != 'wifi')
+ continue;
+
+ var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network'));
+ if ($.inArray(entry.name, wnets) > -1)
+ ifnames.push(ifname);
+ }
+
+ if (proto.virtual)
+ l3dev = '%s-%s'.format(s.proto, entry.name);
+ else if (s.type == 'bridge')
+ l3dev = 'br-%s'.format(entry.name);
+ else
+ l3dev = ifnames[0];
+
+ if (!proto.virtual && s.type == 'bridge')
+ l2dev = 'br-%s'.format(entry.name);
+ else if (!proto.virtual)
+ l2dev = ifnames[0];
+
+ entry.proto = proto;
+ entry.sid = sid;
+ entry.l3dev = l3dev;
+ entry.l2dev = l2dev;
+ }
+ }
+
+ for (var i = 0; i < self.rpcCache.ifstate.length; i++)
+ {
+ var iface = self.rpcCache.ifstate[i];
+ var entry = self.getInterfaceObject(iface['interface']);
+ var proto = self.protocolHandlers[iface.proto] || self.protocolHandlers.none;
+
+ /* this is a virtual interface, either deleted from config but
+ not applied yet or set up from external tools (6rd) */
+ if (!entry.sid)
+ {
+ entry.proto = proto;
+ entry.l2dev = iface.device;
+ entry.l3dev = iface.l3_device;
+ }
+ }
+ },
+
+ load: function()
+ {
+ var self = this;
+
+ if (self.rpcCache)
+ return L.deferrable();
+
+ self.rpcCache = { };
+ self.deviceObjects = { };
+ self.interfaceObjects = { };
+ self.protocolHandlers = { };
+
+ return self.loadCache()
+ .then(self.loadProtocolHandlers)
+ .then(self.loadDevicesCallback)
+ .then(self.loadInterfacesCallback);
+ },
+
+ update: function()
+ {
+ delete this.rpcCache;
+ return this.load();
+ },
+
+ refreshInterfaceStatus: function()
+ {
+ return this.loadCache(1).then(this.loadInterfacesCallback);
+ },
+
+ refreshDeviceStatus: function()
+ {
+ return this.loadCache(2).then(this.loadDevicesCallback);
+ },
+
+ refreshStatus: function()
+ {
+ return this.loadCache(1)
+ .then(this.loadCache(2))
+ .then(this.loadDevicesCallback)
+ .then(this.loadInterfacesCallback);
+ },
+
+ getDevices: function()
+ {
+ var devs = [ ];
+
+ for (var ifname in this.deviceObjects)
+ if (ifname != 'lo')
+ devs.push(new L.network.Device(this.deviceObjects[ifname]));
+
+ return devs.sort(this.sortDevicesCallback);
+ },
+
+ getDeviceByInterface: function(iface)
+ {
+ if (iface instanceof L.network.Interface)
+ iface = iface.name();
+
+ if (this.interfaceObjects[iface])
+ return this.getDevice(this.interfaceObjects[iface].l3dev) ||
+ this.getDevice(this.interfaceObjects[iface].l2dev);
+
+ return undefined;
+ },
+
+ getDevice: function(ifname)
+ {
+ if (this.deviceObjects[ifname])
+ return new L.network.Device(this.deviceObjects[ifname]);
+
+ return undefined;
+ },
+
+ createDevice: function(name)
+ {
+ return new L.network.Device(this.getDeviceObject(name));
+ },
+
+ getInterfaces: function()
+ {
+ var ifaces = [ ];
+
+ for (var name in this.interfaceObjects)
+ if (name != 'loopback')
+ ifaces.push(this.getInterface(name));
+
+ ifaces.sort(function(a, b) {
+ if (a.name() < b.name())
+ return -1;
+ else if (a.name() > b.name())
+ return 1;
+ else
+ return 0;
+ });
+
+ return ifaces;
+ },
+
+ getInterfacesByDevice: function(dev)
+ {
+ var ifaces = [ ];
+
+ if (dev instanceof L.network.Device)
+ dev = dev.name();
+
+ for (var name in this.interfaceObjects)
+ {
+ var iface = this.interfaceObjects[name];
+ if (iface.l2dev == dev || iface.l3dev == dev)
+ ifaces.push(this.getInterface(name));
+ }
+
+ ifaces.sort(function(a, b) {
+ if (a.name() < b.name())
+ return -1;
+ else if (a.name() > b.name())
+ return 1;
+ else
+ return 0;
+ });
+
+ return ifaces;
+ },
+
+ getInterface: function(iface)
+ {
+ if (this.interfaceObjects[iface])
+ return new L.network.Interface(this.interfaceObjects[iface]);
+
+ return undefined;
+ },
+
+ getProtocols: function()
+ {
+ var rv = [ ];
+
+ for (var proto in this.protocolHandlers)
+ {
+ var pr = this.protocolHandlers[proto];
+
+ rv.push({
+ name: proto,
+ description: pr.description,
+ virtual: pr.virtual,
+ tunnel: pr.tunnel
+ });
+ }
+
+ return rv.sort(function(a, b) {
+ if (a.name < b.name)
+ return -1;
+ else if (a.name > b.name)
+ return 1;
+ else
+ return 0;
+ });
+ },
+
+ findWANByAddr: function(ipaddr)
+ {
+ for (var i = 0; i < this.rpcCache.ifstate.length; i++)
+ {
+ var ifstate = this.rpcCache.ifstate[i];
+
+ if (!ifstate.route)
+ continue;
+
+ for (var j = 0; j < ifstate.route.length; j++)
+ if (ifstate.route[j].mask == 0 &&
+ ifstate.route[j].target == ipaddr &&
+ typeof(ifstate.route[j].table) == 'undefined')
+ {
+ return this.getInterface(ifstate['interface']);
+ }
+ }
+
+ return undefined;
+ },
+
+ findWAN: function()
+ {
+ return this.findWANByAddr('0.0.0.0');
+ },
+
+ findWAN6: function()
+ {
+ return this.findWANByAddr('::');
+ },
+
+ resolveAlias: function(ifname)
+ {
+ if (ifname instanceof L.network.Device)
+ ifname = ifname.name();
+
+ var dev = this.deviceObjects[ifname];
+ var seen = { };
+
+ while (dev && dev.kind == 'alias')
+ {
+ // loop
+ if (seen[dev.ifname])
+ return undefined;
+
+ var ifc = this.interfaceObjects[dev.sid];
+
+ seen[dev.ifname] = true;
+ dev = ifc ? this.deviceObjects[ifc.l3dev] : undefined;
+ }
+
+ return dev ? this.getDevice(dev.ifname) : undefined;
+ }
+ };
+
+ network_class.Interface = Class.extend({
+ getStatus: function(key)
+ {
+ var s = L.network.rpcCache.ifstate;
+
+ for (var i = 0; i < s.length; i++)
+ if (s[i]['interface'] == this.options.name)
+ return key ? s[i][key] : s[i];
+
+ return undefined;
+ },
+
+ get: function(key)
+ {
+ return L.uci.get('network', this.options.name, key);
+ },
+
+ set: function(key, val)
+ {
+ return L.uci.set('network', this.options.name, key, val);
+ },
+
+ name: function()
+ {
+ return this.options.name;
+ },
+
+ protocol: function()
+ {
+ return (this.get('proto') || 'none');
+ },
+
+ isUp: function()
+ {
+ return (this.getStatus('up') === true);
+ },
+
+ isVirtual: function()
+ {
+ return (typeof(this.options.sid) != 'string');
+ },
+
+ getProtocol: function()
+ {
+ var prname = this.get('proto') || 'none';
+ return L.network.protocolHandlers[prname] || L.network.protocolHandlers.none;
+ },
+
+ getUptime: function()
+ {
+ var uptime = this.getStatus('uptime');
+ return isNaN(uptime) ? 0 : uptime;
+ },
+
+ getDevice: function(resolveAlias)
+ {
+ if (this.options.l3dev)
+ return L.network.getDevice(this.options.l3dev);
+
+ return undefined;
+ },
+
+ getPhysdev: function()
+ {
+ if (this.options.l2dev)
+ return L.network.getDevice(this.options.l2dev);
+
+ return undefined;
+ },
+
+ getSubdevices: function()
+ {
+ var rv = [ ];
+ var dev = this.options.l2dev ?
+ L.network.deviceObjects[this.options.l2dev] : undefined;
+
+ if (dev && dev.kind == 'bridge' && dev.ports && dev.ports.length)
+ for (var i = 0; i < dev.ports.length; i++)
+ rv.push(L.network.getDevice(dev.ports[i]));
+
+ return rv;
+ },
+
+ getIPv4Addrs: function(mask)
+ {
+ var rv = [ ];
+ var addrs = this.getStatus('ipv4-address');
+
+ if (addrs)
+ for (var i = 0; i < addrs.length; i++)
+ if (!mask)
+ rv.push(addrs[i].address);
+ else
+ rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask));
+
+ return rv;
+ },
+
+ getIPv6Addrs: function(mask)
+ {
+ var rv = [ ];
+ var addrs;
+
+ addrs = this.getStatus('ipv6-address');
+
+ if (addrs)
+ for (var i = 0; i < addrs.length; i++)
+ if (!mask)
+ rv.push(addrs[i].address);
+ else
+ rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask));
+
+ addrs = this.getStatus('ipv6-prefix-assignment');
+
+ if (addrs)
+ for (var i = 0; i < addrs.length; i++)
+ if (!mask)
+ rv.push('%s1'.format(addrs[i].address));
+ else
+ rv.push('%s1/%d'.format(addrs[i].address, addrs[i].mask));
+
+ return rv;
+ },
+
+ getDNSAddrs: function()
+ {
+ var rv = [ ];
+ var addrs = this.getStatus('dns-server');
+
+ if (addrs)
+ for (var i = 0; i < addrs.length; i++)
+ rv.push(addrs[i]);
+
+ return rv;
+ },
+
+ getIPv4DNS: function()
+ {
+ var rv = [ ];
+ var dns = this.getStatus('dns-server');
+
+ if (dns)
+ for (var i = 0; i < dns.length; i++)
+ if (dns[i].indexOf(':') == -1)
+ rv.push(dns[i]);
+
+ return rv;
+ },
+
+ getIPv6DNS: function()
+ {
+ var rv = [ ];
+ var dns = this.getStatus('dns-server');
+
+ if (dns)
+ for (var i = 0; i < dns.length; i++)
+ if (dns[i].indexOf(':') > -1)
+ rv.push(dns[i]);
+
+ return rv;
+ },
+
+ getIPv4Gateway: function()
+ {
+ var rt = this.getStatus('route');
+
+ if (rt)
+ for (var i = 0; i < rt.length; i++)
+ if (rt[i].target == '0.0.0.0' && rt[i].mask == 0)
+ return rt[i].nexthop;
+
+ return undefined;
+ },
+
+ getIPv6Gateway: function()
+ {
+ var rt = this.getStatus('route');
+
+ if (rt)
+ for (var i = 0; i < rt.length; i++)
+ if (rt[i].target == '::' && rt[i].mask == 0)
+ return rt[i].nexthop;
+
+ return undefined;
+ },
+
+ getStatistics: function()
+ {
+ var dev = this.getDevice() || new L.network.Device({});
+ return dev.getStatistics();
+ },
+
+ getTrafficHistory: function()
+ {
+ var dev = this.getDevice() || new L.network.Device({});
+ return dev.getTrafficHistory();
+ },
+
+ renderBadge: function()
+ {
+ var badge = $(' ')
+ .addClass('badge')
+ .text('%s: '.format(this.name()));
+
+ var dev = this.getDevice();
+ var subdevs = this.getSubdevices();
+
+ if (subdevs.length)
+ for (var j = 0; j < subdevs.length; j++)
+ badge.append($(' ')
+ .attr('src', subdevs[j].icon())
+ .attr('title', '%s (%s)'.format(subdevs[j].description(), subdevs[j].name() || '?')));
+ else if (dev)
+ badge.append($(' ')
+ .attr('src', dev.icon())
+ .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?')));
+ else
+ badge.append($(' ').text(L.tr('(No devices attached)')));
+
+ return badge;
+ },
+
+ setDevices: function(devs)
+ {
+ var dev = this.getPhysdev();
+ var old_devs = [ ];
+ var changed = false;
+
+ if (dev && dev.isBridge())
+ old_devs = this.getSubdevices();
+ else if (dev)
+ old_devs = [ dev ];
+
+ if (old_devs.length != devs.length)
+ changed = true;
+ else
+ for (var i = 0; i < old_devs.length; i++)
+ {
+ var dev = devs[i];
+
+ if (dev instanceof L.network.Device)
+ dev = dev.name();
+
+ if (!dev || old_devs[i].name() != dev)
+ {
+ changed = true;
+ break;
+ }
+ }
+
+ if (changed)
+ {
+ for (var i = 0; i < old_devs.length; i++)
+ old_devs[i].removeFromInterface(this);
+
+ for (var i = 0; i < devs.length; i++)
+ {
+ var dev = devs[i];
+
+ if (!(dev instanceof L.network.Device))
+ dev = L.network.getDevice(dev);
+
+ if (dev)
+ dev.attachToInterface(this);
+ }
+ }
+ },
+
+ changeProtocol: function(proto)
+ {
+ var pr = L.network.protocolHandlers[proto];
+
+ if (!pr)
+ return;
+
+ for (var opt in (this.get() || { }))
+ {
+ switch (opt)
+ {
+ case 'type':
+ case 'ifname':
+ case 'macaddr':
+ if (pr.virtual)
+ this.set(opt, undefined);
+ break;
+
+ case 'auto':
+ case 'mtu':
+ break;
+
+ case 'proto':
+ this.set(opt, pr.protocol);
+ break;
+
+ default:
+ this.set(opt, undefined);
+ break;
+ }
+ }
+ },
+
+ createFormPrepareCallback: function()
+ {
+ var map = this;
+ var iface = map.options.netIface;
+ var proto = iface.getProtocol();
+ var device = iface.getDevice();
+
+ map.options.caption = L.tr('Configure "%s"').format(iface.name());
+
+ var section = map.section(L.cbi.SingleSection, iface.name(), {
+ anonymous: true
+ });
+
+ section.tab({
+ id: 'general',
+ caption: L.tr('General Settings')
+ });
+
+ section.tab({
+ id: 'advanced',
+ caption: L.tr('Advanced Settings')
+ });
+
+ section.tab({
+ id: 'ipv6',
+ caption: L.tr('IPv6')
+ });
+
+ section.tab({
+ id: 'physical',
+ caption: L.tr('Physical Settings')
+ });
+
+
+ section.taboption('general', L.cbi.CheckboxValue, 'auto', {
+ caption: L.tr('Start on boot'),
+ optional: true,
+ initial: true
+ });
+
+ var pr = section.taboption('general', L.cbi.ListValue, 'proto', {
+ caption: L.tr('Protocol')
+ });
+
+ pr.ucivalue = function(sid) {
+ return iface.get('proto') || 'none';
+ };
+
+ var ok = section.taboption('general', L.cbi.ButtonValue, '_confirm', {
+ caption: L.tr('Really switch?'),
+ description: L.tr('Changing the protocol will clear all configuration for this interface!'),
+ text: L.tr('Change protocol')
+ });
+
+ ok.on('click', function(ev) {
+ iface.changeProtocol(pr.formvalue(ev.data.sid));
+ iface.createForm(mapwidget).show();
+ });
+
+ var protos = L.network.getProtocols();
+
+ for (var i = 0; i < protos.length; i++)
+ pr.value(protos[i].name, protos[i].description);
+
+ proto.populateForm(section, iface);
+
+ if (!proto.virtual)
+ {
+ var br = section.taboption('physical', L.cbi.CheckboxValue, 'type', {
+ caption: L.tr('Network bridge'),
+ description: L.tr('Merges multiple devices into one logical bridge'),
+ optional: true,
+ enabled: 'bridge',
+ disabled: '',
+ initial: ''
+ });
+
+ section.taboption('physical', L.cbi.DeviceList, '__iface_multi', {
+ caption: L.tr('Devices'),
+ multiple: true,
+ bridges: false
+ }).depends('type', true);
+
+ section.taboption('physical', L.cbi.DeviceList, '__iface_single', {
+ caption: L.tr('Device'),
+ multiple: false,
+ bridges: true
+ }).depends('type', false);
+
+ var mac = section.taboption('physical', L.cbi.InputValue, 'macaddr', {
+ caption: L.tr('Override MAC'),
+ optional: true,
+ placeholder: device ? device.getMACAddress() : undefined,
+ datatype: 'macaddr'
+ })
+
+ mac.ucivalue = function(sid)
+ {
+ if (device)
+ return device.get('macaddr');
+
+ return this.callSuper('ucivalue', sid);
+ };
+
+ mac.save = function(sid)
+ {
+ if (!this.changed(sid))
+ return false;
+
+ if (device)
+ device.set('macaddr', this.formvalue(sid));
+ else
+ this.callSuper('set', sid);
+
+ return true;
+ };
+ }
+
+ section.taboption('physical', L.cbi.InputValue, 'mtu', {
+ caption: L.tr('Override MTU'),
+ optional: true,
+ placeholder: device ? device.getMTU() : undefined,
+ datatype: 'range(1, 9000)'
+ });
+
+ section.taboption('physical', L.cbi.InputValue, 'metric', {
+ caption: L.tr('Override Metric'),
+ optional: true,
+ placeholder: 0,
+ datatype: 'uinteger'
+ });
+
+ for (var field in section.fields)
+ {
+ switch (field)
+ {
+ case 'proto':
+ break;
+
+ case '_confirm':
+ for (var i = 0; i < protos.length; i++)
+ if (protos[i].name != proto.protocol)
+ section.fields[field].depends('proto', protos[i].name);
+ break;
+
+ default:
+ section.fields[field].depends('proto', proto.protocol, true);
+ break;
+ }
+ }
+ },
+
+ createForm: function(mapwidget)
+ {
+ var self = this;
+
+ if (!mapwidget)
+ mapwidget = L.cbi.Map;
+
+ var map = new mapwidget('network', {
+ prepare: self.createFormPrepareCallback,
+ netIface: self
+ });
+
+ return map;
+ }
+ });
+
+ network_class.Device = Class.extend({
+ wifiModeStrings: {
+ ap: L.tr('Master'),
+ sta: L.tr('Client'),
+ adhoc: L.tr('Ad-Hoc'),
+ monitor: L.tr('Monitor'),
+ wds: L.tr('Static WDS')
+ },
+
+ getStatus: function(key)
+ {
+ var s = L.network.rpcCache.devstate[this.options.ifname];
+
+ if (s)
+ return key ? s[key] : s;
+
+ return undefined;
+ },
+
+ get: function(key)
+ {
+ var sid = this.options.sid;
+ var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network';
+ return L.uci.get(pkg, sid, key);
+ },
+
+ set: function(key, val)
+ {
+ var sid = this.options.sid;
+ var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network';
+ return L.uci.set(pkg, sid, key, val);
+ },
+
+ init: function()
+ {
+ if (typeof(this.options.type) == 'undefined')
+ this.options.type = 1;
+
+ if (typeof(this.options.kind) == 'undefined')
+ this.options.kind = 'ethernet';
+
+ if (typeof(this.options.networks) == 'undefined')
+ this.options.networks = [ ];
+ },
+
+ name: function()
+ {
+ return this.options.ifname;
+ },
+
+ description: function()
+ {
+ switch (this.options.kind)
+ {
+ case 'alias':
+ return L.tr('Alias for network "%s"').format(this.options.ifname.substring(1));
+
+ case 'bridge':
+ return L.tr('Network bridge');
+
+ case 'ethernet':
+ return L.tr('Network device');
+
+ case 'tunnel':
+ switch (this.options.type)
+ {
+ case 1: /* tuntap */
+ return L.tr('TAP device');
+
+ case 512: /* PPP */
+ return L.tr('PPP tunnel');
+
+ case 768: /* IP-IP Tunnel */
+ return L.tr('IP-in-IP tunnel');
+
+ case 769: /* IP6-IP6 Tunnel */
+ return L.tr('IPv6-in-IPv6 tunnel');
+
+ case 776: /* IPv6-in-IPv4 */
+ return L.tr('IPv6-over-IPv4 tunnel');
+ break;
+
+ case 778: /* GRE over IP */
+ return L.tr('GRE-over-IP tunnel');
+
+ default:
+ return L.tr('Tunnel device');
+ }
+
+ case 'vlan':
+ return L.tr('VLAN %d on %s').format(this.options.vid, this.options.vsw.model);
+
+ case 'wifi':
+ var o = this.options;
+ return L.trc('(Wifi-Mode) "(SSID)" on (radioX)', '%s "%h" on %s').format(
+ o.wmode ? this.wifiModeStrings[o.wmode] : L.tr('Unknown mode'),
+ o.wssid || '?', o.wdev
+ );
+ }
+
+ return L.tr('Unknown device');
+ },
+
+ icon: function(up)
+ {
+ var kind = this.options.kind;
+
+ if (kind == 'alias')
+ kind = 'ethernet';
+
+ if (typeof(up) == 'undefined')
+ up = this.isUp();
+
+ return L.globals.resource + '/icons/%s%s.png'.format(kind, up ? '' : '_disabled');
+ },
+
+ isUp: function()
+ {
+ var l = L.network.rpcCache.devlist;
+
+ for (var i = 0; i < l.length; i++)
+ if (l[i].device == this.options.ifname)
+ return (l[i].is_up === true);
+
+ return false;
+ },
+
+ isAlias: function()
+ {
+ return (this.options.kind == 'alias');
+ },
+
+ isBridge: function()
+ {
+ return (this.options.kind == 'bridge');
+ },
+
+ isBridgeable: function()
+ {
+ return (this.options.type == 1 && this.options.kind != 'bridge');
+ },
+
+ isWireless: function()
+ {
+ return (this.options.kind == 'wifi');
+ },
+
+ isInNetwork: function(net)
+ {
+ if (!(net instanceof L.network.Interface))
+ net = L.network.getInterface(net);
+
+ if (net)
+ {
+ if (net.options.l3dev == this.options.ifname ||
+ net.options.l2dev == this.options.ifname)
+ return true;
+
+ var dev = L.network.deviceObjects[net.options.l2dev];
+ if (dev && dev.kind == 'bridge' && dev.ports)
+ return ($.inArray(this.options.ifname, dev.ports) > -1);
+ }
+
+ return false;
+ },
+
+ getMTU: function()
+ {
+ var dev = L.network.rpcCache.devstate[this.options.ifname];
+ if (dev && !isNaN(dev.mtu))
+ return dev.mtu;
+
+ return undefined;
+ },
+
+ getMACAddress: function()
+ {
+ if (this.options.type != 1)
+ return undefined;
+
+ var dev = L.network.rpcCache.devstate[this.options.ifname];
+ if (dev && dev.macaddr)
+ return dev.macaddr.toUpperCase();
+
+ return undefined;
+ },
+
+ getInterfaces: function()
+ {
+ return L.network.getInterfacesByDevice(this.options.name);
+ },
+
+ getStatistics: function()
+ {
+ var s = this.getStatus('statistics') || { };
+ return {
+ rx_bytes: (s.rx_bytes || 0),
+ tx_bytes: (s.tx_bytes || 0),
+ rx_packets: (s.rx_packets || 0),
+ tx_packets: (s.tx_packets || 0)
+ };
+ },
+
+ getTrafficHistory: function()
+ {
+ var def = new Array(120);
+
+ for (var i = 0; i < 120; i++)
+ def[i] = 0;
+
+ var h = L.network.rpcCache.bwstate[this.options.ifname] || { };
+ return {
+ rx_bytes: (h.rx_bytes || def),
+ tx_bytes: (h.tx_bytes || def),
+ rx_packets: (h.rx_packets || def),
+ tx_packets: (h.tx_packets || def)
+ };
+ },
+
+ removeFromInterface: function(iface)
+ {
+ if (!(iface instanceof L.network.Interface))
+ iface = L.network.getInterface(iface);
+
+ if (!iface)
+ return;
+
+ var ifnames = L.toArray(iface.get('ifname'));
+ if ($.inArray(this.options.ifname, ifnames) > -1)
+ iface.set('ifname', L.filterArray(ifnames, this.options.ifname));
+
+ if (this.options.kind != 'wifi')
+ return;
+
+ var networks = L.toArray(this.get('network'));
+ if ($.inArray(iface.name(), networks) > -1)
+ this.set('network', L.filterArray(networks, iface.name()));
+ },
+
+ attachToInterface: function(iface)
+ {
+ if (!(iface instanceof L.network.Interface))
+ iface = L.network.getInterface(iface);
+
+ if (!iface)
+ return;
+
+ if (this.options.kind != 'wifi')
+ {
+ var ifnames = L.toArray(iface.get('ifname'));
+ if ($.inArray(this.options.ifname, ifnames) < 0)
+ {
+ ifnames.push(this.options.ifname);
+ iface.set('ifname', (ifnames.length > 1) ? ifnames : ifnames[0]);
+ }
+ }
+ else
+ {
+ var networks = L.toArray(this.get('network'));
+ if ($.inArray(iface.name(), networks) < 0)
+ {
+ networks.push(iface.name());
+ this.set('network', (networks.length > 1) ? networks : networks[0]);
+ }
+ }
+ }
+ });
+
+ network_class.Protocol = network_class.Interface.extend({
+ description: '__unknown__',
+ tunnel: false,
+ virtual: false,
+
+ populateForm: function(section, iface)
+ {
+
+ }
+ });
+
+ return Class.extend(network_class);
+})();
diff --git a/luci2/luci2/htdocs/luci2/proto/6in4.js b/luci2/luci2/htdocs/luci2/proto/6in4.js
new file mode 100644
index 000000000..322306541
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/proto/6in4.js
@@ -0,0 +1,90 @@
+L.network.Protocol.extend({
+ protocol: '6in4',
+ description: L.tr('IPv6-in-IPv4 (RFC4213)'),
+ tunnel: true,
+ virtual: true,
+
+ populateForm: function(section, iface)
+ {
+ var wan = L.network.findWAN();
+
+ section.taboption('general', L.cbi.InputValue, 'ipaddr', {
+ caption: L.tr('Local IPv4 address'),
+ description: L.tr('Leave empty to use the current WAN address'),
+ datatype: 'ip4addr',
+ placeholder: wan ? wan.getIPv4Addrs()[0] : undefined,
+ optional: true
+ });
+
+ section.taboption('general', L.cbi.InputValue, 'peeraddr', {
+ caption: L.tr('Remote IPv4 address'),
+ description: L.tr('This is usually the address of the nearest PoP operated by the tunnel broker'),
+ datatype: 'ip4addr',
+ optional: false
+ });
+
+ section.taboption('general', L.cbi.InputValue, 'ip6addr', {
+ caption: L.tr('Local IPv6 address'),
+ description: L.tr('This is the local endpoint address assigned by the tunnel broker'),
+ datatype: 'cidr6',
+ optional: false
+ });
+
+ section.taboption('general', L.cbi.InputValue, 'ip6prefix', {
+ caption: L.tr('IPv6 routed prefix'),
+ description: L.tr('This is the prefix routed to you by the tunnel broker for use by clients'),
+ datatype: 'cidr6',
+ optional: true
+ });
+
+ var update = section.taboption('general', L.cbi.CheckboxValue, '_update', {
+ caption: L.tr('Dynamic tunnel'),
+ description: L.tr('Enable HE.net dynamic endpoint update'),
+ enabled: '1',
+ disabled: '0'
+ });
+
+ update.save = function(sid) { };
+ update.ucivalue = function(sid) {
+ var n = parseInt(this.ownerMap.get('network', sid, 'tunnelid'));
+ return !isNaN(n);
+ };
+
+ section.taboption('general', L.cbi.InputValue, 'tunnelid', {
+ caption: L.tr('Tunnel ID'),
+ datatype: 'uinteger',
+ optional: false,
+ keep: false
+ }).depends('_update', true);
+
+ section.taboption('general', L.cbi.InputValue, 'username', {
+ caption: L.tr('HE.net user ID'),
+ description: L.tr('The login name of the HE.net account'),
+ datatype: 'string',
+ optional: false,
+ keep: false
+ }).depends('_update', true);
+
+ section.taboption('general', L.cbi.PasswordValue, 'password', {
+ caption: L.tr('Password'),
+ description: L.tr('Tunnel update key or HE.net account password'),
+ optional: false,
+ keep: false
+ }).depends('_update', true);
+
+ section.taboption('advanced', L.cbi.CheckboxValue, 'defaultroute', {
+ caption: L.tr('Default route'),
+ description: L.tr('Create IPv6 default route via tunnel'),
+ optional: true,
+ initial: true
+ });
+
+ section.taboption('advanced', L.cbi.InputValue, 'ttl', {
+ caption: L.tr('Override TTL'),
+ description: L.tr('Specifies the Time-to-Live on the tunnel interface'),
+ datatype: 'range(1,255)',
+ placeholder: 64,
+ optional: true
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/proto/6rd.js b/luci2/luci2/htdocs/luci2/proto/6rd.js
new file mode 100644
index 000000000..36030f47b
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/proto/6rd.js
@@ -0,0 +1,63 @@
+L.network.Protocol.extend({
+ protocol: '6rd',
+ description: L.tr('IPv6-over-IPv4 (6rd)'),
+ tunnel: true,
+ virtual: true,
+
+ populateForm: function(section, iface)
+ {
+ var wan = L.network.findWAN();
+
+ section.taboption('general', L.cbi.InputValue, 'peeraddr', {
+ caption: L.tr('6RD Gateway'),
+ datatype: 'ip4addr',
+ optional: false
+ });
+
+ section.taboption('general', L.cbi.InputValue, 'ipaddr', {
+ caption: L.tr('Local IPv4 address'),
+ description: L.tr('Leave empty to use the current WAN address'),
+ datatype: 'ip4addr',
+ placeholder: wan ? wan.getIPv4Addrs()[0] : undefined,
+ optional: true
+ });
+
+ section.taboption('general', L.cbi.InputValue, 'ip4prefixlen', {
+ caption: L.tr('IPv4 prefix length'),
+ description: L.tr('The length of the IPv4 prefix in bits, the remainder is used in the IPv6 addresses'),
+ datatype: 'range(0, 32)',
+ placeholder: 0,
+ optional: true
+ });
+
+ section.taboption('general', L.cbi.InputValue, 'ip6prefix', {
+ caption: L.tr('IPv6 prefix'),
+ description: L.tr('The IPv6 prefix assigned to the provider, usually ends with "::"'),
+ datatype: 'ip6addr',
+ optional: false
+ });
+
+ section.taboption('general', L.cbi.InputValue, 'ip6prefixlen', {
+ caption: L.tr('IPv6 prefix length'),
+ description: L.tr('The length of the IPv6 prefix in bits'),
+ datatype: 'range(0, 128)',
+ placeholder: 16,
+ optional: true
+ });
+
+ section.taboption('advanced', L.cbi.CheckboxValue, 'defaultroute', {
+ caption: L.tr('Default route'),
+ description: L.tr('Create IPv6 default route via tunnel'),
+ optional: true,
+ initial: true
+ });
+
+ section.taboption('advanced', L.cbi.InputValue, 'ttl', {
+ caption: L.tr('Override TTL'),
+ description: L.tr('Specifies the Time-to-Live on the tunnel interface'),
+ datatype: 'range(1,255)',
+ placeholder: 64,
+ optional: true
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/proto/6to4.js b/luci2/luci2/htdocs/luci2/proto/6to4.js
new file mode 100644
index 000000000..0fa358715
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/proto/6to4.js
@@ -0,0 +1,35 @@
+L.network.Protocol.extend({
+ protocol: '6to4',
+ description: L.tr('IPv6-over-IPv4 (6to4)'),
+ tunnel: true,
+ virtual: true,
+
+ populateForm: function(section, iface)
+ {
+ section.taboption('general', L.cbi.InputValue, 'ipaddr', {
+ caption: L.tr('Local IPv4 address'),
+ description: L.tr('Leave empty to use the current WAN address'),
+ datatype: 'ip4addr',
+ optional: true
+ }).load = function() {
+ var wan = L.network.findWAN();
+ if (wan)
+ this.options.placeholder = wan.getIPv4Addrs()[0];
+ };
+
+ section.taboption('advanced', L.cbi.CheckboxValue, 'defaultroute', {
+ caption: L.tr('Default route'),
+ description: L.tr('Create IPv6 default route via tunnel'),
+ optional: true,
+ initial: true
+ });
+
+ section.taboption('advanced', L.cbi.InputValue, 'ttl', {
+ caption: L.tr('Override TTL'),
+ description: L.tr('Specifies the Time-to-Live on the tunnel interface'),
+ datatype: 'range(1,255)',
+ placeholder: 64,
+ optional: true
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/proto/dhcp.js b/luci2/luci2/htdocs/luci2/proto/dhcp.js
new file mode 100644
index 000000000..9af8c1667
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/proto/dhcp.js
@@ -0,0 +1,60 @@
+L.network.Protocol.extend({
+ protocol: 'dhcp',
+ description: L.tr('DHCP client'),
+ tunnel: false,
+ virtual: false,
+
+ populateForm: function(section, iface)
+ {
+ section.taboption('general', L.cbi.InputValue, 'hostname', {
+ caption: L.tr('Hostname'),
+ description: L.tr('Hostname to send when requesting DHCP'),
+ datatype: 'hostname',
+ optional: true
+ }).load = function() {
+ var self = this;
+ return L.system.getBoardInfo().then(function(info) {
+ self.options.placeholder = info.hostname;
+ });
+ };
+
+ section.taboption('advanced', L.cbi.CheckboxValue, 'broadcast', {
+ caption: L.tr('Use broadcast'),
+ description: L.tr('Required for certain ISPs, e.g. Charter with DOCSIS3'),
+ optional: true
+ });
+
+ section.taboption('advanced', L.cbi.CheckboxValue, 'defaultroute', {
+ caption: L.tr('Use gateway'),
+ description: L.tr('Create default route via DHCP gateway'),
+ optional: true,
+ initial: true
+ });
+
+ section.taboption('advanced', L.cbi.CheckboxValue, 'peerdns', {
+ caption: L.tr('Use DNS'),
+ description: L.tr('Use DNS servers advertised by DHCP'),
+ optional: true,
+ initial: true
+ });
+
+ section.taboption('advanced', L.cbi.DynamicList, 'dns', {
+ caption: L.tr('Custom DNS'),
+ description: L.tr('Use custom DNS servers instead of DHCP ones'),
+ datatype: 'ipaddr',
+ optional: true
+ }).depends('peerdns', false);
+
+ section.taboption('advanced', L.cbi.InputValue, 'clientid', {
+ caption: L.tr('Client ID'),
+ description: L.tr('Client ID to send when requesting DHCP'),
+ optional: true
+ });
+
+ section.taboption('advanced', L.cbi.InputValue, 'vendorid', {
+ caption: L.tr('Vendor Class'),
+ description: L.tr('Vendor Class to send when requesting DHCP'),
+ optional: true
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/proto/dhcpv6.js b/luci2/luci2/htdocs/luci2/proto/dhcpv6.js
new file mode 100644
index 000000000..48d84aa02
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/proto/dhcpv6.js
@@ -0,0 +1,59 @@
+L.network.Protocol.extend({
+ protocol: 'dhcpv6',
+ description: L.tr('DHCPv6 client / IPv6 autoconfig'),
+ tunnel: false,
+ virtual: false,
+
+ populateForm: function(section, iface)
+ {
+ section.taboption('general', L.cbi.ListValue, 'reqaddress', {
+ caption: L.tr('Request IPv6 address'),
+ initial: 'try'
+ }).value('try', L.tr('Attempt DHCPv6, fallback to RA'))
+ .value('force', L.tr('Force DHCPv6'))
+ .value('none', L.tr('RA only'));
+
+ section.taboption('general', L.cbi.ComboBox, 'reqprefix', {
+ caption: L.tr('Request IPv6 prefix'),
+ description: L.tr('Specifies the requested prefix length'),
+ initial: 'auto',
+ datatype: 'or("auto", "no", range(32, 64))'
+ }).value('auto', L.tr('automatic'))
+ .value('no', L.tr('disabled'))
+ .value('48').value('52').value('56').value('60').value('64');
+
+ section.taboption('general', L.cbi.InputValue, 'ip6prefix', {
+ caption: L.tr('Custom prefix'),
+ description: L.tr('Specifies an additional custom IPv6 prefix for distribution to clients'),
+ datatype: 'ip6addr',
+ optional: true
+ });
+
+ section.taboption('advanced', L.cbi.CheckboxValue, 'defaultroute', {
+ caption: L.tr('Default route'),
+ description: L.tr('Create IPv6 default route via tunnel'),
+ optional: true,
+ initial: true
+ });
+
+ section.taboption('advanced', L.cbi.CheckboxValue, 'peerdns', {
+ caption: L.tr('Use DNS'),
+ description: L.tr('Use DNS servers advertised by DHCPv6'),
+ optional: true,
+ initial: true
+ });
+
+ section.taboption('advanced', L.cbi.DynamicList, 'dns', {
+ caption: L.tr('Custom DNS'),
+ description: L.tr('Use custom DNS servers instead of DHCPv6 ones'),
+ datatype: 'ipaddr',
+ optional: true
+ }).depends('peerdns', false);
+
+ section.taboption('advanced', L.cbi.InputValue, 'clientid', {
+ caption: L.tr('Client ID'),
+ description: L.tr('Client ID to send when requesting DHCPv6'),
+ optional: true
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/proto/dslite.js b/luci2/luci2/htdocs/luci2/proto/dslite.js
new file mode 100644
index 000000000..84df4e266
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/proto/dslite.js
@@ -0,0 +1,46 @@
+L.network.Protocol.extend({
+ protocol: 'dslite',
+ description: L.tr('Dual-Stack Lite (RFC6333)'),
+ tunnel: true,
+ virtual: true,
+
+ populateForm: function(section, iface)
+ {
+ var wan6 = L.network.findWAN6();
+
+ section.taboption('general', L.cbi.InputValue, 'peeraddr', {
+ caption: L.tr('DS-Lite AFTR address'),
+ datatype: 'ip6addr',
+ optional: false
+ });
+
+ section.taboption('general', L.cbi.InputValue, 'ip6addr', {
+ caption: L.tr('Local IPv6 address'),
+ description: L.tr('Leave empty to use the current WAN address'),
+ datatype: 'ip6addr',
+ placeholder: wan6 ? wan6.getIPv6Addrs()[0] : undefined,
+ optional: true
+ });
+
+ section.taboption('advanced', L.cbi.NetworkList, 'tunlink', {
+ caption: L.tr('Tunnel Link'),
+ initial: wan6 ? wan6.name() : undefined,
+ optional: true
+ });
+
+ section.taboption('advanced', L.cbi.CheckboxValue, 'defaultroute', {
+ caption: L.tr('Default route'),
+ description: L.tr('Create IPv4 default route via tunnel'),
+ optional: true,
+ initial: true
+ });
+
+ section.taboption('advanced', L.cbi.InputValue, 'ttl', {
+ caption: L.tr('Override TTL'),
+ description: L.tr('Specifies the Time-to-Live on the tunnel interface'),
+ datatype: 'range(1,255)',
+ placeholder: 64,
+ optional: true
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/proto/none.js b/luci2/luci2/htdocs/luci2/proto/none.js
new file mode 100644
index 000000000..94c0f1451
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/proto/none.js
@@ -0,0 +1,6 @@
+L.network.Protocol.extend({
+ protocol: 'none',
+ description: L.tr('Unmanaged'),
+ tunnel: false,
+ virtual: false
+});
diff --git a/luci2/luci2/htdocs/luci2/proto/static.js b/luci2/luci2/htdocs/luci2/proto/static.js
new file mode 100644
index 000000000..c1697afcd
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/proto/static.js
@@ -0,0 +1,113 @@
+L.network.Protocol.extend({
+ protocol: 'static',
+ description: L.tr('Static address'),
+ tunnel: false,
+ virtual: false,
+
+ _ev_broadcast: function(ev)
+ {
+ var self = ev.data.self;
+ var sid = ev.data.sid;
+
+ var i = ($('#' + self.ownerSection.id('field', sid, 'ipaddr')).val() || '').split(/\./);
+ var m = ($('#' + self.ownerSection.id('field', sid, 'netmask') + ' select').val() || '').split(/\./);
+
+ var I = 0;
+ var M = 0;
+
+ for (var n = 0; n < 4; n++)
+ {
+ i[n] = parseInt(i[n]);
+ m[n] = parseInt(m[n]);
+
+ if (isNaN(i[n]) || i[n] < 0 || i[n] > 255 ||
+ isNaN(m[n]) || m[n] < 0 || m[n] > 255)
+ return;
+
+ I |= (i[n] << ((3 - n) * 8));
+ M |= (m[n] << ((3 - n) * 8));
+ }
+
+ var B = I | ~M;
+
+ $('#' + self.section.id('field', sid, 'broadcast'))
+ .attr('placeholder', '%d.%d.%d.%d'.format(
+ (B >> 24) & 0xFF, (B >> 16) & 0xFF,
+ (B >> 8) & 0xFF, (B >> 0) & 0xFF
+ ));
+ },
+
+ populateForm: function(section, iface)
+ {
+ var device = L.network.getDeviceByInterface(iface);
+
+ section.taboption('general', L.cbi.InputValue, 'ipaddr', {
+ caption: L.tr('IPv4 address'),
+ datatype: 'ip4addr',
+ optional: true
+ }).on('blur validate', this._ev_broadcast);
+
+ section.taboption('general', L.cbi.ComboBox, 'netmask', {
+ caption: L.tr('IPv4 netmask'),
+ datatype: 'ip4addr',
+ optional: true
+ }).on('blur validate', this._ev_broadcast)
+ .value('255.255.255.0')
+ .value('255.255.0.0')
+ .value('255.0.0.0');
+
+ section.taboption('general', L.cbi.InputValue, 'broadcast', {
+ caption: L.tr('IPv4 broadcast'),
+ datatype: 'ip4addr',
+ optional: true
+ });
+
+ section.taboption('general', L.cbi.InputValue, 'gateway', {
+ caption: L.tr('IPv4 gateway'),
+ datatype: 'ip4addr',
+ optional: true
+ });
+
+ section.taboption('general', L.cbi.DynamicList, 'dns', {
+ caption: L.tr('DNS servers'),
+ datatype: 'ipaddr',
+ optional: true
+ });
+
+
+ section.taboption('ipv6', L.cbi.ComboBox, 'ip6assign', {
+ caption: L.tr('IPv6 assignment length'),
+ description: L.tr('Assign a part of given length of every public IPv6-prefix to this interface'),
+ datatype: 'max(64)',
+ optional: true
+ }).value('', L.tr('disabled')).value('64');
+
+ var ip6hint = section.taboption('ipv6', L.cbi.InputValue, 'ip6hint', {
+ caption: L.tr('IPv6 assignment hint'),
+ description: L.tr('Assign prefix parts using this hexadecimal subprefix ID for this interface'),
+ optional: true
+ });
+
+ for (var i = 33; i <= 64; i++)
+ ip6hint.depends('ip6assign', i);
+
+ section.taboption('ipv6', L.cbi.InputValue, 'ip6addr', {
+ caption: L.tr('IPv6 address'),
+ datatype: 'ip6addr',
+ optional: true
+ }).depends('ip6assign', false);
+
+ section.taboption('ipv6', L.cbi.InputValue, 'ip6gw', {
+ caption: L.tr('IPv6 gateway'),
+ datatype: 'ip6addr',
+ optional: true
+ }).depends('ip6assign', false);
+
+ section.taboption('ipv6', L.cbi.InputValue, 'ip6prefix', {
+ caption: L.tr('IPv6 routed prefix'),
+ description: L.tr('Public prefix routed to this device for distribution to clients'),
+ datatype: 'ip6addr',
+ optional: true
+ }).depends('ip6assign', false);
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/respond.min.js b/luci2/luci2/htdocs/luci2/respond.min.js
new file mode 100644
index 000000000..e3dc2c0d6
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/respond.min.js
@@ -0,0 +1,6 @@
+/*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas. Dual MIT/BSD license */
+/*! NOTE: If you're already including a window.matchMedia polyfill via Modernizr or otherwise, you don't need this part */
+window.matchMedia=window.matchMedia||function(a){"use strict";var c,d=a.documentElement,e=d.firstElementChild||d.firstChild,f=a.createElement("body"),g=a.createElement("div");return g.id="mq-test-1",g.style.cssText="position:absolute;top:-100em",f.style.background="none",f.appendChild(g),function(a){return g.innerHTML='',d.insertBefore(f,e),c=42===g.offsetWidth,d.removeChild(f),{matches:c,media:a}}}(document);
+
+/*! Respond.js v1.3.0: min/max-width media query polyfill. (c) Scott Jehl. MIT/GPLv2 Lic. j.mp/respondjs */
+(function(a){"use strict";function x(){u(!0)}var b={};if(a.respond=b,b.update=function(){},b.mediaQueriesSupported=a.matchMedia&&a.matchMedia("only all").matches,!b.mediaQueriesSupported){var q,r,t,c=a.document,d=c.documentElement,e=[],f=[],g=[],h={},i=30,j=c.getElementsByTagName("head")[0]||d,k=c.getElementsByTagName("base")[0],l=j.getElementsByTagName("link"),m=[],n=function(){for(var b=0;l.length>b;b++){var c=l[b],d=c.href,e=c.media,f=c.rel&&"stylesheet"===c.rel.toLowerCase();d&&f&&!h[d]&&(c.styleSheet&&c.styleSheet.rawCssText?(p(c.styleSheet.rawCssText,d,e),h[d]=!0):(!/^([a-zA-Z:]*\/\/)/.test(d)&&!k||d.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&m.push({href:d,media:e}))}o()},o=function(){if(m.length){var b=m.shift();v(b.href,function(c){p(c,b.href,b.media),h[b.href]=!0,a.setTimeout(function(){o()},0)})}},p=function(a,b,c){var d=a.match(/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi),g=d&&d.length||0;b=b.substring(0,b.lastIndexOf("/"));var h=function(a){return a.replace(/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,"$1"+b+"$2$3")},i=!g&&c;b.length&&(b+="/"),i&&(g=1);for(var j=0;g>j;j++){var k,l,m,n;i?(k=c,f.push(h(a))):(k=d[j].match(/@media *([^\{]+)\{([\S\s]+?)$/)&&RegExp.$1,f.push(RegExp.$2&&h(RegExp.$2))),m=k.split(","),n=m.length;for(var o=0;n>o;o++)l=m[o],e.push({media:l.split("(")[0].match(/(only\s+)?([a-zA-Z]+)\s?/)&&RegExp.$2||"all",rules:f.length-1,hasquery:l.indexOf("(")>-1,minw:l.match(/\(\s*min\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:l.match(/\(\s*max\-width\s*:\s*(\s*[0-9\.]+)(px|em)\s*\)/)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},s=function(){var a,b=c.createElement("div"),e=c.body,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",e||(e=f=c.createElement("body"),e.style.background="none"),e.appendChild(b),d.insertBefore(e,d.firstChild),a=b.offsetWidth,f?d.removeChild(e):e.removeChild(b),a=t=parseFloat(a)},u=function(b){var h="clientWidth",k=d[h],m="CSS1Compat"===c.compatMode&&k||c.body[h]||k,n={},o=l[l.length-1],p=(new Date).getTime();if(b&&q&&i>p-q)return a.clearTimeout(r),r=a.setTimeout(u,i),void 0;q=p;for(var v in e)if(e.hasOwnProperty(v)){var w=e[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?t||s():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?t||s():1)),w.hasquery&&(z&&A||!(z||m>=x)||!(A||y>=m))||(n[w.media]||(n[w.media]=[]),n[w.media].push(f[w.rules]))}for(var C in g)g.hasOwnProperty(C)&&g[C]&&g[C].parentNode===j&&j.removeChild(g[C]);for(var D in n)if(n.hasOwnProperty(D)){var E=c.createElement("style"),F=n[D].join("\n");E.type="text/css",E.media=D,j.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(c.createTextNode(F)),g.push(E)}},v=function(a,b){var c=w();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))},w=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}();n(),b.update=n,a.addEventListener?a.addEventListener("resize",x,!1):a.attachEvent&&a.attachEvent("onresize",x)}})(this);
diff --git a/luci2/luci2/htdocs/luci2/rpc.js b/luci2/luci2/htdocs/luci2/rpc.js
new file mode 100644
index 000000000..0aa061212
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/rpc.js
@@ -0,0 +1,188 @@
+Class.extend({
+ _id: 1,
+ _batch: undefined,
+ _requests: { },
+
+ _call: function(req, cb)
+ {
+ var q = '';
+
+ if ($.isArray(req))
+ for (var i = 0; i < req.length; i++)
+ q += '%s%s.%s'.format(
+ q ? ';' : '/',
+ req[i].params[1],
+ req[i].params[2]
+ );
+ else
+ q += '/%s.%s'.format(req.params[1], req.params[2]);
+
+ return $.ajax('/ubus' + q, {
+ cache: false,
+ contentType: 'application/json',
+ data: JSON.stringify(req),
+ dataType: 'json',
+ type: 'POST',
+ timeout: L.globals.timeout,
+ _rpc_req: req
+ }).then(cb, cb);
+ },
+
+ _list_cb: function(msg)
+ {
+ var list = msg.result;
+
+ /* verify message frame */
+ if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !$.isArray(list))
+ list = [ ];
+
+ return $.Deferred().resolveWith(this, [ list ]);
+ },
+
+ _call_cb: function(msg)
+ {
+ var data = [ ];
+ var type = Object.prototype.toString;
+ var reqs = this._rpc_req;
+
+ if (!$.isArray(reqs))
+ {
+ msg = [ msg ];
+ reqs = [ reqs ];
+ }
+
+ for (var i = 0; i < msg.length; i++)
+ {
+ /* fetch related request info */
+ var req = L.rpc._requests[reqs[i].id];
+ if (typeof(req) != 'object')
+ throw 'No related request for JSON response';
+
+ /* fetch response attribute and verify returned type */
+ var ret = undefined;
+
+ /* verify message frame */
+ if (typeof(msg[i]) == 'object' && msg[i].jsonrpc == '2.0')
+ if ($.isArray(msg[i].result) && msg[i].result[0] == 0)
+ ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0];
+
+ if (req.expect)
+ {
+ for (var key in req.expect)
+ {
+ if (typeof(ret) != 'undefined' && key != '')
+ ret = ret[key];
+
+ if (typeof(ret) == 'undefined' || type.call(ret) != type.call(req.expect[key]))
+ ret = req.expect[key];
+
+ break;
+ }
+ }
+
+ /* apply filter */
+ if (typeof(req.filter) == 'function')
+ {
+ req.priv[0] = ret;
+ req.priv[1] = req.params;
+ ret = req.filter.apply(L.rpc, req.priv);
+ }
+
+ /* store response data */
+ if (typeof(req.index) == 'number')
+ data[req.index] = ret;
+ else
+ data = ret;
+
+ /* delete request object */
+ delete L.rpc._requests[reqs[i].id];
+ }
+
+ return $.Deferred().resolveWith(this, [ data ]);
+ },
+
+ list: function()
+ {
+ var params = [ ];
+ for (var i = 0; i < arguments.length; i++)
+ params[i] = arguments[i];
+
+ var msg = {
+ jsonrpc: '2.0',
+ id: this._id++,
+ method: 'list',
+ params: (params.length > 0) ? params : undefined
+ };
+
+ return this._call(msg, this._list_cb);
+ },
+
+ batch: function()
+ {
+ if (!$.isArray(this._batch))
+ this._batch = [ ];
+ },
+
+ flush: function()
+ {
+ if (!$.isArray(this._batch))
+ return L.deferrable([ ]);
+
+ var req = this._batch;
+ delete this._batch;
+
+ /* call rpc */
+ return this._call(req, this._call_cb);
+ },
+
+ declare: function(options)
+ {
+ var _rpc = this;
+
+ return function() {
+ /* build parameter object */
+ var p_off = 0;
+ var params = { };
+ if ($.isArray(options.params))
+ for (p_off = 0; p_off < options.params.length; p_off++)
+ params[options.params[p_off]] = arguments[p_off];
+
+ /* all remaining arguments are private args */
+ var priv = [ undefined, undefined ];
+ for (; p_off < arguments.length; p_off++)
+ priv.push(arguments[p_off]);
+
+ /* store request info */
+ var req = _rpc._requests[_rpc._id] = {
+ expect: options.expect,
+ filter: options.filter,
+ params: params,
+ priv: priv
+ };
+
+ /* build message object */
+ var msg = {
+ jsonrpc: '2.0',
+ id: _rpc._id++,
+ method: 'call',
+ params: [
+ L.globals.sid,
+ options.object,
+ options.method,
+ params
+ ]
+ };
+
+ /* when a batch is in progress then store index in request data
+ * and push message object onto the stack */
+ if ($.isArray(_rpc._batch))
+ {
+ req.index = _rpc._batch.push(msg) - 1;
+ return L.deferrable(msg);
+ }
+
+ /* call rpc */
+ return _rpc._call(msg, _rpc._call_cb);
+ };
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/session.js b/luci2/luci2/htdocs/luci2/session.js
new file mode 100644
index 000000000..9e5b435da
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/session.js
@@ -0,0 +1,78 @@
+Class.extend({
+ login: L.rpc.declare({
+ object: 'session',
+ method: 'login',
+ params: [ 'username', 'password' ],
+ expect: { '': { } }
+ }),
+
+ access: L.rpc.declare({
+ object: 'session',
+ method: 'access',
+ params: [ 'scope', 'object', 'function' ],
+ expect: { access: false }
+ }),
+
+ isAlive: function()
+ {
+ return L.session.access('ubus', 'session', 'access');
+ },
+
+ startHeartbeat: function()
+ {
+ this._hearbeatInterval = window.setInterval(function() {
+ L.session.isAlive().then(function(alive) {
+ if (!alive)
+ {
+ L.session.stopHeartbeat();
+ L.ui.login(true);
+ }
+
+ });
+ }, L.globals.timeout * 2);
+ },
+
+ stopHeartbeat: function()
+ {
+ if (typeof(this._hearbeatInterval) != 'undefined')
+ {
+ window.clearInterval(this._hearbeatInterval);
+ delete this._hearbeatInterval;
+ }
+ },
+
+
+ aclCache: { },
+
+ callAccess: L.rpc.declare({
+ object: 'session',
+ method: 'access',
+ expect: { '': { } }
+ }),
+
+ callAccessCallback: function(acls)
+ {
+ L.session.aclCache = acls;
+ },
+
+ updateACLs: function()
+ {
+ return L.session.callAccess()
+ .then(L.session.callAccessCallback);
+ },
+
+ hasACL: function(scope, object, func)
+ {
+ var acls = L.session.aclCache;
+
+ if (typeof(func) == 'undefined')
+ return (acls && acls[scope] && acls[scope][object]);
+
+ if (acls && acls[scope] && acls[scope][object])
+ for (var i = 0; i < acls[scope][object].length; i++)
+ if (acls[scope][object][i] == func)
+ return true;
+
+ return false;
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/system.js b/luci2/luci2/htdocs/luci2/system.js
new file mode 100644
index 000000000..f84d61f10
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/system.js
@@ -0,0 +1,82 @@
+Class.extend({
+ getSystemInfo: L.rpc.declare({
+ object: 'system',
+ method: 'info',
+ expect: { '': { } }
+ }),
+
+ getBoardInfo: L.rpc.declare({
+ object: 'system',
+ method: 'board',
+ expect: { '': { } }
+ }),
+
+ getDiskInfo: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'diskfree',
+ expect: { '': { } }
+ }),
+
+ getInfo: function(cb)
+ {
+ L.rpc.batch();
+
+ this.getSystemInfo();
+ this.getBoardInfo();
+ this.getDiskInfo();
+
+ return L.rpc.flush().then(function(info) {
+ var rv = { };
+
+ $.extend(rv, info[0]);
+ $.extend(rv, info[1]);
+ $.extend(rv, info[2]);
+
+ return rv;
+ });
+ },
+
+
+ initList: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'init_list',
+ expect: { initscripts: [ ] },
+ filter: function(data) {
+ data.sort(function(a, b) { return (a.start || 0) - (b.start || 0) });
+ return data;
+ }
+ }),
+
+ initEnabled: function(init, cb)
+ {
+ return this.initList().then(function(list) {
+ for (var i = 0; i < list.length; i++)
+ if (list[i].name == init)
+ return !!list[i].enabled;
+
+ return false;
+ });
+ },
+
+ initRun: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'init_action',
+ params: [ 'name', 'action' ],
+ filter: function(data) {
+ return (data == 0);
+ }
+ }),
+
+ initStart: function(init, cb) { return L.system.initRun(init, 'start', cb) },
+ initStop: function(init, cb) { return L.system.initRun(init, 'stop', cb) },
+ initRestart: function(init, cb) { return L.system.initRun(init, 'restart', cb) },
+ initReload: function(init, cb) { return L.system.initRun(init, 'reload', cb) },
+ initEnable: function(init, cb) { return L.system.initRun(init, 'enable', cb) },
+ initDisable: function(init, cb) { return L.system.initRun(init, 'disable', cb) },
+
+
+ performReboot: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'reboot'
+ })
+});
diff --git a/luci2/luci2/htdocs/luci2/template/network.diagnostics.htm b/luci2/luci2/htdocs/luci2/template/network.diagnostics.htm
new file mode 100644
index 000000000..a04e5a9d3
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/network.diagnostics.htm
@@ -0,0 +1,20 @@
+<%:Select the utility to run and click "Test" to perform the requested operation.%>
+
+
+
+
+
diff --git a/luci2/luci2/htdocs/luci2/template/network.hosts.htm b/luci2/luci2/htdocs/luci2/template/network.hosts.htm
new file mode 100644
index 000000000..ad19e7dab
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/network.hosts.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/template/network.interfaces.htm b/luci2/luci2/htdocs/luci2/template/network.interfaces.htm
new file mode 100644
index 000000000..ad19e7dab
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/network.interfaces.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/template/network.routes.htm b/luci2/luci2/htdocs/luci2/template/network.routes.htm
new file mode 100644
index 000000000..ad19e7dab
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/network.routes.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/template/network.switch.htm b/luci2/luci2/htdocs/luci2/template/network.switch.htm
new file mode 100644
index 000000000..ad19e7dab
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/network.switch.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/template/network.wireless.htm b/luci2/luci2/htdocs/luci2/template/network.wireless.htm
new file mode 100644
index 000000000..a16cde497
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/network.wireless.htm
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/luci2/luci2/htdocs/luci2/template/status.dmesg.htm b/luci2/luci2/htdocs/luci2/template/status.dmesg.htm
new file mode 100644
index 000000000..ae05d0303
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/status.dmesg.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/template/status.overview.htm b/luci2/luci2/htdocs/luci2/template/status.overview.htm
new file mode 100644
index 000000000..c94ab24b0
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/status.overview.htm
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/luci2/luci2/htdocs/luci2/template/status.processes.htm b/luci2/luci2/htdocs/luci2/template/status.processes.htm
new file mode 100644
index 000000000..e3af7753b
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/status.processes.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/template/status.routes.htm b/luci2/luci2/htdocs/luci2/template/status.routes.htm
new file mode 100644
index 000000000..aa35a1e31
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/status.routes.htm
@@ -0,0 +1,3 @@
+
+
+
diff --git a/luci2/luci2/htdocs/luci2/template/status.syslog.htm b/luci2/luci2/htdocs/luci2/template/status.syslog.htm
new file mode 100644
index 000000000..ae05d0303
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/status.syslog.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/template/system.admin.htm b/luci2/luci2/htdocs/luci2/template/system.admin.htm
new file mode 100644
index 000000000..ad19e7dab
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/system.admin.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/template/system.cron.htm b/luci2/luci2/htdocs/luci2/template/system.cron.htm
new file mode 100644
index 000000000..94471b077
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/system.cron.htm
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/luci2/luci2/htdocs/luci2/template/system.leds.htm b/luci2/luci2/htdocs/luci2/template/system.leds.htm
new file mode 100644
index 000000000..ad19e7dab
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/system.leds.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/template/system.software.htm b/luci2/luci2/htdocs/luci2/template/system.software.htm
new file mode 100644
index 000000000..5e27a9318
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/system.software.htm
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+ « 0 - 0
+ 0 - 0 »
+
+
+
+
+
+
+
+
+
+
diff --git a/luci2/luci2/htdocs/luci2/template/system.startup.htm b/luci2/luci2/htdocs/luci2/template/system.startup.htm
new file mode 100644
index 000000000..0d7ca804f
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/system.startup.htm
@@ -0,0 +1,21 @@
+
+
+
+
+
<%:This is the content of /etc/rc.local. Insert your own commands here (in front of 'exit 0') to execute them at the end of the boot process.%>
+
+
+
+
+ <%:Save%>
+
+
+
+
+
<%:You can enable or disable installed init scripts here. Changes will applied after a device reboot. Warning: If you disable essential init scripts like "network", your device might become inaccessible!%>
+
+
+
diff --git a/luci2/luci2/htdocs/luci2/template/system.system.htm b/luci2/luci2/htdocs/luci2/template/system.system.htm
new file mode 100644
index 000000000..ad19e7dab
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/system.system.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/template/system.upgrade.htm b/luci2/luci2/htdocs/luci2/template/system.upgrade.htm
new file mode 100644
index 000000000..e9b8d3aba
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/system.upgrade.htm
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
<%:Backup / Restore%>
+
+
+
<%:Click "Generate archive" to download a tar archive of the current configuration files. To reset the firmware to its initial state, click "Perform reset" (only possible with squashfs images).%>
+
+
+
+
+
+
<%:To restore configuration files, you can upload a previously generated backup archive here.%>
+
+
+
+
+
+
+
+
+
<%:Flash new firmware image%>
+
+
+
<%:Upload a sysupgrade-compatible image here to replace the running firmware. Check "Keep settings" to retain the current configuration (requires an OpenWrt compatible firmware image).%>
+
+
+
+
+
+
+
+
+
diff --git a/luci2/luci2/htdocs/luci2/template/system.users.htm b/luci2/luci2/htdocs/luci2/template/system.users.htm
new file mode 100644
index 000000000..ad19e7dab
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/template/system.users.htm
@@ -0,0 +1 @@
+
diff --git a/luci2/luci2/htdocs/luci2/uci.js b/luci2/luci2/htdocs/luci2/uci.js
new file mode 100644
index 000000000..c751de3ca
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/uci.js
@@ -0,0 +1,532 @@
+Class.extend({
+ init: function()
+ {
+ this.state = {
+ newidx: 0,
+ values: { },
+ creates: { },
+ changes: { },
+ deletes: { },
+ reorder: { }
+ };
+ },
+
+ callLoad: L.rpc.declare({
+ object: 'uci',
+ method: 'get',
+ params: [ 'config' ],
+ expect: { values: { } }
+ }),
+
+ callOrder: L.rpc.declare({
+ object: 'uci',
+ method: 'order',
+ params: [ 'config', 'sections' ]
+ }),
+
+ callAdd: L.rpc.declare({
+ object: 'uci',
+ method: 'add',
+ params: [ 'config', 'type', 'name', 'values' ],
+ expect: { section: '' }
+ }),
+
+ callSet: L.rpc.declare({
+ object: 'uci',
+ method: 'set',
+ params: [ 'config', 'section', 'values' ]
+ }),
+
+ callDelete: L.rpc.declare({
+ object: 'uci',
+ method: 'delete',
+ params: [ 'config', 'section', 'options' ]
+ }),
+
+ callApply: L.rpc.declare({
+ object: 'uci',
+ method: 'apply',
+ params: [ 'timeout', 'rollback' ]
+ }),
+
+ callConfirm: L.rpc.declare({
+ object: 'uci',
+ method: 'confirm'
+ }),
+
+ createSID: function(conf)
+ {
+ var v = this.state.values;
+ var n = this.state.creates;
+ var sid;
+
+ do {
+ sid = "new%06x".format(Math.random() * 0xFFFFFF);
+ } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
+
+ return sid;
+ },
+
+ reorderSections: function()
+ {
+ var v = this.state.values;
+ var n = this.state.creates;
+ var r = this.state.reorder;
+
+ if ($.isEmptyObject(r))
+ return L.deferrable();
+
+ L.rpc.batch();
+
+ /*
+ gather all created and existing sections, sort them according
+ to their index value and issue an uci order call
+ */
+ for (var c in r)
+ {
+ var o = [ ];
+
+ if (n[c])
+ for (var s in n[c])
+ o.push(n[c][s]);
+
+ for (var s in v[c])
+ o.push(v[c][s]);
+
+ if (o.length > 0)
+ {
+ o.sort(function(a, b) {
+ return (a['.index'] - b['.index']);
+ });
+
+ var sids = [ ];
+
+ for (var i = 0; i < o.length; i++)
+ sids.push(o[i]['.name']);
+
+ this.callOrder(c, sids);
+ }
+ }
+
+ this.state.reorder = { };
+ return L.rpc.flush();
+ },
+
+ load: function(packages)
+ {
+ var self = this;
+ var seen = { };
+ var pkgs = [ ];
+
+ if (!$.isArray(packages))
+ packages = [ packages ];
+
+ L.rpc.batch();
+
+ for (var i = 0; i < packages.length; i++)
+ if (!seen[packages[i]] && !self.state.values[packages[i]])
+ {
+ pkgs.push(packages[i]);
+ seen[packages[i]] = true;
+ self.callLoad(packages[i]);
+ }
+
+ return L.rpc.flush().then(function(responses) {
+ for (var i = 0; i < responses.length; i++)
+ self.state.values[pkgs[i]] = responses[i];
+
+ return pkgs;
+ });
+ },
+
+ unload: function(packages)
+ {
+ if (!$.isArray(packages))
+ packages = [ packages ];
+
+ for (var i = 0; i < packages.length; i++)
+ {
+ delete this.state.values[packages[i]];
+ delete this.state.creates[packages[i]];
+ delete this.state.changes[packages[i]];
+ delete this.state.deletes[packages[i]];
+ }
+ },
+
+ add: function(conf, type, name)
+ {
+ var n = this.state.creates;
+ var sid = name || this.createSID(conf);
+
+ if (!n[conf])
+ n[conf] = { };
+
+ n[conf][sid] = {
+ '.type': type,
+ '.name': sid,
+ '.create': name,
+ '.anonymous': !name,
+ '.index': 1000 + this.state.newidx++
+ };
+
+ return sid;
+ },
+
+ remove: function(conf, sid)
+ {
+ var n = this.state.creates;
+ var c = this.state.changes;
+ var d = this.state.deletes;
+
+ /* requested deletion of a just created section */
+ if (n[conf] && n[conf][sid])
+ {
+ delete n[conf][sid];
+ }
+ else
+ {
+ if (c[conf])
+ delete c[conf][sid];
+
+ if (!d[conf])
+ d[conf] = { };
+
+ d[conf][sid] = true;
+ }
+ },
+
+ sections: function(conf, type, cb)
+ {
+ var sa = [ ];
+ var v = this.state.values[conf];
+ var n = this.state.creates[conf];
+ var c = this.state.changes[conf];
+ var d = this.state.deletes[conf];
+
+ if (!v)
+ return sa;
+
+ for (var s in v)
+ if (!d || d[s] !== true)
+ if (!type || v[s]['.type'] == type)
+ sa.push($.extend({ }, v[s], c ? c[s] : undefined));
+
+ if (n)
+ for (var s in n)
+ if (!type || n[s]['.type'] == type)
+ sa.push(n[s]);
+
+ sa.sort(function(a, b) {
+ return a['.index'] - b['.index'];
+ });
+
+ for (var i = 0; i < sa.length; i++)
+ sa[i]['.index'] = i;
+
+ if (typeof(cb) == 'function')
+ for (var i = 0; i < sa.length; i++)
+ cb.call(this, sa[i], sa[i]['.name']);
+
+ return sa;
+ },
+
+ get: function(conf, sid, opt)
+ {
+ var v = this.state.values;
+ var n = this.state.creates;
+ var c = this.state.changes;
+ var d = this.state.deletes;
+
+ if (typeof(sid) == 'undefined')
+ return undefined;
+
+ /* requested option in a just created section */
+ if (n[conf] && n[conf][sid])
+ {
+ if (!n[conf])
+ return undefined;
+
+ if (typeof(opt) == 'undefined')
+ return n[conf][sid];
+
+ return n[conf][sid][opt];
+ }
+
+ /* requested an option value */
+ if (typeof(opt) != 'undefined')
+ {
+ /* check whether option was deleted */
+ if (d[conf] && d[conf][sid])
+ {
+ if (d[conf][sid] === true)
+ return undefined;
+
+ for (var i = 0; i < d[conf][sid].length; i++)
+ if (d[conf][sid][i] == opt)
+ return undefined;
+ }
+
+ /* check whether option was changed */
+ if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined')
+ return c[conf][sid][opt];
+
+ /* return base value */
+ if (v[conf] && v[conf][sid])
+ return v[conf][sid][opt];
+
+ return undefined;
+ }
+
+ /* requested an entire section */
+ if (v[conf])
+ return v[conf][sid];
+
+ return undefined;
+ },
+
+ set: function(conf, sid, opt, val)
+ {
+ var v = this.state.values;
+ var n = this.state.creates;
+ var c = this.state.changes;
+ var d = this.state.deletes;
+
+ if (typeof(sid) == 'undefined' ||
+ typeof(opt) == 'undefined' ||
+ opt.charAt(0) == '.')
+ return;
+
+ if (n[conf] && n[conf][sid])
+ {
+ if (typeof(val) != 'undefined')
+ n[conf][sid][opt] = val;
+ else
+ delete n[conf][sid][opt];
+ }
+ else if (typeof(val) != 'undefined' && val !== '')
+ {
+ /* do not set within deleted section */
+ if (d[conf] && d[conf][sid] === true)
+ return;
+
+ /* only set in existing sections */
+ if (!v[conf] || !v[conf][sid])
+ return;
+
+ if (!c[conf])
+ c[conf] = { };
+
+ if (!c[conf][sid])
+ c[conf][sid] = { };
+
+ /* undelete option */
+ if (d[conf] && d[conf][sid])
+ d[conf][sid] = L.filterArray(d[conf][sid], opt);
+
+ c[conf][sid][opt] = val;
+ }
+ else
+ {
+ /* only delete in existing sections */
+ if (!v[conf] || !v[conf][sid])
+ return;
+
+ if (!d[conf])
+ d[conf] = { };
+
+ if (!d[conf][sid])
+ d[conf][sid] = [ ];
+
+ if (d[conf][sid] !== true)
+ d[conf][sid].push(opt);
+ }
+ },
+
+ unset: function(conf, sid, opt)
+ {
+ return this.set(conf, sid, opt, undefined);
+ },
+
+ get_first: function(conf, type, opt)
+ {
+ var sid = undefined;
+
+ L.uci.sections(conf, type, function(s) {
+ if (typeof(sid) != 'string')
+ sid = s['.name'];
+ });
+
+ return this.get(conf, sid, opt);
+ },
+
+ set_first: function(conf, type, opt, val)
+ {
+ var sid = undefined;
+
+ L.uci.sections(conf, type, function(s) {
+ if (typeof(sid) != 'string')
+ sid = s['.name'];
+ });
+
+ return this.set(conf, sid, opt, val);
+ },
+
+ unset_first: function(conf, type, opt)
+ {
+ return this.set_first(conf, type, opt, undefined);
+ },
+
+ swap: function(conf, sid1, sid2)
+ {
+ var s1 = this.get(conf, sid1);
+ var s2 = this.get(conf, sid2);
+ var n1 = s1 ? s1['.index'] : NaN;
+ var n2 = s2 ? s2['.index'] : NaN;
+
+ if (isNaN(n1) || isNaN(n2))
+ return false;
+
+ s1['.index'] = n2;
+ s2['.index'] = n1;
+
+ this.state.reorder[conf] = true;
+
+ return true;
+ },
+
+ save: function()
+ {
+ L.rpc.batch();
+
+ var v = this.state.values;
+ var n = this.state.creates;
+ var c = this.state.changes;
+ var d = this.state.deletes;
+
+ var self = this;
+ var snew = [ ];
+ var pkgs = { };
+
+ if (n)
+ for (var conf in n)
+ {
+ for (var sid in n[conf])
+ {
+ var r = {
+ config: conf,
+ values: { }
+ };
+
+ for (var k in n[conf][sid])
+ {
+ if (k == '.type')
+ r.type = n[conf][sid][k];
+ else if (k == '.create')
+ r.name = n[conf][sid][k];
+ else if (k.charAt(0) != '.')
+ r.values[k] = n[conf][sid][k];
+ }
+
+ snew.push(n[conf][sid]);
+
+ self.callAdd(r.config, r.type, r.name, r.values);
+ }
+
+ pkgs[conf] = true;
+ }
+
+ if (c)
+ for (var conf in c)
+ {
+ for (var sid in c[conf])
+ self.callSet(conf, sid, c[conf][sid]);
+
+ pkgs[conf] = true;
+ }
+
+ if (d)
+ for (var conf in d)
+ {
+ for (var sid in d[conf])
+ {
+ var o = d[conf][sid];
+ self.callDelete(conf, sid, (o === true) ? undefined : o);
+ }
+
+ pkgs[conf] = true;
+ }
+
+ return L.rpc.flush().then(function(responses) {
+ /*
+ array "snew" holds references to the created uci sections,
+ use it to assign the returned names of the new sections
+ */
+ for (var i = 0; i < snew.length; i++)
+ snew[i]['.name'] = responses[i];
+
+ return self.reorderSections();
+ }).then(function() {
+ pkgs = L.toArray(pkgs);
+
+ self.unload(pkgs);
+
+ return self.load(pkgs);
+ });
+ },
+
+ apply: function(timeout)
+ {
+ var self = this;
+ var date = new Date();
+ var deferred = $.Deferred();
+
+ if (typeof(timeout) != 'number' || timeout < 1)
+ timeout = 10;
+
+ self.callApply(timeout, true).then(function(rv) {
+ if (rv != 0)
+ {
+ deferred.rejectWith(self, [ rv ]);
+ return;
+ }
+
+ var try_deadline = date.getTime() + 1000 * timeout;
+ var try_confirm = function()
+ {
+ return self.callConfirm().then(function(rv) {
+ if (rv != 0)
+ {
+ if (date.getTime() < try_deadline)
+ window.setTimeout(try_confirm, 250);
+ else
+ deferred.rejectWith(self, [ rv ]);
+
+ return;
+ }
+
+ deferred.resolveWith(self, [ rv ]);
+ });
+ };
+
+ window.setTimeout(try_confirm, 1000);
+ });
+
+ return deferred;
+ },
+
+ changes: L.rpc.declare({
+ object: 'uci',
+ method: 'changes',
+ expect: { changes: { } }
+ }),
+
+ readable: function(conf)
+ {
+ return L.session.hasACL('uci', conf, 'read');
+ },
+
+ writable: function(conf)
+ {
+ return L.session.hasACL('uci', conf, 'write');
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/ui.js b/luci2/luci2/htdocs/luci2/ui.js
new file mode 100644
index 000000000..af2bf320b
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/ui.js
@@ -0,0 +1,1580 @@
+(function() {
+ var ui_class = {
+ saveScrollTop: function()
+ {
+ this._scroll_top = $(document).scrollTop();
+ },
+
+ restoreScrollTop: function()
+ {
+ if (typeof(this._scroll_top) == 'undefined')
+ return;
+
+ $(document).scrollTop(this._scroll_top);
+
+ delete this._scroll_top;
+ },
+
+ loading: function(enable)
+ {
+ var win = $(window);
+ var body = $('body');
+
+ var state = this._loading || (this._loading = {
+ modal: $('
')
+ .css('z-index', 2000)
+ .addClass('modal fade')
+ .append($('
')
+ .addClass('modal-dialog')
+ .append($('
')
+ .addClass('modal-content luci2-modal-loader')
+ .append($('
')
+ .addClass('modal-body')
+ .text(L.tr('Loading data…')))))
+ .appendTo(body)
+ .modal({
+ backdrop: 'static',
+ keyboard: false
+ })
+ });
+
+ state.modal.modal(enable ? 'show' : 'hide');
+ },
+
+ dialog: function(title, content, options)
+ {
+ var win = $(window);
+ var body = $('body');
+ var self = this;
+
+ var state = this._dialog || (this._dialog = {
+ dialog: $('
')
+ .addClass('modal fade')
+ .append($('
')
+ .addClass('modal-dialog')
+ .append($('
')
+ .addClass('modal-content')
+ .append($('
')
+ .addClass('modal-header')
+ .append(' ')
+ .addClass('modal-title'))
+ .append($('
')
+ .addClass('modal-body'))
+ .append($('
')
+ .addClass('modal-footer')
+ .append(self.button(L.tr('Close'), 'primary')
+ .click(function() {
+ $(this).parents('div.modal').modal('hide');
+ })))))
+ .appendTo(body)
+ });
+
+ if (typeof(options) != 'object')
+ options = { };
+
+ if (title === false)
+ {
+ state.dialog.modal('hide');
+
+ return state.dialog;
+ }
+
+ var cnt = state.dialog.children().children().children('div.modal-body');
+ var ftr = state.dialog.children().children().children('div.modal-footer');
+
+ ftr.empty().show();
+
+ if (options.style == 'confirm')
+ {
+ ftr.append(L.ui.button(L.tr('Ok'), 'primary')
+ .click(options.confirm || function() { L.ui.dialog(false) }));
+
+ ftr.append(L.ui.button(L.tr('Cancel'), 'default')
+ .click(options.cancel || function() { L.ui.dialog(false) }));
+ }
+ else if (options.style == 'close')
+ {
+ ftr.append(L.ui.button(L.tr('Close'), 'primary')
+ .click(options.close || function() { L.ui.dialog(false) }));
+ }
+ else if (options.style == 'wait')
+ {
+ ftr.append(L.ui.button(L.tr('Close'), 'primary')
+ .attr('disabled', true));
+ }
+
+ if (options.wide)
+ {
+ state.dialog.addClass('wide');
+ }
+ else
+ {
+ state.dialog.removeClass('wide');
+ }
+
+ state.dialog.find('h4:first').text(title);
+ state.dialog.modal('show');
+
+ cnt.empty().append(content);
+
+ return state.dialog;
+ },
+
+ upload: function(title, content, options)
+ {
+ var state = L.ui._upload || (L.ui._upload = {
+ form: $('')
+ .attr('method', 'post')
+ .attr('action', '/cgi-bin/luci-upload')
+ .attr('enctype', 'multipart/form-data')
+ .attr('target', 'cbi-fileupload-frame')
+ .append($('
'))
+ .append($(' ')
+ .attr('type', 'hidden')
+ .attr('name', 'sessionid'))
+ .append($(' ')
+ .attr('type', 'hidden')
+ .attr('name', 'filename'))
+ .append($(' ')
+ .attr('type', 'file')
+ .attr('name', 'filedata')
+ .addClass('cbi-input-file'))
+ .append($('
')
+ .css('width', '100%')
+ .addClass('progress progress-striped active')
+ .append($('
')
+ .addClass('progress-bar')
+ .css('width', '100%')))
+ .append($('')
+ .addClass('pull-right')
+ .attr('name', 'cbi-fileupload-frame')
+ .css('width', '1px')
+ .css('height', '1px')
+ .css('visibility', 'hidden')),
+
+ finish_cb: function(ev) {
+ $(this).off('load');
+
+ var body = (this.contentDocument || this.contentWindow.document).body;
+ if (body.firstChild.tagName.toLowerCase() == 'pre')
+ body = body.firstChild;
+
+ var json;
+ try {
+ json = $.parseJSON(body.innerHTML);
+ } catch(e) {
+ json = {
+ message: L.tr('Invalid server response received'),
+ error: [ -1, L.tr('Invalid data') ]
+ };
+ };
+
+ if (json.error)
+ {
+ L.ui.dialog(L.tr('File upload'), [
+ $('
').text(L.tr('The file upload failed with the server response below:')),
+ $(' ').addClass('alert-message').text(json.message || json.error[1]),
+ $('
').text(L.tr('In case of network problems try uploading the file again.'))
+ ], { style: 'close' });
+ }
+ else if (typeof(state.success_cb) == 'function')
+ {
+ state.success_cb(json);
+ }
+ },
+
+ confirm_cb: function() {
+ var f = state.form.find('.cbi-input-file');
+ var b = state.form.find('.progress');
+ var p = state.form.find('p');
+
+ if (!f.val())
+ return;
+
+ state.form.find('iframe').on('load', state.finish_cb);
+ state.form.submit();
+
+ f.hide();
+ b.show();
+ p.text(L.tr('File upload in progress …'));
+
+ state.form.parent().parent().find('button').prop('disabled', true);
+ }
+ });
+
+ state.form.find('.progress').hide();
+ state.form.find('.cbi-input-file').val('').show();
+ state.form.find('p').text(content || L.tr('Select the file to upload and press "%s" to proceed.').format(L.tr('Ok')));
+
+ state.form.find('[name=sessionid]').val(L.globals.sid);
+ state.form.find('[name=filename]').val(options.filename);
+
+ state.success_cb = options.success;
+
+ L.ui.dialog(title || L.tr('File upload'), state.form, {
+ style: 'confirm',
+ confirm: state.confirm_cb
+ });
+ },
+
+ reconnect: function()
+ {
+ var protocols = (location.protocol == 'https:') ? [ 'http', 'https' ] : [ 'http' ];
+ var ports = (location.protocol == 'https:') ? [ 80, location.port || 443 ] : [ location.port || 80 ];
+ var address = location.hostname.match(/^[A-Fa-f0-9]*:[A-Fa-f0-9:]+$/) ? '[' + location.hostname + ']' : location.hostname;
+ var images = $();
+ var interval, timeout;
+
+ L.ui.dialog(
+ L.tr('Waiting for device'), [
+ $('
').text(L.tr('Please stand by while the device is reconfiguring …')),
+ $('
')
+ .css('width', '100%')
+ .addClass('progressbar')
+ .addClass('intermediate')
+ .append($('
')
+ .css('width', '100%'))
+ ], { style: 'wait' }
+ );
+
+ for (var i = 0; i < protocols.length; i++)
+ images = images.add($(' ').attr('url', protocols[i] + '://' + address + ':' + ports[i]));
+
+ //L.network.getNetworkStatus(function(s) {
+ // for (var i = 0; i < protocols.length; i++)
+ // {
+ // for (var j = 0; j < s.length; j++)
+ // {
+ // for (var k = 0; k < s[j]['ipv4-address'].length; k++)
+ // images = images.add($(' ').attr('url', protocols[i] + '://' + s[j]['ipv4-address'][k].address + ':' + ports[i]));
+ //
+ // for (var l = 0; l < s[j]['ipv6-address'].length; l++)
+ // images = images.add($(' ').attr('url', protocols[i] + '://[' + s[j]['ipv6-address'][l].address + ']:' + ports[i]));
+ // }
+ // }
+ //}).then(function() {
+ images.on('load', function() {
+ var url = this.getAttribute('url');
+ L.session.isAlive().then(function(access) {
+ if (access)
+ {
+ window.clearTimeout(timeout);
+ window.clearInterval(interval);
+ L.ui.dialog(false);
+ images = null;
+ }
+ else
+ {
+ location.href = url;
+ }
+ });
+ });
+
+ interval = window.setInterval(function() {
+ images.each(function() {
+ this.setAttribute('src', this.getAttribute('url') + L.globals.resource + '/icons/loading.gif?r=' + Math.random());
+ });
+ }, 5000);
+
+ timeout = window.setTimeout(function() {
+ window.clearInterval(interval);
+ images.off('load');
+
+ L.ui.dialog(
+ L.tr('Device not responding'),
+ L.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'),
+ { style: 'close' }
+ );
+ }, 180000);
+ //});
+ },
+
+ login: function(invalid)
+ {
+ var state = L.ui._login || (L.ui._login = {
+ form: $('')
+ .attr('target', '')
+ .attr('method', 'post')
+ .append($('
')
+ .addClass('alert alert-danger')
+ .text(L.tr('Wrong username or password given!')))
+ .append($('
')
+ .append($(' ')
+ .text(L.tr('Username'))
+ .append($(' '))
+ .append($(' ')
+ .attr('type', 'text')
+ .attr('name', 'username')
+ .attr('value', 'root')
+ .addClass('form-control')
+ .keypress(function(ev) {
+ if (ev.which == 10 || ev.which == 13)
+ state.confirm_cb();
+ }))))
+ .append($('
')
+ .append($(' ')
+ .text(L.tr('Password'))
+ .append($(' '))
+ .append($(' ')
+ .attr('type', 'password')
+ .attr('name', 'password')
+ .addClass('form-control')
+ .keypress(function(ev) {
+ if (ev.which == 10 || ev.which == 13)
+ state.confirm_cb();
+ }))))
+ .append($('
')
+ .text(L.tr('Enter your username and password above, then click "%s" to proceed.').format(L.tr('Ok')))),
+
+ response_cb: function(response) {
+ if (!response.ubus_rpc_session)
+ {
+ L.ui.login(true);
+ }
+ else
+ {
+ L.globals.sid = response.ubus_rpc_session;
+ L.setHash('id', L.globals.sid);
+ L.session.startHeartbeat();
+ L.ui.dialog(false);
+ state.deferred.resolve();
+ }
+ },
+
+ confirm_cb: function() {
+ var u = state.form.find('[name=username]').val();
+ var p = state.form.find('[name=password]').val();
+
+ if (!u)
+ return;
+
+ L.ui.dialog(
+ L.tr('Logging in'), [
+ $('
').text(L.tr('Log in in progress …')),
+ $('
')
+ .css('width', '100%')
+ .addClass('progressbar')
+ .addClass('intermediate')
+ .append($('
')
+ .css('width', '100%'))
+ ], { style: 'wait' }
+ );
+
+ L.globals.sid = '00000000000000000000000000000000';
+ L.session.login(u, p).then(state.response_cb);
+ }
+ });
+
+ if (!state.deferred || state.deferred.state() != 'pending')
+ state.deferred = $.Deferred();
+
+ /* try to find sid from hash */
+ var sid = L.getHash('id');
+ if (sid && sid.match(/^[a-f0-9]{32}$/))
+ {
+ L.globals.sid = sid;
+ L.session.isAlive().then(function(access) {
+ if (access)
+ {
+ L.session.startHeartbeat();
+ state.deferred.resolve();
+ }
+ else
+ {
+ L.setHash('id', undefined);
+ L.ui.login();
+ }
+ });
+
+ return state.deferred;
+ }
+
+ if (invalid)
+ state.form.find('.alert').show();
+ else
+ state.form.find('.alert').hide();
+
+ L.ui.dialog(L.tr('Authorization Required'), state.form, {
+ style: 'confirm',
+ confirm: state.confirm_cb
+ });
+
+ state.form.find('[name=password]').focus();
+
+ return state.deferred;
+ },
+
+ cryptPassword: L.rpc.declare({
+ object: 'luci2.ui',
+ method: 'crypt',
+ params: [ 'data' ],
+ expect: { crypt: '' }
+ }),
+
+
+ mergeACLScope: function(acl_scope, scope)
+ {
+ if ($.isArray(scope))
+ {
+ for (var i = 0; i < scope.length; i++)
+ acl_scope[scope[i]] = true;
+ }
+ else if ($.isPlainObject(scope))
+ {
+ for (var object_name in scope)
+ {
+ if (!$.isArray(scope[object_name]))
+ continue;
+
+ var acl_object = acl_scope[object_name] || (acl_scope[object_name] = { });
+
+ for (var i = 0; i < scope[object_name].length; i++)
+ acl_object[scope[object_name][i]] = true;
+ }
+ }
+ },
+
+ mergeACLPermission: function(acl_perm, perm)
+ {
+ if ($.isPlainObject(perm))
+ {
+ for (var scope_name in perm)
+ {
+ var acl_scope = acl_perm[scope_name] || (acl_perm[scope_name] = { });
+ L.ui.mergeACLScope(acl_scope, perm[scope_name]);
+ }
+ }
+ },
+
+ mergeACLGroup: function(acl_group, group)
+ {
+ if ($.isPlainObject(group))
+ {
+ if (!acl_group.description)
+ acl_group.description = group.description;
+
+ if (group.read)
+ {
+ var acl_perm = acl_group.read || (acl_group.read = { });
+ L.ui.mergeACLPermission(acl_perm, group.read);
+ }
+
+ if (group.write)
+ {
+ var acl_perm = acl_group.write || (acl_group.write = { });
+ L.ui.mergeACLPermission(acl_perm, group.write);
+ }
+ }
+ },
+
+ callACLsCallback: function(trees)
+ {
+ var acl_tree = { };
+
+ for (var i = 0; i < trees.length; i++)
+ {
+ if (!$.isPlainObject(trees[i]))
+ continue;
+
+ for (var group_name in trees[i])
+ {
+ var acl_group = acl_tree[group_name] || (acl_tree[group_name] = { });
+ L.ui.mergeACLGroup(acl_group, trees[i][group_name]);
+ }
+ }
+
+ return acl_tree;
+ },
+
+ callACLs: L.rpc.declare({
+ object: 'luci2.ui',
+ method: 'acls',
+ expect: { acls: [ ] }
+ }),
+
+ getAvailableACLs: function()
+ {
+ return this.callACLs().then(this.callACLsCallback);
+ },
+
+ renderChangeIndicator: function()
+ {
+ return $('')
+ .addClass('nav navbar-nav navbar-right')
+ .append($(' ')
+ .append($(' ')
+ .attr('id', 'changes')
+ .attr('href', '#')
+ .append($(' ')
+ .addClass('label label-info'))));
+ },
+
+ callMenuCallback: function(entries)
+ {
+ L.globals.mainMenu = new L.ui.menu();
+ L.globals.mainMenu.entries(entries);
+
+ $('#mainmenu')
+ .empty()
+ .append(L.globals.mainMenu.render(0, 1))
+ .append(L.ui.renderChangeIndicator());
+ },
+
+ callMenu: L.rpc.declare({
+ object: 'luci2.ui',
+ method: 'menu',
+ expect: { menu: { } }
+ }),
+
+ renderMainMenu: function()
+ {
+ return this.callMenu().then(this.callMenuCallback);
+ },
+
+ renderViewMenu: function()
+ {
+ $('#viewmenu')
+ .empty()
+ .append(L.globals.mainMenu.render(2, 900));
+ },
+
+ renderView: function()
+ {
+ var node = arguments[0];
+ var name = node.view.split(/\//).join('.');
+ var cname = L.toClassName(name);
+ var views = L.views || (L.views = { });
+ var args = [ ];
+
+ for (var i = 1; i < arguments.length; i++)
+ args.push(arguments[i]);
+
+ if (L.globals.currentView)
+ L.globals.currentView.finish();
+
+ L.ui.renderViewMenu();
+ L.setHash('view', node.view);
+
+ if (views[cname] instanceof L.ui.view)
+ {
+ L.globals.currentView = views[cname];
+ return views[cname].render.apply(views[cname], args);
+ }
+
+ var url = L.globals.resource + '/view/' + name + '.js';
+
+ return $.ajax(url, {
+ method: 'GET',
+ cache: true,
+ dataType: 'text'
+ }).then(function(data) {
+ try {
+ var viewConstructorSource = (
+ '(function(L, $) { ' +
+ 'return %s' +
+ '})(L, $);\n\n' +
+ '//@ sourceURL=%s'
+ ).format(data, url);
+
+ var viewConstructor = eval(viewConstructorSource);
+
+ views[cname] = new viewConstructor({
+ name: name,
+ acls: node.write || { }
+ });
+
+ L.globals.currentView = views[cname];
+ return views[cname].render.apply(views[cname], args);
+ }
+ catch(e) {
+ alert('Unable to instantiate view "%s": %s'.format(url, e));
+ };
+
+ return $.Deferred().resolve();
+ });
+ },
+
+ changeView: function()
+ {
+ var name = L.getHash('view');
+ var node = L.globals.defaultNode;
+
+ if (name && L.globals.mainMenu)
+ node = L.globals.mainMenu.getNode(name);
+
+ if (node)
+ {
+ L.ui.loading(true);
+ L.ui.renderView(node).then(function() {
+ $('#mainmenu.in').collapse('hide');
+ L.ui.loading(false);
+ });
+ }
+ },
+
+ updateHostname: function()
+ {
+ return L.system.getBoardInfo().then(function(info) {
+ if (info.hostname)
+ $('#hostname').text(info.hostname);
+ });
+ },
+
+ updateChanges: function()
+ {
+ return L.uci.changes().then(function(changes) {
+ var n = 0;
+ var html = '';
+
+ for (var config in changes)
+ {
+ var log = [ ];
+
+ for (var i = 0; i < changes[config].length; i++)
+ {
+ var c = changes[config][i];
+
+ switch (c[0])
+ {
+ case 'order':
+ log.push('uci reorder %s.%s=%s '.format(config, c[1], c[2]));
+ break;
+
+ case 'remove':
+ if (c.length < 3)
+ log.push('uci delete %s.%s'.format(config, c[1]));
+ else
+ log.push('uci delete %s.%s.%s'.format(config, c[1], c[2]));
+ break;
+
+ case 'rename':
+ if (c.length < 4)
+ log.push('uci rename %s.%s=%s '.format(config, c[1], c[2], c[3]));
+ else
+ log.push('uci rename %s.%s.%s=%s '.format(config, c[1], c[2], c[3], c[4]));
+ break;
+
+ case 'add':
+ log.push('uci add %s %s (= %s )'.format(config, c[2], c[1]));
+ break;
+
+ case 'list-add':
+ log.push('uci add_list %s.%s.%s=%s '.format(config, c[1], c[2], c[3], c[4]));
+ break;
+
+ case 'list-del':
+ log.push('uci del_list %s.%s.%s=%s '.format(config, c[1], c[2], c[3], c[4]));
+ break;
+
+ case 'set':
+ if (c.length < 4)
+ log.push('uci set %s.%s=%s '.format(config, c[1], c[2]));
+ else
+ log.push('uci set %s.%s.%s=%s '.format(config, c[1], c[2], c[3], c[4]));
+ break;
+ }
+ }
+
+ html += '/etc/config/%s%s '.format(config, log.join('\n'));
+ n += changes[config].length;
+ }
+
+ if (n > 0)
+ $('#changes')
+ .click(function(ev) {
+ L.ui.dialog(L.tr('Staged configuration changes'), html, {
+ style: 'confirm',
+ confirm: function() {
+ L.uci.apply().then(
+ function(code) { alert('Success with code ' + code); },
+ function(code) { alert('Error with code ' + code); }
+ );
+ }
+ });
+ ev.preventDefault();
+ })
+ .children('span')
+ .show()
+ .text(L.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n));
+ else
+ $('#changes').children('span').hide();
+ });
+ },
+
+ load: function()
+ {
+ var self = this;
+
+ self.loading(true);
+
+ $.when(
+ L.session.updateACLs(),
+ self.updateHostname(),
+ self.updateChanges(),
+ self.renderMainMenu(),
+ L.network.load()
+ ).then(function() {
+ self.renderView(L.globals.defaultNode).then(function() {
+ self.loading(false);
+ });
+
+ $(window).on('hashchange', function() {
+ self.changeView();
+ });
+ });
+ },
+
+ button: function(label, style, title)
+ {
+ style = style || 'default';
+
+ return $(' ')
+ .attr('type', 'button')
+ .attr('title', title ? title : '')
+ .addClass('btn btn-' + style)
+ .text(label);
+ },
+
+ icon: function(src, alt, title)
+ {
+ if (!src.match(/\.[a-z]+$/))
+ src += '.png';
+
+ if (!src.match(/^\//))
+ src = L.globals.resource + '/icons/' + src;
+
+ var icon = $(' ')
+ .attr('src', src);
+
+ if (typeof(alt) !== 'undefined')
+ icon.attr('alt', alt);
+
+ if (typeof(title) !== 'undefined')
+ icon.attr('title', title);
+
+ return icon;
+ }
+ };
+
+ ui_class.AbstractWidget = Class.extend({
+ i18n: function(text) {
+ return text;
+ },
+
+ label: function() {
+ var key = arguments[0];
+ var args = [ ];
+
+ for (var i = 1; i < arguments.length; i++)
+ args.push(arguments[i]);
+
+ switch (typeof(this.options[key]))
+ {
+ case 'undefined':
+ return '';
+
+ case 'function':
+ return this.options[key].apply(this, args);
+
+ default:
+ return ''.format.apply('' + this.options[key], args);
+ }
+ },
+
+ toString: function() {
+ return $('
').append(this.render()).html();
+ },
+
+ insertInto: function(id) {
+ return $(id).empty().append(this.render());
+ },
+
+ appendTo: function(id) {
+ return $(id).append(this.render());
+ },
+
+ on: function(evname, evfunc)
+ {
+ var evnames = L.toArray(evname);
+
+ if (!this.events)
+ this.events = { };
+
+ for (var i = 0; i < evnames.length; i++)
+ this.events[evnames[i]] = evfunc;
+
+ return this;
+ },
+
+ trigger: function(evname, evdata)
+ {
+ if (this.events)
+ {
+ var evnames = L.toArray(evname);
+
+ for (var i = 0; i < evnames.length; i++)
+ if (this.events[evnames[i]])
+ this.events[evnames[i]].call(this, evdata);
+ }
+
+ return this;
+ }
+ });
+
+ ui_class.view = ui_class.AbstractWidget.extend({
+ _fetch_template: function()
+ {
+ return $.ajax(L.globals.resource + '/template/' + this.options.name + '.htm', {
+ method: 'GET',
+ cache: true,
+ dataType: 'text',
+ success: function(data) {
+ data = data.replace(/<%([#:=])?(.+?)%>/g, function(match, p1, p2) {
+ p2 = p2.replace(/^\s+/, '').replace(/\s+$/, '');
+ switch (p1)
+ {
+ case '#':
+ return '';
+
+ case ':':
+ return L.tr(p2);
+
+ case '=':
+ return L.globals[p2] || '';
+
+ default:
+ return '(?' + match + ')';
+ }
+ });
+
+ $('#maincontent').append(data);
+ }
+ });
+ },
+
+ execute: function()
+ {
+ throw "Not implemented";
+ },
+
+ render: function()
+ {
+ var container = $('#maincontent');
+
+ container.empty();
+
+ if (this.title)
+ container.append($(' ').append(this.title));
+
+ if (this.description)
+ container.append($('
').append(this.description));
+
+ var self = this;
+ var args = [ ];
+
+ for (var i = 0; i < arguments.length; i++)
+ args.push(arguments[i]);
+
+ return this._fetch_template().then(function() {
+ return L.deferrable(self.execute.apply(self, args));
+ });
+ },
+
+ repeat: function(func, interval)
+ {
+ var self = this;
+
+ if (!self._timeouts)
+ self._timeouts = [ ];
+
+ var index = self._timeouts.length;
+
+ if (typeof(interval) != 'number')
+ interval = 5000;
+
+ var setTimer, runTimer;
+
+ setTimer = function() {
+ if (self._timeouts)
+ self._timeouts[index] = window.setTimeout(runTimer, interval);
+ };
+
+ runTimer = function() {
+ L.deferrable(func.call(self)).then(setTimer, setTimer);
+ };
+
+ runTimer();
+ },
+
+ finish: function()
+ {
+ if ($.isArray(this._timeouts))
+ {
+ for (var i = 0; i < this._timeouts.length; i++)
+ window.clearTimeout(this._timeouts[i]);
+
+ delete this._timeouts;
+ }
+ }
+ });
+
+ ui_class.menu = ui_class.AbstractWidget.extend({
+ init: function() {
+ this._nodes = { };
+ },
+
+ entries: function(entries)
+ {
+ for (var entry in entries)
+ {
+ var path = entry.split(/\//);
+ var node = this._nodes;
+
+ for (i = 0; i < path.length; i++)
+ {
+ if (!node.childs)
+ node.childs = { };
+
+ if (!node.childs[path[i]])
+ node.childs[path[i]] = { };
+
+ node = node.childs[path[i]];
+ }
+
+ $.extend(node, entries[entry]);
+ }
+ },
+
+ sortNodesCallback: function(a, b)
+ {
+ var x = a.index || 0;
+ var y = b.index || 0;
+ return (x - y);
+ },
+
+ firstChildView: function(node)
+ {
+ if (node.view)
+ return node;
+
+ var nodes = [ ];
+ for (var child in (node.childs || { }))
+ nodes.push(node.childs[child]);
+
+ nodes.sort(this.sortNodesCallback);
+
+ for (var i = 0; i < nodes.length; i++)
+ {
+ var child = this.firstChildView(nodes[i]);
+ if (child)
+ {
+ for (var key in child)
+ if (!node.hasOwnProperty(key) && child.hasOwnProperty(key))
+ node[key] = child[key];
+
+ return node;
+ }
+ }
+
+ return undefined;
+ },
+
+ handleClick: function(ev)
+ {
+ L.setHash('view', ev.data);
+
+ ev.preventDefault();
+ this.blur();
+ },
+
+ renderNodes: function(childs, level, min, max)
+ {
+ var nodes = [ ];
+ for (var node in childs)
+ {
+ var child = this.firstChildView(childs[node]);
+ if (child)
+ nodes.push(childs[node]);
+ }
+
+ nodes.sort(this.sortNodesCallback);
+
+ var list = $('');
+
+ if (level == 0)
+ list.addClass('nav').addClass('navbar-nav');
+ else if (level == 1)
+ list.addClass('dropdown-menu').addClass('navbar-inverse');
+
+ for (var i = 0; i < nodes.length; i++)
+ {
+ if (!L.globals.defaultNode)
+ {
+ var v = L.getHash('view');
+ if (!v || v == nodes[i].view)
+ L.globals.defaultNode = nodes[i];
+ }
+
+ var item = $(' ')
+ .append($(' ')
+ .attr('href', '#')
+ .text(L.tr(nodes[i].title)))
+ .appendTo(list);
+
+ if (nodes[i].childs && level < max)
+ {
+ item.addClass('dropdown');
+
+ item.find('a')
+ .addClass('dropdown-toggle')
+ .attr('data-toggle', 'dropdown')
+ .append(' ');
+
+ item.append(this.renderNodes(nodes[i].childs, level + 1));
+ }
+ else
+ {
+ item.find('a').click(nodes[i].view, this.handleClick);
+ }
+ }
+
+ return list.get(0);
+ },
+
+ render: function(min, max)
+ {
+ var top = min ? this.getNode(L.globals.defaultNode.view, min) : this._nodes;
+ return this.renderNodes(top.childs, 0, min, max);
+ },
+
+ getNode: function(path, max)
+ {
+ var p = path.split(/\//);
+ var n = this._nodes;
+
+ if (typeof(max) == 'undefined')
+ max = p.length;
+
+ for (var i = 0; i < max; i++)
+ {
+ if (!n.childs[p[i]])
+ return undefined;
+
+ n = n.childs[p[i]];
+ }
+
+ return n;
+ }
+ });
+
+ ui_class.table = ui_class.AbstractWidget.extend({
+ init: function()
+ {
+ this._rows = [ ];
+ },
+
+ row: function(values)
+ {
+ if ($.isArray(values))
+ {
+ this._rows.push(values);
+ }
+ else if ($.isPlainObject(values))
+ {
+ var v = [ ];
+ for (var i = 0; i < this.options.columns.length; i++)
+ {
+ var col = this.options.columns[i];
+
+ if (typeof col.key == 'string')
+ v.push(values[col.key]);
+ else
+ v.push(null);
+ }
+ this._rows.push(v);
+ }
+ },
+
+ rows: function(rows)
+ {
+ for (var i = 0; i < rows.length; i++)
+ this.row(rows[i]);
+ },
+
+ render: function(id)
+ {
+ var fieldset = document.createElement('fieldset');
+ fieldset.className = 'cbi-section';
+
+ if (this.options.caption)
+ {
+ var legend = document.createElement('legend');
+ $(legend).append(this.options.caption);
+ fieldset.appendChild(legend);
+ }
+
+ var table = document.createElement('table');
+ table.className = 'table table-condensed table-hover';
+
+ var has_caption = false;
+ var has_description = false;
+
+ for (var i = 0; i < this.options.columns.length; i++)
+ if (this.options.columns[i].caption)
+ {
+ has_caption = true;
+ break;
+ }
+ else if (this.options.columns[i].description)
+ {
+ has_description = true;
+ break;
+ }
+
+ if (has_caption)
+ {
+ var tr = table.insertRow(-1);
+ tr.className = 'cbi-section-table-titles';
+
+ for (var i = 0; i < this.options.columns.length; i++)
+ {
+ var col = this.options.columns[i];
+ var th = document.createElement('th');
+ th.className = 'cbi-section-table-cell';
+
+ tr.appendChild(th);
+
+ if (col.width)
+ th.style.width = col.width;
+
+ if (col.align)
+ th.style.textAlign = col.align;
+
+ if (col.caption)
+ $(th).append(col.caption);
+ }
+ }
+
+ if (has_description)
+ {
+ var tr = table.insertRow(-1);
+ tr.className = 'cbi-section-table-descr';
+
+ for (var i = 0; i < this.options.columns.length; i++)
+ {
+ var col = this.options.columns[i];
+ var th = document.createElement('th');
+ th.className = 'cbi-section-table-cell';
+
+ tr.appendChild(th);
+
+ if (col.width)
+ th.style.width = col.width;
+
+ if (col.align)
+ th.style.textAlign = col.align;
+
+ if (col.description)
+ $(th).append(col.description);
+ }
+ }
+
+ if (this._rows.length == 0)
+ {
+ if (this.options.placeholder)
+ {
+ var tr = table.insertRow(-1);
+ var td = tr.insertCell(-1);
+ td.className = 'cbi-section-table-cell';
+
+ td.colSpan = this.options.columns.length;
+ $(td).append(this.options.placeholder);
+ }
+ }
+ else
+ {
+ for (var i = 0; i < this._rows.length; i++)
+ {
+ var tr = table.insertRow(-1);
+
+ for (var j = 0; j < this.options.columns.length; j++)
+ {
+ var col = this.options.columns[j];
+ var td = tr.insertCell(-1);
+
+ var val = this._rows[i][j];
+
+ if (typeof(val) == 'undefined')
+ val = col.placeholder;
+
+ if (typeof(val) == 'undefined')
+ val = '';
+
+ if (col.width)
+ td.style.width = col.width;
+
+ if (col.align)
+ td.style.textAlign = col.align;
+
+ if (typeof col.format == 'string')
+ $(td).append(col.format.format(val));
+ else if (typeof col.format == 'function')
+ $(td).append(col.format(val, i));
+ else
+ $(td).append(val);
+ }
+ }
+ }
+
+ this._rows = [ ];
+ fieldset.appendChild(table);
+
+ return fieldset;
+ }
+ });
+
+ ui_class.grid = ui_class.AbstractWidget.extend({
+ init: function()
+ {
+ this._rows = [ ];
+ },
+
+ row: function(values)
+ {
+ if ($.isArray(values))
+ {
+ this._rows.push(values);
+ }
+ else if ($.isPlainObject(values))
+ {
+ var v = [ ];
+ for (var i = 0; i < this.options.columns.length; i++)
+ {
+ var col = this.options.columns[i];
+
+ if (typeof col.key == 'string')
+ v.push(values[col.key]);
+ else
+ v.push(null);
+ }
+ this._rows.push(v);
+ }
+ },
+
+ rows: function(rows)
+ {
+ for (var i = 0; i < rows.length; i++)
+ this.row(rows[i]);
+ },
+
+ createCell: function(col, classNames)
+ {
+ var sizes = [ 'xs', 'sm', 'md', 'lg' ];
+
+ var cell = $('
')
+ .addClass('cell clearfix');
+
+ if (classNames)
+ cell.addClass(classNames);
+
+ if (col.nowrap)
+ cell.addClass('nowrap');
+
+ if (col.align)
+ cell.css('text-align', col.align);
+
+ for (var i = 0; i < sizes.length; i++)
+ cell.addClass((col['width_' + sizes[i]] > 0)
+ ? 'col-%s-%d'.format(sizes[i], col['width_' + sizes[i]])
+ : 'hidden-%s'.format(sizes[i]));
+
+ if (col.hidden)
+ cell.addClass('hidden-%s'.format(col.hidden));
+
+ return cell;
+ },
+
+ render: function(id)
+ {
+ var fieldset = $(' ')
+ .addClass('cbi-section');
+
+ if (this.options.caption)
+ fieldset.append($(' ').append(this.options.caption));
+
+ var grid = $('
')
+ .addClass('luci2-grid luci2-grid-hover');
+
+ if (this.options.condensed)
+ grid.addClass('luci2-grid-condensed');
+
+ var has_caption = false;
+ var has_description = false;
+
+ var sizes = [ 'xs', 'sm', 'md', 'lg' ];
+
+ for (var i = 0; i < sizes.length; i++)
+ {
+ var size = sizes[i];
+ var width_unk = 0;
+ var width_dyn = 0;
+ var width_rem = 12;
+
+ for (var j = 0; j < this.options.columns.length; j++)
+ {
+ var col = this.options.columns[j];
+ var k = i, width = NaN;
+
+ do { width = col['width_' + sizes[k++]]; }
+ while (isNaN(width) && k < sizes.length);
+
+ if (isNaN(width))
+ width = col.width;
+
+ if (isNaN(width))
+ width_unk++;
+ else
+ width_rem -= width, col['width_' + size] = width;
+
+ if (col.caption)
+ has_caption = true;
+
+ if (col.description)
+ has_description = true;
+ }
+
+ if (width_unk > 0)
+ width_dyn = Math.floor(width_rem / width_unk);
+
+ for (var j = 0; j < this.options.columns.length; j++)
+ if (isNaN(this.options.columns[j]['width_' + size]))
+ this.options.columns[j]['width_' + size] = width_dyn;
+ }
+
+ if (has_caption)
+ {
+ var row = $('
')
+ .addClass('row')
+ .appendTo(grid);
+
+ for (var i = 0; i < this.options.columns.length; i++)
+ {
+ var col = this.options.columns[i];
+ var cell = this.createCell(col, 'caption')
+ .appendTo(row);
+
+ if (col.caption)
+ cell.append(col.caption);
+ }
+ }
+
+ if (has_description)
+ {
+ var row = $('
')
+ .addClass('row')
+ .appendTo(grid);
+
+ for (var i = 0; i < this.options.columns.length; i++)
+ {
+ var col = this.options.columns[i];
+ var cell = this.createCell(col, 'description')
+ .appendTo(row);
+
+ if (col.description)
+ cell.append(col.description);
+ }
+ }
+
+ if (this._rows.length == 0)
+ {
+ if (this.options.placeholder)
+ $('
')
+ .addClass('row')
+ .append($('
')
+ .addClass('col-md-12 cell placeholder clearfix')
+ .append(this.options.placeholder))
+ .appendTo(grid);
+ }
+ else
+ {
+ for (var i = 0; i < this._rows.length; i++)
+ {
+ var row = $('
')
+ .addClass('row')
+ .appendTo(grid);
+
+ for (var j = 0; j < this.options.columns.length; j++)
+ {
+ var col = this.options.columns[j];
+ var cell = this.createCell(col, 'content')
+ .appendTo(row);
+
+ var val = this._rows[i][j];
+
+ if (typeof(val) == 'undefined')
+ val = col.placeholder;
+
+ if (typeof(val) == 'undefined')
+ val = '';
+
+ if (typeof col.format == 'string')
+ cell.append(col.format.format(val));
+ else if (typeof col.format == 'function')
+ cell.append(col.format(val, i));
+ else
+ cell.append(val);
+ }
+ }
+ }
+
+ this._rows = [ ];
+
+ return fieldset.append(grid);
+ }
+ });
+
+ ui_class.hlist = ui_class.AbstractWidget.extend({
+ render: function()
+ {
+ if (!$.isArray(this.options.items))
+ return '';
+
+ var list = $(' ');
+ var sep = this.options.separator || ' | ';
+ var items = [ ];
+
+ for (var i = 0; i < this.options.items.length; i += 2)
+ {
+ if (typeof(this.options.items[i+1]) === 'undefined' ||
+ this.options.items[i+1] === '')
+ continue;
+
+ items.push(this.options.items[i], this.options.items[i+1]);
+ }
+
+ for (var i = 0; i < items.length; i += 2)
+ {
+ list.append($(' ')
+ .addClass('nowrap')
+ .append($(' ')
+ .append(items[i])
+ .append(': '))
+ .append(items[i+1])
+ .append(((i+2) < items.length) ? sep : ''))
+ .append(' ');
+ }
+
+ return list;
+ }
+ });
+
+ ui_class.progress = ui_class.AbstractWidget.extend({
+ render: function()
+ {
+ var vn = parseInt(this.options.value) || 0;
+ var mn = parseInt(this.options.max) || 100;
+ var pc = Math.floor((100 / mn) * vn);
+
+ var text;
+
+ if (typeof(this.options.format) == 'string')
+ text = this.options.format.format(this.options.value, this.options.max, pc);
+ else if (typeof(this.options.format) == 'function')
+ text = this.options.format(pc);
+ else
+ text = '%.2f%%'.format(pc);
+
+ return $('
')
+ .addClass('progress')
+ .append($('
')
+ .addClass('progress-bar')
+ .addClass('progress-bar-info')
+ .css('width', pc + '%'))
+ .append($(' ')
+ .text(text));
+ }
+ });
+
+ ui_class.devicebadge = ui_class.AbstractWidget.extend({
+ render: function()
+ {
+ var l2dev = this.options.l2_device || this.options.device;
+ var l3dev = this.options.l3_device;
+ var dev = l3dev || l2dev || '?';
+
+ var span = document.createElement('span');
+ span.className = 'badge';
+
+ if (typeof(this.options.signal) == 'number' ||
+ typeof(this.options.noise) == 'number')
+ {
+ var r = 'none';
+ if (typeof(this.options.signal) != 'undefined' &&
+ typeof(this.options.noise) != 'undefined')
+ {
+ var q = (-1 * (this.options.noise - this.options.signal)) / 5;
+ if (q < 1)
+ r = '0';
+ else if (q < 2)
+ r = '0-25';
+ else if (q < 3)
+ r = '25-50';
+ else if (q < 4)
+ r = '50-75';
+ else
+ r = '75-100';
+ }
+
+ span.appendChild(document.createElement('img'));
+ span.lastChild.src = L.globals.resource + '/icons/signal-' + r + '.png';
+
+ if (r == 'none')
+ span.title = L.tr('No signal');
+ else
+ span.title = '%s: %d %s / %s: %d %s'.format(
+ L.tr('Signal'), this.options.signal, L.tr('dBm'),
+ L.tr('Noise'), this.options.noise, L.tr('dBm')
+ );
+ }
+ else
+ {
+ var type = 'ethernet';
+ var desc = L.tr('Ethernet device');
+
+ if (l3dev != l2dev)
+ {
+ type = 'tunnel';
+ desc = L.tr('Tunnel interface');
+ }
+ else if (dev.indexOf('br-') == 0)
+ {
+ type = 'bridge';
+ desc = L.tr('Bridge');
+ }
+ else if (dev.indexOf('.') > 0)
+ {
+ type = 'vlan';
+ desc = L.tr('VLAN interface');
+ }
+ else if (dev.indexOf('wlan') == 0 ||
+ dev.indexOf('ath') == 0 ||
+ dev.indexOf('wl') == 0)
+ {
+ type = 'wifi';
+ desc = L.tr('Wireless Network');
+ }
+
+ span.appendChild(document.createElement('img'));
+ span.lastChild.src = L.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
+ span.title = desc;
+ }
+
+ $(span).append(' ');
+ $(span).append(dev);
+
+ return span;
+ }
+ });
+
+ return Class.extend(ui_class);
+})();
diff --git a/luci2/luci2/htdocs/luci2/view/network.diagnostics.js b/luci2/luci2/htdocs/luci2/view/network.diagnostics.js
new file mode 100644
index 000000000..16492640f
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/network.diagnostics.js
@@ -0,0 +1,90 @@
+L.ui.view.extend({
+ title: L.tr('Diagnostics'),
+
+ runPing: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'ping',
+ params: [ 'data' ],
+ expect: { '': { code: -1 } }
+ }),
+
+ runPing6: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'ping6',
+ params: [ 'data' ],
+ expect: { '': { code: -1 } }
+ }),
+
+ runTraceroute: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'traceroute',
+ params: [ 'data' ],
+ expect: { '': { code: -1 } }
+ }),
+
+ runTraceroute6: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'traceroute6',
+ params: [ 'data' ],
+ expect: { '': { code: -1 } }
+ }),
+
+ runNslookup: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'nslookup',
+ params: [ 'data' ],
+ expect: { '': { code: -1 } }
+ }),
+
+ execute: function() {
+ var self = this;
+ var tools = [ ];
+
+ $.when(
+ self.runPing('?').then(function(rv) {
+ if (rv.code != -1) tools.push(['runPing', L.tr('IPv4 Ping')]);
+ }),
+ self.runPing6('?').then(function(rv) {
+ if (rv.code != -1) tools.push(['runPing6', L.tr('IPv6 Ping')]);
+ }),
+ self.runTraceroute('?').then(function(rv) {
+ if (rv.code != -1) tools.push(['runTraceroute', L.tr('IPv4 Traceroute')]);
+ }),
+ self.runTraceroute6('?').then(function(rv) {
+ if (rv.code != -1) tools.push(['runTraceroute6', L.tr('IPv6 Tracroute')]);
+ }),
+ self.runNslookup('?').then(function(rv) {
+ if (rv.code != -1) tools.push(['runNslookup', L.tr('DNS Lookup')]);
+ })
+ ).then(function() {
+ tools.sort(function(a, b) {
+ if (a[0] < b[0])
+ return -1;
+ else if (a[0] > b[0])
+ return 1;
+ else
+ return 0;
+ });
+
+ for (var i = 0; i < tools.length; i++)
+ $('#tool').append($(' ').attr('value', tools[i][0]).text(tools[i][1]));
+
+ $('#tool').val('runPing');
+
+ $('#run').click(function() {
+ L.ui.loading(true);
+ self[$('#tool').val()]($('#host').val()).then(function(rv) {
+ $('#output').empty().show();
+
+ if (rv.stdout)
+ $('#output').text(rv.stdout);
+
+ if (rv.stderr)
+ $('#output').append($(' ').css('color', 'red').text(rv.stderr));
+
+ L.ui.loading(false);
+ });
+ });
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/network.hosts.js b/luci2/luci2/htdocs/luci2/view/network.hosts.js
new file mode 100644
index 000000000..99fbe470a
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/network.hosts.js
@@ -0,0 +1,29 @@
+L.ui.view.extend({
+ title: L.tr('Hostnames'),
+ description: L.tr('Manage static host records to let the local DNS server resolve certain names to specific IP addresses.'),
+
+ execute: function() {
+ var m = new L.cbi.Map('dhcp', {
+ readonly: !this.options.acls.hostnames
+ });
+
+ var s = m.section(L.cbi.TableSection, 'domain', {
+ anonymous: true,
+ addremove: true,
+ add_caption: L.tr('Add new hostname'),
+ remove_caption: L.tr('Remove hostname')
+ });
+
+ s.option(L.cbi.InputValue, 'name', {
+ caption: L.tr('Hostname'),
+ datatype: 'hostname'
+ });
+
+ s.option(L.cbi.InputValue, 'ip', {
+ caption: L.tr('IP address'),
+ datatype: 'ipaddr'
+ });
+
+ return m.insertInto('#map');
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/network.interfaces.js b/luci2/luci2/htdocs/luci2/view/network.interfaces.js
new file mode 100644
index 000000000..8b925c7d9
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/network.interfaces.js
@@ -0,0 +1,361 @@
+L.ui.view.extend({
+ title: L.tr('Interface Overview'),
+
+ pendingRestart: [ ],
+ pendingShutdown: [ ],
+
+ setUp: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'ifup',
+ params: [ 'data' ],
+ expect: { '': { code: -1 } }
+ }),
+
+ setDown: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'ifdown',
+ params: [ 'data' ],
+ expect: { '': { code: -1 } }
+ }),
+
+ renderDeviceIcon: function(dev, up)
+ {
+ var icon = dev ? dev.icon(up) : L.globals.resource + '/icons/ethernet_disabled.png';
+ var desc = dev ? '%s (%s)'.format(dev.description(), dev.name()) : L.tr('Network interface not present');
+
+ return $(' ')
+ .attr('title', desc)
+ .attr('src', icon);
+ },
+
+ renderNetworkBadge: function(network, div)
+ {
+ var dest = div || $('#network-badge-%s'.format(network.name()));
+ var device = network.getDevice(); //network.device || { type: 'Network device', device: '?' };
+ var subdevs = network.getSubdevices();
+
+ if (div)
+ {
+ var h = $('
')
+ .addClass('ifacebox-head')
+ .text(network.name());
+
+ if (network.zone)
+ h.css('background-color', network.zone.color).attr('title', L.trc('Interface status', 'Part of zone "%s"').format(network.zone.name));
+ else
+ h.css('background-color', '#cccccc').attr('title', L.trc('Interface status', 'Not part of any zone'));
+
+ dest.append(h);
+ }
+ else
+ {
+ dest.children('div.ifacebox-body').remove();
+ }
+
+ var b = $('
')
+ .addClass('ifacebox-body');
+
+ b.append(this.renderDeviceIcon(device, network.isUp()));
+
+ if (subdevs.length)
+ {
+ b.append('(');
+
+ for (var i = 0; i < subdevs.length; i++)
+ b.append(this.renderDeviceIcon(subdevs[i], subdevs[i].isUp()));
+
+ b.append(')');
+ }
+
+ b.append($(' ')).append($(' ').text(device ? device.name() : '?'));
+
+ return dest.append(b);
+ },
+
+ renderNetworkStatus: function(network, div)
+ {
+ var rv = '';
+
+ if (network.isUp())
+ {
+ rv += '%s : %t '.format(
+ L.tr('Uptime'),
+ network.getUptime()
+ );
+ }
+ else
+ {
+ rv += '%s : %s '.format(
+ L.tr('Uptime'),
+ L.tr('Interface is down')
+ );
+ }
+
+ var v4 = network.getIPv4Addrs();
+ if (v4.length)
+ rv += '%s : %s '.format(
+ L.trc('Interface status', 'IPv4'),
+ v4.join(', ')
+ );
+
+ var v6 = network.getIPv6Addrs();
+ if (v6.length)
+ rv += '%s : %s '.format(
+ L.trc('Interface status', 'IPv6'),
+ v6.join(', ')
+ );
+
+ return (div || $('#network-status-%s'.format(network.name())))
+ .empty()
+ .append(rv);
+ },
+
+ renderNetworkChart: function(network, div)
+ {
+ var dest = (div || $('#network-chart-%s'.format(network.name())));
+
+ dest.empty();
+
+ dest.append($('
')
+ .addClass('traffic-chart')
+ .append($(' ')
+ .attr('id', 'network-chart-tx-%s'.format(network.name()))
+ .hide())
+ .append($(' ')));
+
+ dest.append($('
')
+ .addClass('traffic-chart')
+ .append($(' ')
+ .attr('id', 'network-chart-rx-%s'.format(network.name()))
+ .hide())
+ .append($(' ')));
+
+ dest.append($(' ')
+ .addClass('traffic-stats')
+ .text(L.tr('Loading statistics…')));
+
+ return dest;
+ },
+
+ refreshNetworkStatus: function()
+ {
+ var self = this;
+ var deferreds = [ ];
+
+ while (self.pendingRestart.length)
+ deferreds.push(self.setUp(self.pendingRestart.shift()));
+
+ while (self.pendingShutdown.length)
+ deferreds.push(self.setDown(self.pendingShutdown.shift()));
+
+ return $.when.apply($, deferreds).then(function() {
+ $('button').prop('disabled', false);
+ return $.when(
+ L.network.refreshDeviceStatus(),
+ L.network.refreshInterfaceStatus()
+ );
+ }).then(function() {
+ var networks = L.network.getInterfaces();
+
+ for (var i = 0; i < networks.length; i++)
+ {
+ self.renderNetworkBadge(networks[i]);
+ self.renderNetworkStatus(networks[i]);
+ }
+
+ var max = 0.1;
+ var networks = L.network.getInterfaces();
+
+ for (var i = 0; i < networks.length; i++)
+ {
+ var network = networks[i];
+ var history = network.getTrafficHistory();
+ var stats = network.getStatistics();
+
+ var tx = $('#network-chart-tx-%s'.format(network.name()));
+ var rx = $('#network-chart-rx-%s'.format(network.name()));
+
+ var tx_rate = history.tx_bytes[history.tx_bytes.length - 1];
+ var rx_rate = history.rx_bytes[history.rx_bytes.length - 1];
+
+ max = Math.max(Math.max.apply(Math, history.rx_bytes),
+ Math.max.apply(Math, history.tx_bytes),
+ max);
+
+ for (var j = 0; j < history.rx_bytes.length; j++)
+ history.rx_bytes[j] = -Math.abs(history.rx_bytes[j]);
+
+ tx.text(history.tx_bytes.join(','));
+ rx.text(history.rx_bytes.join(','));
+
+ tx.next().attr('title', '%.2mB/s'.format(tx_rate));
+ rx.next().attr('title', '%.2mB/s'.format(rx_rate));
+
+ tx.nextAll('label').html('↑ %.2mB/s'.format(tx_rate));
+ rx.nextAll('label').html('↓ %.2mB/s'.format(rx_rate));
+
+ tx.parent().nextAll('small.traffic-stats').html(
+ '%s : %.2mB (%d Pkts.) '.format(
+ L.trc('Interface status', 'TX'),
+ stats.tx_bytes, stats.tx_packets) +
+ '%s : %.2mB (%d Pkts.) '.format(
+ L.trc('Interface status', 'RX'),
+ stats.rx_bytes, stats.rx_packets));
+ }
+
+ for (var i = 0; i < networks.length; i++)
+ {
+ var network = networks[i];
+
+ var tx = $('#network-chart-tx-%s'.format(network.name()));
+ var rx = $('#network-chart-rx-%s'.format(network.name()));
+
+ tx.peity('line', { width: 200, min: 0, max: max });
+ rx.peity('line', { width: 200, min: -max, max: 0 });
+ }
+
+ L.ui.loading(false);
+ });
+ },
+
+ renderContents: function(networks)
+ {
+ var self = this;
+
+ var list = new L.ui.table({
+ columns: [ {
+ caption: L.tr('Network'),
+ width: '120px',
+ format: function(v) {
+ var div = $('
')
+ .attr('id', 'network-badge-%s'.format(v.name()))
+ .addClass('ifacebox');
+
+ return self.renderNetworkBadge(v, div);
+ }
+ }, {
+ caption: L.tr('Traffic'),
+ width: '215px',
+ format: function(v) {
+ var div = $('
').attr('id', 'network-chart-%s'.format(v.name()));
+ return self.renderNetworkChart(v, div);
+ }
+ }, {
+ caption: L.tr('Status'),
+ format: function(v) {
+ var div = $(' ').attr('id', 'network-status-%s'.format(v.name()));
+ return self.renderNetworkStatus(v, div);
+ }
+ }, {
+ caption: L.tr('Actions'),
+ format: function(v, n) {
+ return $('
')
+ .addClass('btn-group btn-group-sm')
+ .append(L.ui.button(L.tr('Restart'), 'default', L.tr('Enable or restart interface'))
+ .click({ self: self, network: v }, self.handleIfup))
+ .append(L.ui.button(L.tr('Shutdown'), 'default', L.tr('Shut down interface'))
+ .click({ self: self, network: v }, self.handleIfdown))
+ .append(L.ui.button(L.tr('Edit'), 'primary', L.tr('Edit interface'))
+ .click({ self: self, network: v }, self.handleEdit))
+ .append(L.ui.button(L.tr('Delete'), 'danger', L.tr('Delete interface'))
+ .click({ self: self, network: v }, self.handleRemove));
+ }
+ } ]
+ });
+
+ for (var i = 0; i < networks.length; i++)
+ list.row([ networks[i], networks[i], networks[i], networks[i] ]);
+
+ self.repeat(self.refreshNetworkStatus, 5000);
+
+ $('#map')
+ .append(list.render());
+ },
+
+ renderInterfaceForm: function(network)
+ {
+ var m = new L.cbi.Map('network', {
+ tabbed: true,
+ caption: 'Interface config',
+ description: 'I can config interface!!!!'
+ });
+
+
+
+ var s4 = m.section(L.cbi.TypedSection, 'route', {
+ caption: L.tr('Static IPv4 Routes'),
+ anonymous: true,
+ addremove: true,
+ sortable: true,
+ add_caption: L.tr('Add new route'),
+ remove_caption: L.tr('Remove route')
+ });
+
+ var ifc = s4.option(L.cbi.ListValue, 'interface', {
+ caption: L.tr('Interface')
+ });
+
+ ifc.value('foo');
+
+ s4.option(L.cbi.InputValue, 'target', {
+ caption: L.tr('Target'),
+ datatype: 'ip4addr'
+ });
+
+ s4.option(L.cbi.InputValue, 'netmask', {
+ caption: L.tr('IPv4-Netmask'),
+ datatype: 'ip4addr',
+ placeholder: '255.255.255.255',
+ optional: true
+ });
+
+ s4.option(L.cbi.InputValue, 'gateway', {
+ caption: L.tr('IPv4-Gateway'),
+ datatype: 'ip4addr',
+ optional: true
+ });
+
+ s4.option(L.cbi.InputValue, 'metric', {
+ caption: L.tr('Metric'),
+ datatype: 'range(0,255)',
+ placeholder: 0,
+ optional: true
+ });
+
+ s4.option(L.cbi.InputValue, 'mtu', {
+ caption: L.tr('MTU'),
+ datatype: 'range(64,9000)',
+ placeholder: 1500,
+ optional: true
+ });
+
+ return m;
+ },
+
+ handleIfup: function(ev) {
+ this.disabled = true;
+ this.blur();
+ ev.data.self.pendingRestart.push(ev.data.network['interface']);
+ },
+
+ handleIfdown: function(ev) {
+ this.disabled = true;
+ this.blur();
+ ev.data.self.pendingShutdown.push(ev.data.network['interface']);
+ },
+
+ handleEdit: function(ev) {
+ var self = ev.data.self;
+ var network = ev.data.network;
+
+ return network.createForm(L.cbi.Modal).show();
+ },
+
+ execute: function() {
+ var self = this;
+
+ return L.network.load().then(function() {
+ self.renderContents(L.network.getInterfaces());
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/network.routes.js b/luci2/luci2/htdocs/luci2/view/network.routes.js
new file mode 100644
index 000000000..b323c6ea1
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/network.routes.js
@@ -0,0 +1,110 @@
+L.ui.view.extend({
+ title: L.tr('Routes'),
+ description: L.tr('Routes specify over which interface and gateway a certain host or network can be reached.'),
+
+ execute: function() {
+ var self = this;
+ var ifaces = L.network.getInterfaces();
+
+ var m = new L.cbi.Map('network', {
+ readonly: !self.options.acls.network
+ });
+
+ var s4 = m.section(L.cbi.GridSection, 'route', {
+ caption: L.tr('Static IPv4 Routes'),
+ anonymous: true,
+ addremove: true,
+ sortable: true,
+ add_caption: L.tr('Add new route'),
+ remove_caption: L.tr('Remove route')
+ });
+
+ var ifc = s4.option(L.cbi.ListValue, 'interface', {
+ caption: L.tr('Interface')
+ });
+
+ for (var i = 0; i < ifaces.length; i++)
+ ifc.value(ifaces[i].name());
+
+ s4.option(L.cbi.InputValue, 'target', {
+ caption: L.tr('Target'),
+ datatype: 'ip4addr',
+ width: 2
+ });
+
+ s4.option(L.cbi.InputValue, 'netmask', {
+ caption: L.tr('IPv4-Netmask'),
+ datatype: 'ip4addr',
+ placeholder: '255.255.255.255',
+ optional: true,
+ width: 2
+ });
+
+ s4.option(L.cbi.InputValue, 'gateway', {
+ caption: L.tr('IPv4-Gateway'),
+ datatype: 'ip4addr',
+ optional: true,
+ width: 2
+ });
+
+ s4.option(L.cbi.InputValue, 'metric', {
+ caption: L.tr('Metric'),
+ datatype: 'range(0,255)',
+ placeholder: 0,
+ optional: true
+ });
+
+ s4.option(L.cbi.InputValue, 'mtu', {
+ caption: L.tr('MTU'),
+ datatype: 'range(64,9000)',
+ placeholder: 1500,
+ optional: true
+ });
+
+
+ var s6 = m.section(L.cbi.GridSection, 'route6', {
+ caption: L.tr('Static IPv6 Routes'),
+ anonymous: true,
+ addremove: true,
+ sortable: true,
+ add_caption: L.tr('Add new route'),
+ remove_caption: L.tr('Remove route')
+ });
+
+ var ifc = s6.option(L.cbi.ListValue, 'interface', {
+ caption: L.tr('Interface')
+ });
+
+ for (var i = 0; i < ifaces.length; i++)
+ ifc.value(ifaces[i].name());
+
+ s6.option(L.cbi.InputValue, 'target', {
+ caption: L.tr('Target'),
+ datatype: 'ip6addr',
+ width: 3
+ });
+
+ s6.option(L.cbi.InputValue, 'gateway', {
+ caption: L.tr('IPv6-Gateway'),
+ datatype: 'ip6addr',
+ optional: true,
+ width: 3
+ });
+
+ s6.option(L.cbi.InputValue, 'metric', {
+ caption: L.tr('Metric'),
+ datatype: 'range(0,255)',
+ placeholder: 0,
+ optional: true
+ });
+
+ s6.option(L.cbi.InputValue, 'mtu', {
+ caption: L.tr('MTU'),
+ datatype: 'range(64,9000)',
+ placeholder: 1500,
+ optional: true
+ });
+
+ m.insertInto('#map');
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/network.switch.js b/luci2/luci2/htdocs/luci2/view/network.switch.js
new file mode 100644
index 000000000..057955e3b
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/network.switch.js
@@ -0,0 +1,338 @@
+L.ui.view.extend({
+ title: L.tr('Switch'),
+ description: L.tr('The network ports on this device can be combined to several VLANs in which computers can communicate directly with each other. VLANs are often used to separate different network segments. Often there is by default one Uplink port for a connection to the next greater network like the internet and other ports for a local network.'),
+
+ listSwitchNames: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'switch_list',
+ expect: { switches: [ ] }
+ }),
+
+ getSwitchInfo: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'switch_info',
+ params: [ 'switch' ],
+ expect: { info: { } },
+ filter: function(data, params) {
+ data['attrs'] = data['switch'];
+ data['vlan_attrs'] = data['vlan'];
+ data['port_attrs'] = data['port'];
+ data['switch'] = params['switch'];
+
+ delete data.vlan;
+ delete data.port;
+
+ return data;
+ }
+ }),
+
+ getSwitchStatus: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'switch_status',
+ params: [ 'switch' ],
+ expect: { ports: [ ] }
+ }),
+
+ switchPortState: L.cbi.ListValue.extend({
+ choices: [
+ [ 'n', L.trc('Switch port state', 'off') ],
+ [ 'u', L.trc('Switch port state', 'untagged') ],
+ [ 't', L.trc('Switch port state', 'tagged') ]
+ ],
+
+ init: function(name, options)
+ {
+ var self = this;
+
+ options.datatype = function(val, elem)
+ {
+ if (val == 'u')
+ {
+ var u = false;
+ var sections = self.ownerSection.getUCISections();
+
+ for (var i = 0; i < sections.length; i++)
+ {
+ var v = self.formvalue(sections[i]['.name']);
+ if (v == 'u')
+ {
+ if (u)
+ return L.tr('Port must not be untagged in multiple VLANs');
+
+ u = true;
+ }
+ }
+ }
+
+ return true;
+ };
+
+ this.callSuper('init', name, options);
+ },
+
+ ucivalue: function(sid)
+ {
+ var ports = (this.ownerMap.get('network', sid, 'ports') || '').match(/[0-9]+[tu]?/g);
+
+ if (ports)
+ for (var i = 0; i < ports.length; i++)
+ if (ports[i].match(/^([0-9]+)([tu]?)$/))
+ if (RegExp.$1 == this.name)
+ return RegExp.$2 || 'u';
+
+ return 'n';
+ },
+
+ save: function(sid)
+ {
+ return;
+ }
+ }),
+
+ execute: function() {
+ var self = this;
+ return self.listSwitchNames().then(function(switches) {
+ L.rpc.batch();
+
+ for (var i = 0; i < switches.length; i++)
+ self.getSwitchInfo(switches[i]);
+
+ return L.rpc.flush();
+ }).then(function(switches) {
+ var m = new L.cbi.Map('network', {
+ readonly: !self.options.acls.network
+ });
+
+ for (var i = 0; i < switches.length; i++)
+ {
+ var swname = switches[i]['switch'];
+
+ var vid_opt = 'vlan';
+ var v4k_opt = undefined;
+ var pvid_opt = undefined;
+ var max_vid = switches[i].num_vlans - 1;
+ var num_vlans = switches[i].num_vlans;
+
+ for (var j = 0; j < switches[i].vlan_attrs.length; j++)
+ {
+ switch (switches[i].vlan_attrs[j].name)
+ {
+ case 'tag':
+ case 'vid':
+ case 'pvid':
+ vid_opt = switches[i].vlan_attrs[j].name;
+ max_vid = 4095;
+ break;
+ }
+ }
+
+ for (var j = 0; j < switches[i].port_attrs.length; j++)
+ {
+ switch (switches[i].port_attrs[j].name)
+ {
+ case 'pvid':
+ pvid_opt = switches[i].port_attrs[j].name;
+ break;
+ }
+ }
+
+
+ var sw = m.section(L.cbi.TypedSection, 'switch', {
+ caption: L.tr('Switch "%s"').format(switches[i].model),
+ swname: swname
+ });
+
+ sw.filter = function(section) {
+ return (section['.name'] == this.options.swname ||
+ section.name == this.options.swname);
+ };
+
+ for (var j = 0; j < switches[i].attrs.length; j++)
+ {
+ switch (switches[i].attrs[j].name)
+ {
+ case 'enable_vlan':
+ sw.option(L.cbi.CheckboxValue, 'enable_vlan', {
+ caption: L.tr('Enable VLAN functionality')
+ });
+ break;
+
+ case 'enable_learning':
+ sw.option(L.cbi.CheckboxValue, 'enable_learning', {
+ caption: L.tr('Enable learning and aging'),
+ initial: true,
+ optional: true
+ });
+ break;
+
+ case 'max_length':
+ sw.option(L.cbi.CheckboxValue, 'max_length', {
+ caption: L.tr('Enable Jumbo Frame passthrough'),
+ enabled: '3',
+ optional: true
+ });
+ break;
+
+ case 'enable_vlan4k':
+ v4k_opt = switches[i].attrs[j].name;
+ break;
+ }
+ }
+
+ var vlans = m.section(L.cbi.TableSection, 'switch_vlan', {
+ caption: L.tr('VLANs on "%s"').format(switches[i].model),
+ swname: swname,
+ addremove: true,
+ add_caption: L.tr('Add VLAN entry …')
+ });
+
+ vlans.add = function() {
+ var sections = this.getUCISections();
+ var used_vids = { };
+
+ for (var j = 0; j < sections.length; j++)
+ {
+ var v = this.ownerMap.get('network', sections[j]['.name'], 'vlan');
+ if (v)
+ used_vids[v] = true;
+ }
+
+ for (var j = 1; j < num_vlans; j++)
+ {
+ if (used_vids[j.toString()])
+ continue;
+
+ var sid = this.ownerMap.add('network', 'switch_vlan');
+ this.ownerMap.set('network', sid, 'device', this.options.swname);
+ this.ownerMap.set('network', sid, 'vlan', j);
+ break;
+ }
+ };
+
+ vlans.filter = function(section) {
+ return (section.device == this.options.swname);
+ };
+
+ vlans.sections = function() {
+ var s = this.callSuper('sections');
+
+ s.sort(function(a, b) {
+ var x = parseInt(a[vid_opt] || a.vlan);
+ if (isNaN(x))
+ x = 9999;
+
+ var y = parseInt(b[vid_opt] || b.vlan);
+ if (isNaN(y))
+ y = 9999;
+
+ return (x - y);
+ });
+
+ return s;
+ };
+
+ var port_opts = [ ];
+
+ var vo = vlans.option(L.cbi.InputValue, vid_opt, {
+ caption: L.tr('VLAN ID'),
+ datatype: function(val) {
+ var sections = vlans.getUCISections();
+ var used_vids = { };
+
+ for (var j = 0; j < sections.length; j++)
+ {
+ var v = vlans.fields[vid_opt].formvalue(sections[j]['.name']);
+ if (!v)
+ continue;
+
+ if (used_vids[v])
+ return L.tr('VLAN ID must be unique');
+
+ used_vids[v] = true;
+ }
+
+ if (val.match(/[^0-9]/))
+ return L.tr('Invalid VLAN ID');
+
+ val = parseInt(val, 10);
+
+ if (val < 1 || val > max_vid)
+ return L.tr('VLAN ID must be a value between %u and %u').format(1, max_vid);
+
+ return true;
+ }
+ });
+
+ vo.ucivalue = function(sid) {
+ var id = this.ownerMap.get('network', sid, vid_opt);
+
+ if (isNaN(parseInt(id)))
+ id = this.ownerMap.get('network', sid, 'vlan');
+
+ return id;
+ };
+
+ vo.save = function(sid) {
+ var old_ports = this.ownerMap.get('network', sid, 'ports');
+ var new_ports = '';
+
+ for (var j = 0; j < port_opts.length; j++)
+ {
+ var v = port_opts[j].formvalue(sid);
+ if (v != 'n')
+ new_ports += '%s%d%s'.format(
+ new_ports ? ' ' : '', j,
+ (v == 'u') ? '' : 't');
+ }
+
+ if (new_ports != old_ports)
+ this.ownerMap.set('network', sid, 'ports', new_ports);
+
+ if (v4k_opt)
+ {
+ var s = sw.getUCISections();
+ for (var j = 0; j < s.length; j++)
+ this.ownerMap.set('network', s[j]['.name'], v4k_opt, '1');
+ }
+
+ this.callSuper('save', sid);
+ };
+
+ for (var j = 0; j < switches[i].num_ports; j++)
+ {
+ var label = L.trc('Switch port label', 'Port %d').format(j);
+
+ if (j == switches[i].cpu_port)
+ label = L.trc('Switch port label', 'CPU');
+
+ var po = vlans.option(self.switchPortState, j.toString(), {
+ caption: label + ' '.format(swname, j)
+ });
+
+ port_opts.push(po);
+ }
+ }
+
+ return m.insertInto('#map').then(function() {
+ self.repeat(function() {
+ return self.getSwitchStatus(swname).then(function(ports) {
+ for (var j = 0; j < ports.length; j++)
+ {
+ var s = L.tr('No link');
+ var d = ' ';
+
+ if (ports[j].link)
+ {
+ s = '%dbaseT'.format(ports[j].speed);
+ d = ports[j].full_duplex ? L.tr('Full-duplex') : L.tr('Half-duplex');
+ }
+
+ $('#portstatus-%s-%d'.format(swname, j))
+ .empty().append(s + ' ' + d);
+ }
+ });
+ }, 5000);
+ });
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/network.wireless.js b/luci2/luci2/htdocs/luci2/view/network.wireless.js
new file mode 100644
index 000000000..ab006a086
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/network.wireless.js
@@ -0,0 +1,89 @@
+L.ui.view.extend({
+ execute: function() {
+ var self = this;
+
+ var m = new L.cbi.Map('wireless', {
+ caption: L.tr('Wireless configuration')
+ });
+
+ var s = m.section(L.cbi.TypedSection, 'wifi-device', {
+ caption: L.tr('WiFi devices'),
+ collabsible: true
+ });
+
+ (s.option(L.cbi.DummyValue, '__name', {
+ caption: L.tr('Device')
+ })).ucivalue = function(sid)
+ {
+ return sid;
+ };
+
+ s.option(L.cbi.CheckboxValue, 'disabled', {
+ caption: L.tr('Disabled')
+ });
+
+ s.option(L.cbi.InputValue, 'channel', {
+ caption: L.tr('Channel')
+ });
+
+ var s_1 = s.subsection(L.cbi.TypedSection, 'wifi-iface', {
+ caption: L.tr('Device interfaces'),
+ addremove: true,
+ add_caption: L.tr('Add interface …')
+ });
+
+ s_1.filter = function(section, parent_sid) {
+ return section.device == parent_sid;
+ };
+
+ s_1.add = function(name, sid) {
+ var iface = this.ownerMap.add('wireless', 'wifi-iface');
+ this.ownerMap.set('wireless', iface, 'device', sid);
+ };
+
+ s_1.tab({
+ id: 'general',
+ caption: L.tr('General Settings')
+ });
+
+ s_1.taboption('general', L.cbi.CheckboxValue, 'disabled', {
+ caption: L.tr('Disabled')
+ });
+
+ s_1.taboption('general', L.cbi.ListValue, 'mode', {
+ caption: L.tr('Mode'),
+ initial: 'ap'
+ })
+ .value('ap', L.tr('Access Point'))
+ .value('sta', L.tr('Client'))
+ .value('adhoc', L.tr('Ad-Hoc'))
+ .value('wds', L.tr('WDS (Wireless Distribution System)'))
+ .value('monitor', L.tr('Monitor'))
+ .value('mesh', L.tr('Mesh'));
+
+ s_1.taboption('general', L.cbi.InputValue, 'ssid', {
+ caption: 'SSID'
+ });
+
+ s_1.tab({
+ id: 'security',
+ caption: L.tr('Security')
+ });
+
+ s_1.taboption('security', L.cbi.ListValue, 'encryption', {
+ caption: L.tr('Encryption'),
+ initial: 'none'
+ })
+ .value('none', L.tr('No encryption'))
+ .value('psk', L.tr('WPA Personal (PSK)'))
+ .value('psk2', L.tr('WPA2 Personal (PSK)'))
+ .value('mixed-psk', L.tr('WPA/WPA2 Personal (PSK) mixed'));
+
+ s_1.taboption('security', L.cbi.PasswordValue, 'key', {
+ caption: L.tr('Passphrase'),
+ optional: true
+ });
+
+ return m.insertInto('#map');
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/status.dmesg.js b/luci2/luci2/htdocs/luci2/view/status.dmesg.js
new file mode 100644
index 000000000..de039736c
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/status.dmesg.js
@@ -0,0 +1,20 @@
+L.ui.view.extend({
+ title: L.tr('Kernel Log'),
+ refresh: 5000,
+
+ getKernelLog: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'dmesg',
+ expect: { log: '' }
+ }),
+
+ execute: function() {
+ return this.getKernelLog().then(function(log) {
+ var ta = document.getElementById('syslog');
+ var lines = log.replace(/\n+$/, '').split(/\n/);
+
+ ta.rows = lines.length;
+ ta.value = lines.reverse().join("\n");
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/status.overview.js b/luci2/luci2/htdocs/luci2/view/status.overview.js
new file mode 100644
index 000000000..dc6fce2b2
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/status.overview.js
@@ -0,0 +1,427 @@
+L.ui.view.extend({
+ title: L.tr('Status'),
+
+ getConntrackCount: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'conntrack_count',
+ expect: { '': { count: 0, limit: 0 } }
+ }),
+
+ getDHCPLeases: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'dhcp_leases',
+ expect: { leases: [ ] }
+ }),
+
+ getDHCPv6Leases: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'dhcp6_leases',
+ expect: { leases: [ ] }
+ }),
+
+ renderContents: function() {
+ var self = this;
+ return $.when(
+ L.network.refreshStatus().then(function() {
+ var wan = L.network.findWAN();
+ var wan6 = L.network.findWAN6();
+
+ if (!wan && !wan6)
+ {
+ $('#network_status_table').empty();
+ return;
+ }
+
+ var networkTable = new L.ui.grid({
+ caption: L.tr('Network'),
+ columns: [ {
+ width: 2,
+ width_sm: 12,
+ format: '%s'
+ }, {
+ width: 2,
+ width_sm: 3,
+ align: 'right',
+ format: function(v) {
+ var dev = L.network.resolveAlias(v.getDevice());
+ if (dev)
+ return $(' ')
+ .addClass('badge')
+ .attr('title', dev.description())
+ .append($(' ').attr('src', dev.icon()))
+ .append(' %s'.format(dev.name()));
+
+ return '';
+ }
+ }, {
+ width: 6,
+ width_sm: 9,
+ format: function(v, n) {
+ return new L.ui.hlist({ items: [
+ L.tr('Type'), v.getProtocol().description,
+ L.tr('Connected'), '%t'.format(v.getUptime()),
+ L.tr('Address'), (n ? v.getIPv6Addrs() : v.getIPv4Addrs()).join(', '),
+ L.tr('Gateway'), v.getIPv4Gateway(),
+ L.tr('DNS'), (n ? v.getIPv6DNS() : v.getIPv4DNS()).join(', ')
+ ] }).render();
+ }
+ } ]
+ });
+
+ if (wan)
+ networkTable.row([ L.tr('IPv4 WAN Status'), wan, wan ]);
+
+ if (wan6)
+ networkTable.row([ L.tr('IPv6 WAN Status'), wan6, wan6 ]);
+
+ networkTable.insertInto('#network_status_table');
+ }),
+ self.getConntrackCount().then(function(count) {
+ var conntrackTable = new L.ui.grid({
+ caption: L.tr('Connection Tracking'),
+ columns: [ {
+ width: 4
+ }, {
+ format: function(v) {
+ return new L.ui.progress({
+ value: v.count,
+ max: v.limit,
+ format: '%d / %d (%d%%)'
+ }).render();
+ }
+ } ]
+ });
+
+ conntrackTable.row([ L.tr('Active Connections'), count ]);
+ conntrackTable.insertInto('#conntrack_status_table');
+ }),
+ L.system.getInfo().then(function(info) {
+ var sysinfoTable = new L.ui.grid({
+ caption: L.tr('System'),
+ columns: [ {
+ width: 4
+ }, {
+ width: 8,
+ nowrap: true
+ } ]
+ });
+
+ sysinfoTable.rows([
+ [ L.tr('Hostname'), info.hostname ],
+ [ L.tr('Model'), info.model ],
+ [ L.tr('Firmware Version'), info.release.description ],
+ [ L.tr('Kernel Version'), info.kernel ],
+ [ L.tr('Local Time'), (new Date(info.localtime * 1000)).toString() ],
+ [ L.tr('Uptime'), '%t'.format(info.uptime) ],
+ [ L.tr('Load Average'),
+ '%.2f %.2f %.2f'.format(
+ info.load[0] / 65535.0,
+ info.load[1] / 65535.0,
+ info.load[2] / 65535.0
+ ) ]
+ ]);
+
+ sysinfoTable.insertInto('#system_status_table');
+
+ var memoryTable = new L.ui.grid({
+ caption: L.tr('Memory'),
+ columns: [ {
+ format: '%s',
+ width: 4
+ }, {
+ format: function(v) {
+ return new L.ui.progress({
+ value: v,
+ max: info.memory.total,
+ format: function(pc) {
+ return ('%d ' + L.tr('kB') + ' / %d ' + L.tr('kB') + ' (%d%%)').format(
+ v / 1024, info.memory.total / 1024, pc
+ );
+ }
+ }).toString();
+ }
+ } ]
+ });
+
+ memoryTable.rows([
+ [ L.tr('Total Available'), info.memory.free + info.memory.buffered ],
+ [ L.tr('Free'), info.memory.free ],
+ [ L.tr('Cached'), info.memory.shared ],
+ [ L.tr('Buffered'), info.memory.buffered ],
+ ]);
+
+ memoryTable.insertInto('#memory_status_table');
+
+ if (info.swap.total > 0)
+ {
+ var swapTable = new L.ui.grid({
+ caption: L.tr('Swap'),
+ columns: [ {
+ format: '%s',
+ width: 4
+ }, {
+ format: function(v) {
+ return new L.ui.progress({
+ value: v,
+ max: info.swap.total,
+ format: function(pc) {
+ return ('%d ' + L.tr('kB') + ' / %d ' + L.tr('kB') + ' (%d%%)').format(
+ v / 1024, info.swap.total / 1024, pc
+ );
+ }
+ }).toString();
+ }
+ } ]
+ });
+
+ swapTable.row([ L.tr('Free'), info.swap.free ]);
+ swapTable.insertInto('#swap_status_table');
+ }
+
+ var diskTable = new L.ui.grid({
+ caption: L.tr('Storage'),
+ columns: [ {
+ format: '%s',
+ width: 4
+ }, {
+ format: function(v) {
+ return new L.ui.progress({
+ value: v[0],
+ max: v[1],
+ format: function(pc) {
+ return ('%d ' + L.tr('kB') + ' / %d ' + L.tr('kB') + ' (%d%%)').format(
+ v[0] / 1024, v[1] / 1024, pc
+ );
+ }
+ }).toString();
+ }
+ } ]
+ });
+
+ diskTable.row([ '' + L.tr('Root Usage') + ' (/)', [ info.root.used, info.root.total ] ]);
+ diskTable.row([ '' + L.tr('Temporary Usage') + ' (/tmp)', [ info.tmp.used, info.tmp.total ] ]);
+ diskTable.insertInto('#disk_status_table');
+ }),
+ L.wireless.getWirelessStatus().then(function(radios) {
+ var phys = [ ];
+ for (var phy in radios)
+ phys.push(phy);
+
+ phys.sort();
+
+ $('#wifi_status_table').empty();
+
+ for (var i = 0; i < phys.length; i++)
+ {
+ var rows = [ ];
+ var radio = radios[phys[i]];
+
+ rows.push([false, {
+ name: radio.hardware
+ ? '%s 802.11%s (%s)'.format(
+ radio.hardware.name, radio.hwmodes.join(''),
+ radio.phy.replace(/^[^0-9]+/, 'radio'))
+ : ('802.11%s ' + L.tr('Radio') + ' (%s)').format(
+ radio.hwmodes.join(''),
+ radio.phy.replace(/^[^0-9]+/, 'radio')),
+ channel: radio.channel,
+ frequency: radio.frequency,
+ txpower: radio.txpower
+ }]);
+
+ for (var j = 0; j < radio.networks.length; j++)
+ {
+ var network = radio.networks[j];
+
+ if (network.bssid && network.bssid != '00:00:00:00:00:00' && radio.channel)
+ rows[0][0] = true;
+
+ rows.push([{
+ signal: network.signal,
+ noise: network.noise,
+ device: network.device
+ }, {
+ ssid: network.ssid,
+ bssid: network.bssid,
+ mode: network.mode,
+ encryption: network.encryption,
+ bitrate: network.bitrate
+ }]);
+ }
+
+ var wifiTable = new L.ui.grid({
+ caption: i ? null : L.tr('Wireless'),
+ columns: [ {
+ width: 2,
+ width_sm: 3,
+ align: 'right',
+ format: function(v, n)
+ {
+ if (typeof(v) != 'boolean')
+ return new L.ui.devicebadge(v).render();
+ else
+ return L.ui.icon('wifi_big' + (v ? '' : '_disabled'));
+ }
+ }, {
+ width: 6,
+ width_sm: 9,
+ format: function(v, n)
+ {
+ if (typeof(rows[n][0]) != 'boolean')
+ {
+ return new L.ui.hlist({ items: [
+ L.tr('Mode'), v.mode,
+ L.tr('Bitrate'), v.bitrate ? ('~ %.1f ' + L.tr('Mbit/s')).format(v.bitrate / 1000) : undefined,
+ L.tr('SSID'), v.ssid,
+ L.tr('BSSID'), v.bssid,
+ L.tr('Encryption'), L.wireless.formatEncryption(v.encryption)
+ ] }).render();
+ }
+ else
+ {
+ return $(' ')
+ .append($(' ')
+ .addClass('nowrap')
+ .append(v.name))
+ .append(' ')
+ .add(new L.ui.hlist({ items: [
+ L.tr('Channel'), '%d (%.3f %s)'.format(v.channel, v.frequency / 1000, L.tr('GHz')),
+ L.tr('TX Power'), '%d %s'.format(v.txpower, L.tr('dBm'))
+ ] }).render());
+ }
+ }
+ } ]
+ });
+
+ wifiTable.rows(rows);
+ $('#wifi_status_table').append(wifiTable.render());
+ }
+ }),
+ L.wireless.getAssocLists().then(function(assoclist) {
+ var formatRate = function(v)
+ {
+ return '%s '.format(
+ (!isNaN(v.mcs) && v.mcs > 0)
+ ? ('%.1f ' + L.tr('Mbit/s') + ', MCS %d, %d%s').format(v.rate / 1000, v.mcs, v['40mhz'] ? 40 : 20, L.tr('MHz'))
+ : ('%.1f ' + L.tr('Mbit/s')).format(v.rate / 1000));
+ };
+
+ var assocTable = new L.ui.grid({
+ caption: L.tr('Associated Stations'),
+ placeholder: L.tr('No information available'),
+ columns: [ {
+ format: function(v, n) {
+ return new L.ui.devicebadge(assoclist[n]).render();
+ },
+ width: 2,
+ width_sm: 2,
+ align: 'right',
+ key: 'signal'
+ }, {
+ width_sm: 4,
+ caption: L.tr('MAC-Address'),
+ key: 'mac'
+ }, {
+ caption: L.tr('Signal'),
+ format: '%d ' + L.tr('dBm') + '',
+ key: 'signal',
+ width: 1,
+ width_sm: 0
+ }, {
+ caption: L.tr('Noise'),
+ format: '%d ' + L.tr('dBm') + '',
+ key: 'noise',
+ width: 1,
+ width_sm: 0
+ }, {
+ caption: L.tr('RX Rate'),
+ format: formatRate,
+ key: 'rx',
+ width: 3
+ }, {
+ caption: L.tr('TX Rate'),
+ format: formatRate,
+ key: 'tx',
+ width: 3
+ } ]
+ });
+
+ assocTable.rows(assoclist);
+ assocTable.insertInto('#wifi_assoc_table');
+ }),
+ self.getDHCPLeases().then(function(leases) {
+ var leaseTable = new L.ui.grid({
+ caption: L.tr('DHCP Leases'),
+ placeholder: L.tr('There are no active leases.'),
+ columns: [ {
+ caption: L.tr('Hostname'),
+ placeholder: '?',
+ key: 'hostname',
+ nowrap: true,
+ width_sm: 5
+ }, {
+ caption: L.tr('IPv4-Address'),
+ key: 'ipaddr',
+ width_sm: 4,
+ format: '255.255.255.255'
+ }, {
+ caption: L.tr('MAC-Address'),
+ key: 'macaddr',
+ width_sm: 0
+ }, {
+ caption: L.tr('Leasetime remaining'),
+ key: 'expires',
+ width_sm: 3,
+ nowrap: true,
+ format: function(v) {
+ return (v <= 0) ? L.tr('expired') : '%t'.format(v);
+ }
+ } ]
+ });
+
+ leaseTable.rows(leases);
+ leaseTable.insertInto('#lease_status_table');
+ }),
+ self.getDHCPv6Leases().then(function(leases) {
+ if (!leases.length)
+ return;
+
+ var leaseTable = new L.ui.grid({
+ caption: L.tr('DHCPv6 Leases'),
+ columns: [ {
+ caption: L.tr('Hostname'),
+ placeholder: '?',
+ key: 'hostname',
+ width_sm: 0
+ }, {
+ caption: L.tr('IPv6-Address'),
+ key: 'ip6addr',
+ width_sm: 6
+ }, {
+ caption: L.tr('DUID'),
+ key: 'duid',
+ width_sm: 0
+ }, {
+ caption: L.tr('Leasetime remaining'),
+ key: 'expires',
+ width_sm: 6,
+ format: function(v) {
+ return (v <= 0) ? L.tr('expired') : '%t'.format(v);
+ }
+ } ]
+ });
+
+ leaseTable.rows(leases);
+ leaseTable.insertInto('#lease6_status_table');
+ })
+ )
+ },
+
+ execute: function()
+ {
+ var self = this;
+ return L.network.load().then(function() {
+ self.repeat(self.renderContents, 5000);
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/status.processes.js b/luci2/luci2/htdocs/luci2/view/status.processes.js
new file mode 100644
index 000000000..b58df8af6
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/status.processes.js
@@ -0,0 +1,80 @@
+L.ui.view.extend({
+ title: L.tr('Processes'),
+ description: L.tr('This list gives an overview over currently running system processes and their status.'),
+
+ getProcessList: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'process_list',
+ expect: { processes: [ ] },
+ filter: function(data) {
+ data.sort(function(a, b) { return a.pid - b.pid });
+ return data;
+ }
+ }),
+
+ sendSignal: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'process_signal',
+ params: [ 'pid', 'signal' ],
+ filter: function(data) {
+ return (data == 0);
+ }
+ }),
+
+ execute: function() {
+ var self = this;
+ var allow_signals = this.options.acls.status;
+ return self.getProcessList().then(function(list) {
+ var procTable = new L.ui.table({
+ columns: [ {
+ caption: L.tr('PID'),
+ key: 'pid'
+ }, {
+ caption: L.tr('Owner'),
+ key: 'user'
+ }, {
+ caption: L.tr('Command'),
+ key: 'command'
+ }, {
+ caption: L.tr('CPU usage (%)'),
+ key: 'cpu_percent',
+ format: '%d%%'
+ }, {
+ caption: L.tr('Memory usage (%)'),
+ key: 'vsize_percent',
+ format: '%d%%'
+ }, {
+ key: 'pid',
+ format: function(v, n) {
+ return $('
')
+ .addClass('btn-group')
+ .append($(' ')
+ .addClass('btn btn-primary btn-sm dropdown-toggle')
+ .attr('data-toggle', 'dropdown')
+ .text(L.tr('Signal…')))
+ .append($('')
+ .addClass('dropdown-menu pull-right')
+ .append($(' ')
+ .append($(' ')
+ .attr('href', '#')
+ .html('%s (%s)'.format(L.trc('UNIX signal', 'Reload'), 'HUP'))
+ .click(function(ev) { self.sendSignal(v, 1).then(status); ev.preventDefault(); })))
+ .append($(' ')
+ .append($(' ')
+ .attr('href', '#')
+ .html('%s (%s)'.format(L.trc('UNIX signal', 'Terminate'), 'TERM'))
+ .click(function(ev) { self.sendSignal(v, 15).then(status); ev.preventDefault(); })))
+ .append($(' ')
+ .append($(' ')
+ .attr('href', '#')
+ .html('%s (%s)'.format(L.trc('UNIX signal', 'Kill immediately'), 'KILL'))
+ .click(function(ev) { self.sendSignal(v, 9).then(status); ev.preventDefault(); }))))
+ }
+ } ]
+ });
+
+ procTable.rows(list);
+ procTable.insertInto('#process_table');
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/status.routes.js b/luci2/luci2/htdocs/luci2/view/status.routes.js
new file mode 100644
index 000000000..a73cf828a
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/status.routes.js
@@ -0,0 +1,99 @@
+L.ui.view.extend({
+ title: L.tr('Routes'),
+ description: L.tr('The following rules are currently active on this system.'),
+
+ getRoutes: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'routes',
+ expect: { routes: [ ] }
+ }),
+
+ getIPv6Routes: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'routes',
+ expect: { routes: [ ] }
+ }),
+
+ getARPTable: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'arp_table',
+ expect: { entries: [ ] }
+ }),
+
+ execute: function() {
+ var self = this;
+ return $.when(
+ self.getARPTable().then(function(arp) {
+ var arpTable = new L.ui.table({
+ caption: L.tr('ARP'),
+ columns: [{
+ caption: L.tr('IPv4-Address'),
+ key: 'ipaddr'
+ }, {
+ caption: L.tr('MAC-Address'),
+ key: 'macaddr'
+ }, {
+ caption: L.tr('Interface'),
+ key: 'device'
+ }]
+ });
+
+ arpTable.rows(arp);
+ arpTable.insertInto('#arp_table');
+ }),
+ self.getRoutes().then(function(routes) {
+ var routeTable = new L.ui.table({
+ caption: L.tr('Active IPv4-Routes'),
+ columns: [{
+ caption: L.tr('Target'),
+ key: 'target'
+ }, {
+ caption: L.tr('Gateway'),
+ key: 'nexthop'
+ }, {
+ caption: L.tr('Metric'),
+ key: 'metric'
+ }, {
+ caption: L.tr('Interface'),
+ key: 'device'
+ }]
+ });
+
+ routeTable.rows(routes);
+ routeTable.insertInto('#route_table');
+ }),
+ self.getIPv6Routes().then(function(routes) {
+ var route6Table = new L.ui.table({
+ caption: L.tr('Active IPv6-Routes'),
+ columns: [{
+ caption: L.tr('Target'),
+ key: 'target'
+ }, {
+ caption: L.tr('Gateway'),
+ key: 'nexthop'
+ }, {
+ caption: L.tr('Source'),
+ key: 'source'
+ }, {
+ caption: L.tr('Metric'),
+ key: 'metric'
+ }, {
+ caption: L.tr('Interface'),
+ key: 'device'
+ }]
+ });
+
+ for (var i = 0; i < routes.length; i++)
+ {
+ var prefix = routes[i].target.substr(0, 5).toLowerCase();
+ if (prefix == 'fe80:' || prefix == 'fe90:' || prefix == 'fea0:' || prefix == 'feb0:' || prefix == 'ff00:')
+ continue;
+
+ route6Table.row(routes[i]);
+ }
+
+ route6Table.insertInto('#route6_table');
+ })
+ )
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/status.syslog.js b/luci2/luci2/htdocs/luci2/view/status.syslog.js
new file mode 100644
index 000000000..87b338691
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/status.syslog.js
@@ -0,0 +1,20 @@
+L.ui.view.extend({
+ title: L.tr('System Log'),
+ refresh: 5000,
+
+ getSystemLog: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'syslog',
+ expect: { log: '' }
+ }),
+
+ execute: function() {
+ return this.getSystemLog().then(function(log) {
+ var ta = document.getElementById('syslog');
+ var lines = log.replace(/\n+$/, '').split(/\n/);
+
+ ta.rows = lines.length;
+ ta.value = lines.reverse().join("\n");
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/system.admin.js b/luci2/luci2/htdocs/luci2/view/system.admin.js
new file mode 100644
index 000000000..8ecc95f18
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/system.admin.js
@@ -0,0 +1,373 @@
+L.ui.view.extend({
+ PubkeyListValue: L.cbi.AbstractValue.extend({
+ base64Table: {
+ 'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6,
+ 'H': 7, 'I': 8, 'J': 9, 'K': 10, 'L': 11, 'M': 12, 'N': 13,
+ 'O': 14, 'P': 15, 'Q': 16, 'R': 17, 'S': 18, 'T': 19, 'U': 20,
+ 'V': 21, 'W': 22, 'X': 23, 'Y': 24, 'Z': 25, 'a': 26, 'b': 27,
+ 'c': 28, 'd': 29, 'e': 30, 'f': 31, 'g': 32, 'h': 33, 'i': 34,
+ 'j': 35, 'k': 36, 'l': 37, 'm': 38, 'n': 39, 'o': 40, 'p': 41,
+ 'q': 42, 'r': 43, 's': 44, 't': 45, 'u': 46, 'v': 47, 'w': 48,
+ 'x': 49, 'y': 50, 'z': 51, '0': 52, '1': 53, '2': 54, '3': 55,
+ '4': 56, '5': 57, '6': 58, '7': 59, '8': 60, '9': 61, '+': 62,
+ '/': 63, '=': 64
+ },
+
+ base64Decode: function(s)
+ {
+ var i = 0;
+ var d = '';
+
+ if (s.match(/[^A-Za-z0-9\+\/\=]/))
+ return undefined;
+
+ while (i < s.length)
+ {
+ var e1 = this.base64Table[s.charAt(i++)];
+ var e2 = this.base64Table[s.charAt(i++)];
+ var e3 = this.base64Table[s.charAt(i++)];
+ var e4 = this.base64Table[s.charAt(i++)];
+
+ var c1 = ( e1 << 2) | (e2 >> 4);
+ var c2 = ((e2 & 15) << 4) | (e3 >> 2);
+ var c3 = ((e3 & 3) << 6) | e4;
+
+ d += String.fromCharCode(c1);
+
+ if (e3 < 64)
+ d += String.fromCharCode(c2);
+
+ if (e4 < 64)
+ d += String.fromCharCode(c3);
+ }
+
+ return d;
+ },
+
+ lengthDecode: function(s, off)
+ {
+ var l = (s.charCodeAt(off++) << 24) |
+ (s.charCodeAt(off++) << 16) |
+ (s.charCodeAt(off++) << 8) |
+ s.charCodeAt(off++);
+
+ if (l < 0 || (off + l) > s.length)
+ return -1;
+
+ return l;
+ },
+
+ pubkeyDecode: function(s)
+ {
+ var parts = s.split(/\s+/);
+ if (parts.length < 2)
+ return undefined;
+
+ var key = this.base64Decode(parts[1]);
+ if (!key)
+ return undefined;
+
+ var off, len;
+
+ off = 0;
+ len = this.lengthDecode(key, off);
+
+ if (len < 0)
+ return undefined;
+
+ var type = key.substr(off + 4, len);
+ if (type != parts[0])
+ return undefined;
+
+ off += 4 + len;
+
+ var len1 = this.lengthDecode(key, off);
+ if (len1 < 0)
+ return undefined;
+
+ off += 4 + len1;
+
+ var len2 = this.lengthDecode(key, off);
+ if (len2 < 0)
+ return undefined;
+
+ if (len1 & 1)
+ len1--;
+
+ if (len2 & 1)
+ len2--;
+
+ switch (type)
+ {
+ case 'ssh-rsa':
+ return { type: 'RSA', bits: len2 * 8, comment: parts[2] };
+
+ case 'ssh-dss':
+ return { type: 'DSA', bits: len1 * 8, comment: parts[2] };
+
+ default:
+ return undefined;
+ }
+ },
+
+ _remove: function(ev)
+ {
+ var self = ev.data.self;
+
+ self._keys.splice(ev.data.index, 1);
+ self._render(ev.data.div);
+ },
+
+ _add: function(ev)
+ {
+ var self = ev.data.self;
+
+ var form = $('
')
+ .append($('
')
+ .text(L.tr('Paste the public key line into the field below and press "%s" to continue.').format(L.tr('Ok'))))
+ .append($('
')
+ .text(L.tr('Unrecognized public key! Please add only RSA or DSA keys.'))
+ .addClass('alert alert-danger')
+ .hide())
+ .append($('
')
+ .append($(' ')
+ .attr('type', 'text')
+ .attr('placeholder', L.tr('Paste key here'))
+ .addClass('form-control')));
+
+ L.ui.dialog(L.tr('Add new public key'), form, {
+ style: 'confirm',
+ confirm: function() {
+ var val = form.find('input').val();
+ if (!val)
+ {
+ return;
+ }
+
+ var key = self.pubkeyDecode(val);
+ if (!key)
+ {
+ form.find('input').val('');
+ form.find('.alert').show();
+ return;
+ }
+
+ self._keys.push(val);
+ self._render(ev.data.div);
+
+ L.ui.dialog(false);
+ }
+ });
+ },
+
+ _show: function(ev)
+ {
+ var self = ev.data.self;
+
+ L.ui.dialog(
+ L.tr('Public key'),
+ $(' ').text(self._keys[ev.data.index]),
+ { style: 'close' }
+ );
+ },
+
+ _render: function(div)
+ {
+ div.empty();
+
+ for (var i = 0; i < this._keys.length; i++)
+ {
+ var k = this.pubkeyDecode(this._keys[i] || '');
+
+ if (!k)
+ continue;
+
+ $('
')
+ .addClass('input-group')
+ .append($(' ')
+ .addClass('form-control')
+ .attr('type', 'text')
+ .prop('readonly', true)
+ .click({ self: this, index: i }, this._show)
+ .val('%dBit %s - %s'.format(k.bits, k.type, k.comment || '?')))
+ .append($(' ')
+ .addClass('input-group-btn')
+ .append($(' ')
+ .addClass('btn btn-danger')
+ .attr('title', L.tr('Remove public key'))
+ .text('–')
+ .click({ self: this, div: div, index: i }, this._remove)))
+ .appendTo(div);
+ }
+
+ if (this._keys.length > 0)
+ $(' ').appendTo(div);
+
+ L.ui.button(L.tr('Add public key …'), 'success')
+ .click({ self: this, div: div }, this._add)
+ .appendTo(div);
+ },
+
+ widget: function(sid)
+ {
+ this._keys = [ ];
+
+ for (var i = 0; i < this.options.keys.length; i++)
+ this._keys.push(this.options.keys[i]);
+
+ var d = $('
')
+ .attr('id', this.id(sid));
+
+ this._render(d);
+
+ return d;
+ },
+
+ changed: function(sid)
+ {
+ if (this.options.keys.length != this._keys.length)
+ return true;
+
+ for (var i = 0; i < this.options.keys.length; i++)
+ if (this.options.keys[i] != this._keys[i])
+ return true;
+
+ return false;
+ },
+
+ save: function(sid)
+ {
+ if (this.changed(sid))
+ {
+ this.options.keys = [ ];
+
+ for (var i = 0; i < this._keys.length; i++)
+ this.options.keys.push(this._keys[i]);
+
+ return L.views.SystemAdmin.setSSHKeys(this._keys);
+ }
+
+ return undefined;
+ }
+ }),
+
+ getSSHKeys: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'sshkeys_get',
+ expect: { keys: [ ] }
+ }),
+
+ setSSHKeys: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'sshkeys_set',
+ params: [ 'keys' ]
+ }),
+
+ setPassword: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'password_set',
+ params: [ 'user', 'password' ]
+ }),
+
+ execute: function() {
+ var self = this;
+ return self.getSSHKeys().then(function(keys) {
+ var m = new L.cbi.Map('dropbear', {
+ caption: L.tr('SSH Access'),
+ description: L.tr('Dropbear offers SSH network shell access and an integrated SCP server'),
+ tabbed: true
+ });
+
+ var s1 = m.section(L.cbi.DummySection, '__password', {
+ caption: L.tr('Router Password'),
+ description: L.tr('Changes the administrator password for accessing the device'),
+ readonly: !self.options.acls.admin
+ });
+
+ var p1 = s1.option(L.cbi.PasswordValue, 'pass1', {
+ caption: L.tr('Password'),
+ optional: true
+ });
+
+ var p2 = s1.option(L.cbi.PasswordValue, 'pass2', {
+ caption: L.tr('Confirmation'),
+ optional: true,
+ datatype: function(v) {
+ var v1 = p1.formvalue('__password');
+ if (v1 && v1.length && v != v1)
+ return L.tr('Passwords must match!');
+ return true;
+ }
+ });
+
+ p1.save = function(sid) { };
+ p2.save = function(sid) {
+ var v1 = p1.formvalue(sid);
+ var v2 = p2.formvalue(sid);
+ if (v2 && v2.length > 0 && v1 == v2)
+ return L.system.setPassword('root', v2);
+ };
+
+
+ var s2 = m.section(L.cbi.DummySection, '__pubkeys', {
+ caption: L.tr('SSH-Keys'),
+ description: L.tr('Specifies public keys for passwordless SSH authentication'),
+ readonly: !self.options.acls.admin
+ });
+
+ var k = s2.option(self.PubkeyListValue, 'keys', {
+ caption: L.tr('Saved keys'),
+ keys: keys
+ });
+
+
+ var s3 = m.section(L.cbi.TypedSection, 'dropbear', {
+ caption: L.tr('SSH Server'),
+ description: L.tr('This sections define listening instances of the builtin Dropbear SSH server'),
+ addremove: true,
+ add_caption: L.tr('Add instance ...'),
+ readonly: !self.options.acls.admin,
+ collabsible: true
+ });
+
+ s3.option(L.cbi.NetworkList, 'Interface', {
+ caption: L.tr('Interface'),
+ description: L.tr('Listen only on the given interface or, if unspecified, on all')
+ });
+
+ s3.option(L.cbi.InputValue, 'Port', {
+ caption: L.tr('Port'),
+ description: L.tr('Specifies the listening port of this Dropbear instance'),
+ datatype: 'port',
+ placeholder: 22,
+ optional: true
+ });
+
+ s3.option(L.cbi.CheckboxValue, 'PasswordAuth', {
+ caption: L.tr('Password authentication'),
+ description: L.tr('Allow SSH password authentication'),
+ initial: true,
+ enabled: 'on',
+ disabled: 'off'
+ });
+
+ s3.option(L.cbi.CheckboxValue, 'RootPasswordAuth', {
+ caption: L.tr('Allow root logins with password'),
+ description: L.tr('Allow the root user to login with password'),
+ initial: true,
+ enabled: 'on',
+ disabled: 'off'
+ });
+
+ s3.option(L.cbi.CheckboxValue, 'GatewayPorts', {
+ caption: L.tr('Gateway ports'),
+ description: L.tr('Allow remote hosts to connect to local SSH forwarded ports'),
+ initial: false,
+ enabled: 'on',
+ disabled: 'off'
+ });
+
+ return m.insertInto('#map');
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/system.cron.js b/luci2/luci2/htdocs/luci2/view/system.cron.js
new file mode 100644
index 000000000..c663e01a1
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/system.cron.js
@@ -0,0 +1,33 @@
+L.ui.view.extend({
+ title: L.tr('Scheduled Tasks'),
+ description: L.tr('This is the system crontab in which scheduled tasks can be defined.'),
+
+ getCrontab: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'crontab_get',
+ expect: { data: '' }
+ }),
+
+ setCrontab: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'crontab_set',
+ params: [ 'data' ]
+ }),
+
+ execute: function() {
+ var self = this;
+ var allow_write = this.options.acls.cron;
+
+ return self.getCrontab().then(function(data) {
+ $('textarea').val(data).attr('disabled', !allow_write);
+ $('input.cbi-button-save').attr('disabled', !allow_write).click(function() {
+ var data = ($('textarea').val() || '').replace(/\r/g, '').replace(/\n?$/, '\n');
+ L.ui.loading(true);
+ self.setCrontab(data).then(function() {
+ $('textarea').val(data);
+ L.ui.loading(false);
+ });
+ });
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/system.leds.js b/luci2/luci2/htdocs/luci2/view/system.leds.js
new file mode 100644
index 000000000..ed4602673
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/system.leds.js
@@ -0,0 +1,121 @@
+L.ui.view.extend({
+ listLEDs: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'led_list',
+ expect: { leds: [ ] }
+ }),
+
+ listUSBDevices: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'usb_list',
+ expect: { devices: [ ] }
+ }),
+
+ execute: function() {
+ var self = this;
+ var m = new L.cbi.Map('system', {
+ caption: L.tr('LED Configuration'),
+ description: L.tr('Customizes the behaviour of the device LEDs if possible.'),
+ prepare: function() {
+ delete m.sections[0].fields._net_dev.choices;
+
+ var devs = L.network.getDevices().sort(function(a, b) {
+ if (a.name() < b.name())
+ return -1;
+ else if (a.name() > b.name())
+ return 1;
+ else
+ return 0;
+ });
+
+ for (var i = 0; i < devs.length; i++)
+ if (!devs[i].isAlias())
+ m.sections[0].fields._net_dev.value(devs[i].name());
+
+ return $.when(
+ self.listLEDs().then(function(leds) {
+ delete m.sections[0].fields.sysfs.choices;
+ delete m.sections[0].fields.trigger.choices;
+
+ for (var i = 0; i < leds.length; i++)
+ m.sections[0].fields.sysfs.value(leds[i].name);
+
+ for (var i = 0; i < leds[0].triggers.length; i++)
+ m.sections[0].fields.trigger.value(leds[0].triggers[i]);
+ }),
+ self.listUSBDevices().then(function(devs) {
+ delete m.sections[0].fields._usb_dev.choices;
+
+ for (var i = 0; i < devs.length; i++)
+ m.sections[0].fields._usb_dev.value(devs[i].name,
+ '%04x:%04x (%s - %s)'.format(devs[i].vendor_id, devs[i].product_id,
+ devs[i].vendor_name || '?', devs[i].product_name || '?'));
+ })
+ );
+ }
+ });
+
+ var s = m.section(L.cbi.TypedSection, 'led', {
+ caption: L.tr('LED Definitions'),
+ teasers: [ 'name', 'sysfs', 'default', 'trigger', '_net_dev', 'mode', '_usb_dev', 'delayon', 'delayoff' ],
+ collabsible: true,
+ addremove: true,
+ add_caption: L.tr('Add new LED defintion'),
+ remove_caption: L.tr('Remove LED definition'),
+ readonly: !this.options.acls.leds
+ });
+
+ s.option(L.cbi.InputValue, 'name', {
+ caption: L.tr('Name')
+ });
+
+ s.option(L.cbi.ListValue, 'sysfs', {
+ caption: L.tr('LED Name')
+ });
+
+ s.option(L.cbi.ListValue, 'default', {
+ caption: L.tr('Default state'),
+ initial: '0'
+ }).value('0', L.trc('LED state', 'off')).value('1', L.trc('LED state', 'on'));
+
+ s.option(L.cbi.ListValue, 'trigger', {
+ caption: L.tr('Trigger')
+ });
+
+
+ s.option(L.cbi.InputValue, 'delayon', {
+ caption: L.trc('LED timer trigger', 'On-State Delay'),
+ description: L.trc('LED timer trigger', 'Time in milliseconds the LED stays on'),
+ datatype: 'uinteger'
+ }).depends('trigger', 'timer');
+
+ s.option(L.cbi.InputValue, 'delayoff', {
+ caption: L.trc('LED timer trigger', 'Off-State Delay'),
+ description: L.trc('LED timer trigger', 'Time in milliseconds the LED stays off'),
+ datatype: 'uinteger'
+ }).depends('trigger', 'timer');
+
+
+ s.option(L.cbi.ListValue, '_net_dev', {
+ caption: L.trc('LED netdev trigger', 'Device'),
+ uci_option: 'dev',
+ optional: true
+ }).depends('trigger', 'netdev');
+
+ s.option(L.cbi.MultiValue, 'mode', {
+ caption: L.trc('LED netdev trigger', 'Trigger Mode')
+ }).depends('trigger', 'netdev')
+ .value('link', L.trc('LED netdev trigger mode', 'Link On'))
+ .value('tx', L.trc('LED netdev trigger mode', 'Transmit'))
+ .value('rx', L.trc('LED netdev trigger mode', 'Receive'));
+
+
+ s.option(L.cbi.ListValue, '_usb_dev', {
+ caption: L.trc('LED usbdev trigger', 'Device'),
+ uci_option: 'dev',
+ optional: true
+ }).depends('trigger', 'usbdev');
+
+ return m.insertInto('#map');
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/system.software.js b/luci2/luci2/htdocs/luci2/view/system.software.js
new file mode 100644
index 000000000..7aed31c67
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/system.software.js
@@ -0,0 +1,361 @@
+L.ui.view.extend({
+ title: L.tr('Package management'),
+
+ opkg: {
+ updateLists: L.rpc.declare({
+ object: 'luci2.opkg',
+ method: 'update',
+ expect: { '': { } }
+ }),
+
+ _allPackages: L.rpc.declare({
+ object: 'luci2.opkg',
+ method: 'list',
+ params: [ 'offset', 'limit', 'pattern' ],
+ expect: { '': { } }
+ }),
+
+ _installedPackages: L.rpc.declare({
+ object: 'luci2.opkg',
+ method: 'list_installed',
+ params: [ 'offset', 'limit', 'pattern' ],
+ expect: { '': { } }
+ }),
+
+ _findPackages: L.rpc.declare({
+ object: 'luci2.opkg',
+ method: 'find',
+ params: [ 'offset', 'limit', 'pattern' ],
+ expect: { '': { } }
+ }),
+
+ _fetchPackages: function(action, offset, limit, pattern)
+ {
+ var packages = [ ];
+
+ return action(offset, limit, pattern).then(function(list) {
+ if (!list.total || !list.packages)
+ return { length: 0, total: 0 };
+
+ packages.push.apply(packages, list.packages);
+ packages.total = list.total;
+
+ if (limit <= 0)
+ limit = list.total;
+
+ if (packages.length >= limit)
+ return packages;
+
+ L.rpc.batch();
+
+ for (var i = offset + packages.length; i < limit; i += 100)
+ action(i, (Math.min(i + 100, limit) % 100) || 100, pattern);
+
+ return L.rpc.flush();
+ }).then(function(lists) {
+ for (var i = 0; i < lists.length; i++)
+ {
+ if (!lists[i].total || !lists[i].packages)
+ continue;
+
+ packages.push.apply(packages, lists[i].packages);
+ packages.total = lists[i].total;
+ }
+
+ return packages;
+ });
+ },
+
+ listPackages: function(offset, limit, pattern)
+ {
+ return this._fetchPackages(this._allPackages, offset, limit, pattern);
+ },
+
+ installedPackages: function(offset, limit, pattern)
+ {
+ return this._fetchPackages(this._installedPackages, offset, limit, pattern);
+ },
+
+ findPackages: function(offset, limit, pattern)
+ {
+ return this._fetchPackages(this._findPackages, offset, limit, pattern);
+ },
+
+ installPackage: L.rpc.declare({
+ object: 'luci2.opkg',
+ method: 'install',
+ params: [ 'package' ],
+ expect: { '': { } }
+ }),
+
+ removePackage: L.rpc.declare({
+ object: 'luci2.opkg',
+ method: 'remove',
+ params: [ 'package' ],
+ expect: { '': { } }
+ }),
+
+ getConfig: L.rpc.declare({
+ object: 'luci2.opkg',
+ method: 'config_get',
+ expect: { config: '' }
+ }),
+
+ setConfig: L.rpc.declare({
+ object: 'luci2.opkg',
+ method: 'config_set',
+ params: [ 'data' ]
+ }),
+
+ isInstalled: function(pkg)
+ {
+ return this._installedPackages(0, 1, pkg).then(function(list) {
+ return (!isNaN(list.total) && list.total > 0);
+ });
+ }
+ },
+
+ updateDiskSpace: function()
+ {
+ return L.system.getDiskInfo().then(function(info) {
+ $('#package_space').empty().append(
+ new L.ui.progress({
+ value: info.root.used / 1024,
+ max: info.root.total / 1024,
+ format: '%d ' + L.tr('kB') + ' / %d ' + L.tr('kB') + ' ' + L.trc('Used disk space', 'used') + ' (%d%%)'
+ }).render());
+ });
+ },
+
+ installRemovePackage: function(pkgname, installed)
+ {
+ var self = this;
+
+ var dspname = pkgname.replace(/^.+\//, '');
+ var action = installed ? self.opkg.removePackage : self.opkg.installPackage;
+ var title = (installed ? L.tr('Removing package "%s" …') : L.tr('Installing package "%s" …')).format(dspname);
+ var confirm = (installed ? L.tr('Really remove package "%h" ?') : L.tr('Really install package "%h" ?')).format(dspname);
+
+ L.ui.dialog(title, confirm, {
+ style: 'confirm',
+ confirm: function() {
+ L.ui.dialog(title, L.tr('Waiting for package manager …'), { style: 'wait' });
+
+ action.call(self.opkg, pkgname).then(function(res) {
+ self.fetchInstalledList().then(function() { return self.fetchPackageList(); }).then(function() {
+ var output = [ ];
+
+ if (res.stdout)
+ output.push($(' ').text(res.stdout));
+
+ if (res.stderr)
+ output.push($(' ').addClass('alert-message').text(res.stderr));
+
+ output.push(res.code ? L.tr('Package manager failed with status %d.').format(res.code) : L.tr('Package manager finished successfully.'));
+
+ L.ui.dialog(title, output, { style: 'close' });
+
+ if (name)
+ $('#package_url').val('');
+ });
+ });
+ }
+ });
+ },
+
+ fetchInstalledList: function()
+ {
+ var self = this;
+ return self.opkg.installedPackages(0, 0, '*').then(function(list) {
+ self.installedList = { };
+ for (var i = 0; i < list.length; i++)
+ self.installedList[list[i][0]] = true;
+ });
+ },
+
+ fetchPackageList: function(offset, interactive)
+ {
+ if (interactive)
+ L.ui.loading(true);
+
+ if (typeof(offset) == 'undefined')
+ offset = parseInt($('#package_filter').attr('offset')) || 0;
+
+ var self = this;
+
+ var pattern = $('#package_filter').val() || '';
+ var action;
+
+ if (pattern.length)
+ {
+ action = $('#package_which').prop('checked') ? self.opkg.installedPackages : self.opkg.findPackages;
+ pattern = '*' + pattern + '*';
+
+ $('#package_filter').next().attr('src', L.globals.resource + '/icons/cbi/remove.gif');
+ }
+ else
+ {
+ action = $('#package_which').prop('checked') ? self.opkg.installedPackages : self.opkg.listPackages;
+ pattern = '*';
+
+ $('#package_filter').next().attr('src', L.globals.resource + '/icons/cbi/find.gif');
+ }
+
+ $('#package_filter').attr('offset', offset);
+
+ var install_disabled = $('#package_install').attr('disabled');
+
+ return action.call(self.opkg, offset, 100, pattern).then(function(list) {
+ var packageTable = new L.ui.table({
+ placeholder: L.tr('No matching packages found.'),
+ columns: [ {
+ caption: L.trc('Package table header', 'Package'),
+ key: 0
+ }, {
+ caption: L.trc('Package table header', 'Version'),
+ key: 1,
+ format: function(v) {
+ return (v.length > 15 ? v.substring(0, 14) + '…' : v);
+ }
+ }, {
+ caption: L.trc('Package table header', 'Description'),
+ key: 2
+ }, {
+ caption: L.trc('Package table header', 'Installation Status'),
+ key: 0,
+ width: '120px',
+ format: function(v, n) {
+ var inst = self.installedList[list[n][0]];
+ return L.ui.button(inst ? L.trc('Package state', 'Installed') : L.trc('Package state', 'Not installed'), inst ? 'success' : 'danger')
+ .css('width', '100%')
+ .attr('disabled', install_disabled)
+ .attr('pkgname', list[n][0])
+ .attr('installed', inst)
+ .click(function() {
+ self.installRemovePackage(this.getAttribute('pkgname'), this.getAttribute('installed') == 'true');
+ });
+ }
+ } ]
+ });
+
+ packageTable.rows(list);
+ packageTable.insertInto('#package_table');
+
+ if (offset > 0)
+ $('#package_prev')
+ .attr('offset', offset - 100)
+ .attr('disabled', false)
+ .text('« %d - %d'.format(offset - 100 + 1, offset));
+ else
+ $('#package_prev')
+ .attr('disabled', true)
+ .text('« %d - %d'.format(1, Math.min(100, list.total)));
+
+ if ((offset + 100) < list.total)
+ $('#package_next')
+ .attr('offset', offset + 100)
+ .attr('disabled', false)
+ .text('%d - %d »'.format(offset + 100 + 1, Math.min(offset + 200, list.total)));
+ else
+ $('#package_next')
+ .attr('disabled', true)
+ .text('%d - %d »'.format(list.total - (list.total % 100) + 1, list.total));
+
+ if (interactive)
+ L.ui.loading(false);
+ }).then(self.updateDiskSpace);
+ },
+
+ execute: function()
+ {
+ var self = this;
+
+ $('textarea, input.cbi-button-save').attr('disabled', !this.options.acls.software);
+ $('#package_update, #package_url, #package_install').attr('disabled', !this.options.acls.software);
+
+ return $.when(
+ self.opkg.getConfig().then(function(config) {
+ $('#config textarea')
+ .attr('rows', (config.match(/\n/g) || [ ]).length + 1)
+ .val(config);
+
+ $('#config button')
+ .click(function() {
+ var data = ($('#config textarea').val() || '').replace(/\r/g, '').replace(/\n?$/, '\n');
+ L.ui.loading(true);
+ self.opkg.setConfig(data).then(function() {
+ $('#config textarea')
+ .attr('rows', (data.match(/\n/g) || [ ]).length + 1)
+ .val(data);
+
+ L.ui.loading(false);
+ });
+ });
+ }),
+ self.fetchInstalledList(),
+ self.updateDiskSpace()
+ ).then(function() {
+ $('#package_prev, #package_next').click(function(ev) {
+ if (!this.getAttribute('disabled'))
+ {
+ self.fetchPackageList(parseInt(this.getAttribute('offset')), true);
+ this.blur();
+ }
+ });
+
+ $('#package_filter').next().click(function(ev) {
+ $('#package_filter').val('');
+ self.fetchPackageList(0, true);
+ });
+
+ $('#package_filter').keyup(function(ev) {
+ if (ev.which != 13)
+ return true;
+
+ ev.preventDefault();
+ self.fetchPackageList(0, true);
+ return false;
+ });
+
+ $('#package_which').click(function(ev) {
+ this.blur();
+ self.fetchPackageList(0, true);
+ });
+
+ $('#package_url').keyup(function(ev) {
+ if (ev.which != 13)
+ return true;
+
+ ev.preventDefault();
+
+ if (this.value)
+ self.installRemovePackage(this.value, false);
+ });
+
+ $('#package_install').click(function(ev) {
+ var name = $('#package_url').val();
+ if (name)
+ self.installRemovePackage(name, false);
+ });
+
+ $('#package_update').click(function(ev) {
+ L.ui.dialog(L.tr('Updating package lists'), L.tr('Waiting for package manager …'), { style: 'wait' });
+ self.opkg.updateLists().then(function(res) {
+ var output = [ ];
+
+ if (res.stdout)
+ output.push($(' ').text(res.stdout));
+
+ if (res.stderr)
+ output.push($(' ').addClass('alert-message').text(res.stderr));
+
+ output.push(res.code ? L.tr('Package manager failed with status %d.').format(res.code) : L.tr('Package manager finished successfully.'));
+
+ L.ui.dialog(L.tr('Updating package lists'), output, { style: 'close' });
+ });
+ });
+
+ return self.fetchPackageList(0);
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/system.startup.js b/luci2/luci2/htdocs/luci2/view/system.startup.js
new file mode 100644
index 000000000..769d12d41
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/system.startup.js
@@ -0,0 +1,103 @@
+L.ui.view.extend({
+ title: L.tr('Startup'),
+
+ getRcLocal: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'rclocal_get',
+ expect: { data: '' }
+ }),
+
+ setRcLocal: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'rclocal_set',
+ params: [ 'data' ]
+ }),
+
+ execute: function() {
+ var self = this;
+ var redraw = function() { return self.execute(); };
+ var allow_write = self.options.acls.startup;
+
+ return $.when(
+ L.system.initList().then(function(list) {
+ /* filter init scripts with no start prio */
+ for (var i = 0; i < list.length; i++)
+ {
+ if (typeof(list[i].start) != 'undefined')
+ continue;
+
+ list.splice(i--, 1);
+ }
+
+ var initTable = new L.ui.table({
+ columns: [ {
+ caption: L.tr('Start priority'),
+ key: 'start'
+ }, {
+ caption: L.tr('Initscript'),
+ key: 'name'
+ }, {
+ key: 'enabled',
+ format: function(v, n) {
+ return [
+ $('
')
+ .addClass('btn-group pull-right')
+ .append($(' ')
+ .attr('disabled', !allow_write)
+ .attr('name', list[n].name)
+ .addClass('btn btn-sm')
+ .addClass(v ? 'btn-success' : 'btn-danger')
+ .text(v ? L.trc('Init script state', 'Enabled') : L.trc('Init script state', 'Disabled'))
+ .click(function() {
+ L.ui.loading(true);
+ if (v)
+ L.system.initDisable(this.getAttribute('name')).then(redraw);
+ else
+ L.system.initEnable(this.getAttribute('name')).then(redraw);
+ }))
+ .append($(' ')
+ .addClass('btn btn-primary btn-sm dropdown-toggle')
+ .attr('data-toggle', 'dropdown')
+ .attr('disabled', !allow_write)
+ .text(L.tr('Action…')))
+ .append($('')
+ .addClass('dropdown-menu pull-right')
+ .append($(' ')
+ .append($(' ')
+ .attr('href', '#')
+ .text(L.tr('Reload'))
+ .click(function(ev) { L.system.initReload(v).then(redraw); ev.preventDefault(); })))
+ .append($(' ')
+ .append($(' ')
+ .attr('href', '#')
+ .text(L.tr('Restart'))
+ .click(function(ev) { L.system.initRestart(v).then(redraw); ev.preventDefault(); })))
+ .append($(' ')
+ .append($(' ')
+ .attr('href', '#')
+ .text(L.tr('Stop'))
+ .click(function(ev) { L.system.initStop(v).then(redraw); ev.preventDefault(); }))))
+ ];
+ }
+ } ]
+ });
+
+ initTable.rows(list);
+ initTable.insertInto('#init_table');
+
+ L.ui.loading(false);
+ }),
+ self.getRcLocal().then(function(data) {
+ $('textarea').val(data).attr('disabled', !allow_write);
+ $('input.cbi-button-save').attr('disabled', !allow_write).click(function() {
+ var data = ($('textarea').val() || '').replace(/\r/g, '').replace(/\n?$/, '\n');
+ L.ui.loading(true);
+ self.setRcLocal(data).then(function() {
+ $('textarea').val(data);
+ L.ui.loading(false);
+ });
+ });
+ })
+ );
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/system.system.js b/luci2/luci2/htdocs/luci2/view/system.system.js
new file mode 100644
index 000000000..90e0012ba
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/system.system.js
@@ -0,0 +1,212 @@
+L.ui.view.extend({
+ execute: function() {
+ var m = new L.cbi.Map('system', {
+ caption: L.tr('System'),
+ description: L.tr('Here you can configure the basic aspects of your device like its hostname or the timezone.'),
+ collabsible: true
+ });
+
+ var s = m.section(L.cbi.TypedSection, 'system', {
+ caption: L.tr('System Properties'),
+ teasers: [ 'hostname', 'zonename', 'languages', 'themes' ],
+ readonly: !this.options.acls.system
+ });
+
+ s.tab({
+ id: 'general',
+ caption: L.tr('General Settings')
+ });
+
+ var t = s.taboption('general', L.cbi.DummyValue, '__time', {
+ caption: L.tr('Local Time')
+ });
+
+ t.load = function(sid)
+ {
+ var id = this.id(sid);
+
+ return L.system.getSystemInfo().then(function(info) {
+ var date = new Date();
+ var time = info.localtime;
+
+ window.setInterval(function() {
+ date.setTime(++time * 1000);
+
+ $('#' + id).text('%04d/%02d/%02d %02d:%02d:%02d'.format(
+ date.getUTCFullYear(),
+ date.getUTCMonth() + 1,
+ date.getUTCDate(),
+ date.getUTCHours(),
+ date.getUTCMinutes(),
+ date.getUTCSeconds()
+ ));
+ }, 1000);
+ });
+ };
+
+
+ s.taboption('general', L.cbi.InputValue, 'hostname', {
+ caption: L.tr('Hostname'),
+ datatype: 'hostname'
+ });
+
+
+ var z = s.taboption('general', L.cbi.ListValue, 'zonename', {
+ caption: L.tr('Timezone')
+ });
+
+ z.load = function(sid) {
+ return $.getJSON(L.globals.resource + '/zoneinfo.json').then(function(zones) {
+ var znames = [ ];
+
+ for (var i = 0; i < zones.length; i++)
+ for (var j = 5; j < zones[i].length; j++)
+ znames.push(zones[i][j]);
+
+ znames.sort();
+
+ for (var i = 0; i < znames.length; i++)
+ z.value(znames[i]);
+
+ z.zones = zones;
+ });
+ };
+
+ z.save = function(sid)
+ {
+ var uci = this.ucipath(sid);
+ var val = this.formvalue(sid);
+
+ if (!this.callSuper('save', sid))
+ return false;
+
+ for (var i = 0; i < z.zones.length; i++)
+ for (var j = 5; j < z.zones[i].length; j++)
+ if (z.zones[i][j] == val)
+ {
+ m.set(uci.config, uci.section, 'timezone', z.zones[i][0]);
+ return true;
+ }
+
+ m.set(uci.config, uci.section, 'timezone', 'GMT0');
+ return true;
+ };
+
+
+ s.tab({
+ id: 'logging',
+ caption: L.tr('Logging')
+ });
+
+ s.taboption('logging', L.cbi.InputValue, 'log_size', {
+ caption: L.tr('System log buffer size'),
+ description: L.tr('kiB'),
+ placeholder: 16,
+ optional: true,
+ datatype: 'range(0, 32)'
+ });
+
+ s.taboption('logging', L.cbi.InputValue, 'log_ip', {
+ caption: L.tr('External system log server'),
+ placeholder: '0.0.0.0',
+ optional: true,
+ datatype: 'ip4addr'
+ });
+
+ s.taboption('logging', L.cbi.InputValue, 'log_port', {
+ caption: L.tr('External system log server port'),
+ placeholder: 514,
+ optional: true,
+ datatype: 'port'
+ });
+
+ s.taboption('logging', L.cbi.ListValue, 'conloglevel', {
+ caption: L.tr('Log output level')
+ }).value(8, L.tr('Debug'))
+ .value(7, L.tr('Info'))
+ .value(6, L.tr('Notice'))
+ .value(5, L.tr('Warning'))
+ .value(4, L.tr('Error'))
+ .value(3, L.tr('Critical'))
+ .value(2, L.tr('Alert'))
+ .value(1, L.tr('Emergency'));
+
+ s.taboption('logging', L.cbi.ListValue, 'cronloglevel', {
+ caption: L.tr('Cron Log level')
+ }).value(5, L.tr('Debug'))
+ .value(8, L.tr('Normal'))
+ .value(9, L.tr('Warning'));
+
+ s.tab({
+ id: 'language',
+ caption: L.tr('Language and Style')
+ });
+
+
+ var l = s.taboption('language', L.cbi.ListValue, 'languages', {
+ caption: L.tr('Language'),
+ uci_package: 'luci',
+ uci_section: 'main',
+ uci_option: 'lang'
+ }).value('auto', L.tr('Automatic'));
+
+ l.load = function(sid)
+ {
+ var langs = m.get('luci', 'languages');
+ for (var key in langs)
+ if (key.charAt(0) != '.')
+ l.value(key, langs[key]);
+ };
+
+
+ var t = s.taboption('language', L.cbi.ListValue, 'themes', {
+ caption: L.tr('Design'),
+ uci_package: 'luci',
+ uci_section: 'main',
+ uci_option: 'mediaurlbase'
+ });
+
+ t.load = function(sid)
+ {
+ var themes = m.get('luci', 'themes');
+ for (var key in themes)
+ if (key.charAt(0) != '.')
+ t.value(themes[key], key);
+ };
+
+
+ var s2 = m.section(L.cbi.NamedSection, 'ntp', {
+ caption: L.tr('Time Synchronization'),
+ readonly: !this.options.acls.system
+ });
+
+ var e = s2.option(L.cbi.CheckboxValue, '.enable', {
+ caption: L.tr('Enable NTP client'),
+ optional: true
+ });
+
+ e.load = function(sid) {
+ return L.system.initEnabled('sysntpd').then(function(enabled) {
+ e.options.initial = enabled;
+ });
+ };
+
+ e.save = function(sid) {
+ if (this.formvalue(sid))
+ return L.system.initEnable('sysntpd');
+ else
+ return L.system.initDisable('sysntpd');
+ };
+
+ s2.option(L.cbi.CheckboxValue, 'enable_server', {
+ caption: L.tr('Enable NTP server')
+ }).depends('.enable');
+
+ s2.option(L.cbi.DynamicList, 'server', {
+ caption: L.tr('NTP server candidates'),
+ datatype: 'host'
+ }).depends('.enable');
+
+ return m.insertInto('#map');
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/system.upgrade.js b/luci2/luci2/htdocs/luci2/view/system.upgrade.js
new file mode 100644
index 000000000..4c0799ce4
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/system.upgrade.js
@@ -0,0 +1,290 @@
+L.ui.view.extend({
+ title: L.tr('Flash operations'),
+
+ testUpgrade: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'upgrade_test',
+ expect: { '': { } }
+ }),
+
+ startUpgrade: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'upgrade_start',
+ params: [ 'keep' ]
+ }),
+
+ cleanUpgrade: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'upgrade_clean'
+ }),
+
+ restoreBackup: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'backup_restore'
+ }),
+
+ cleanBackup: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'backup_clean'
+ }),
+
+ getBackupConfig: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'backup_config_get',
+ expect: { config: '' }
+ }),
+
+ setBackupConfig: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'backup_config_set',
+ params: [ 'data' ]
+ }),
+
+ listBackup: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'backup_list',
+ expect: { files: [ ] }
+ }),
+
+ testReset: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'reset_test',
+ expect: { supported: false }
+ }),
+
+ startReset: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'reset_start'
+ }),
+
+ handleFlashUpload: function() {
+ var self = this;
+ L.ui.upload(
+ L.tr('Firmware upload'),
+ L.tr('Select the sysupgrade image to flash and click "%s" to proceed.').format(L.tr('Ok')), {
+ filename: '/tmp/firmware.bin',
+ success: function(info) {
+ self.handleFlashVerify(info);
+ }
+ }
+ );
+ },
+
+ handleFlashVerify: function(info) {
+ var self = this;
+ self.testUpgrade().then(function(res) {
+ if (res.code == 0)
+ {
+ L.ui.dialog(
+ L.tr('Verify firmware'), [
+ $('
').text(L.tr('The firmware image was uploaded completely. Please verify the checksum and file size below, then click "%s" to start the flash procedure.').format(L.tr('Ok'))),
+ $('')
+ .append($(' ')
+ .append($(' ').text(L.tr('Checksum') + ': '))
+ .append(info.checksum))
+ .append($(' ')
+ .append($(' ').text(L.tr('Size') + ': '))
+ .append('%1024mB'.format(info.size))),
+ $(' ')
+ .append($(' ')
+ .attr('type', 'checkbox')
+ .prop('checked', true))
+ .append(' ')
+ .append(L.tr('Keep configuration when reflashing'))
+ ], {
+ style: 'confirm',
+ confirm: function() {
+ //self.startUpgrade().then(function() {
+ // L.ui.reconnect();
+ //});
+
+ alert('Flash...');
+ }
+ }
+ );
+ }
+ else
+ {
+ L.ui.dialog(
+ L.tr('Invalid image'), [
+ $('
').text(L.tr('Firmware image verification failed, the "sysupgrade" command responded with the message below:')),
+ $(' ')
+ .addClass('alert-message')
+ .text(res.stdout || res.stderr),
+ $('
').text(L.tr('Image verification failed with code %d.').format(res.code))
+ ], {
+ style: 'close',
+ close: function() {
+ self.cleanUpgrade().then(function() {
+ L.ui.dialog(false);
+ });
+ }
+ }
+ );
+ }
+ });
+ },
+
+ handleBackupUpload: function() {
+ var self = this;
+ L.ui.upload(
+ L.tr('Backup restore'),
+ L.tr('Select the backup archive to restore and click "%s" to proceed.').format(L.tr('Ok')), {
+ filename: '/tmp/backup.tar.gz',
+ success: function(info) {
+ self.handleBackupVerify(info);
+ }
+ }
+ );
+ },
+
+ handleBackupVerify: function(info) {
+ var self = this;
+ L.ui.dialog(
+ L.tr('Backup restore'), [
+ $('
').text(L.tr('The backup archive was uploaded completely. Please verify the checksum and file size below, then click "%s" to restore the archive.').format(L.tr('Ok'))),
+ $('')
+ .append($(' ')
+ .append($(' ').text(L.tr('Checksum') + ': '))
+ .append(info.checksum))
+ .append($(' ')
+ .append($(' ').text(L.tr('Size') + ': '))
+ .append('%1024mB'.format(info.size)))
+ ], {
+ style: 'confirm',
+ confirm: function() {
+ self.handleBackupRestore();
+ }
+ }
+ );
+ },
+
+ handleBackupRestore: function() {
+ var self = this;
+ self.restoreBackup().then(function(res) {
+ if (res.code == 0)
+ {
+ L.ui.dialog(
+ L.tr('Backup restore'), [
+ $('
').text(L.tr('The backup was successfully restored, it is advised to reboot the system now in order to apply all configuration changes.')),
+ $(' ')
+ .addClass('cbi-button')
+ .attr('type', 'button')
+ .attr('value', L.tr('Reboot system'))
+ .click(function() { alert('Reboot...'); })
+ ], {
+ style: 'close',
+ close: function() {
+ self.cleanBackup().then(function() {
+ L.ui.dialog(false);
+ });
+ }
+ }
+ );
+ }
+ else
+ {
+ L.ui.dialog(
+ L.tr('Backup restore'), [
+ $('
').text(L.tr('Backup restoration failed, the "sysupgrade" command responded with the message below:')),
+ $(' ')
+ .addClass('alert-message')
+ .text(res.stdout || res.stderr),
+ $('
').text(L.tr('Backup restoration failed with code %d.').format(res.code))
+ ], {
+ style: 'close',
+ close: function() {
+ self.cleanBackup().then(function() {
+ L.ui.dialog(false);
+ });
+ }
+ }
+ );
+ }
+ });
+ },
+
+ handleBackupDownload: function() {
+ var form = $('#btn_backup').parent();
+
+ form.find('[name=sessionid]').val(L.globals.sid);
+ form.submit();
+ },
+
+ handleReset: function() {
+ var self = this;
+ L.ui.dialog(L.tr('Really reset all changes?'), L.tr('This will reset the system to its initial configuration, all changes made since the initial flash will be lost!'), {
+ style: 'confirm',
+ confirm: function() {
+ //self.startReset().then(function() {
+ // L.ui.reconnect();
+ //});
+
+ alert('Reset...');
+ }
+ });
+ },
+
+ execute: function() {
+ var self = this;
+
+ self.testReset().then(function(reset_avail) {
+ if (!reset_avail) {
+ $('#btn_reset').prop('disabled', true);
+ }
+
+ if (!self.options.acls.backup) {
+ $('#btn_restore, #btn_save, textarea').prop('disabled', true);
+ }
+ else {
+ $('#btn_backup').click(function() { self.handleBackupDownload(); });
+ $('#btn_restore').click(function() { self.handleBackupUpload(); });
+ }
+
+ if (!self.options.acls.upgrade) {
+ $('#btn_flash, #btn_reset').prop('disabled', true);
+ }
+ else {
+ $('#btn_flash').click(function() { self.handleFlashUpload(); });
+ $('#btn_reset').click(function() { self.handleReset(); });
+ }
+
+ return self.getBackupConfig();
+ }).then(function(config) {
+ $('textarea')
+ .attr('rows', (config.match(/\n/g) || [ ]).length + 1)
+ .val(config);
+
+ $('#btn_save')
+ .click(function() {
+ var data = ($('textarea').val() || '').replace(/\r/g, '').replace(/\n?$/, '\n');
+ L.ui.loading(true);
+ self.setBackupConfig(data).then(function() {
+ $('textarea')
+ .attr('rows', (data.match(/\n/g) || [ ]).length + 1)
+ .val(data);
+
+ L.ui.loading(false);
+ });
+ });
+
+ $('#btn_list')
+ .click(function() {
+ L.ui.loading(true);
+ self.listBackup().then(function(list) {
+ L.ui.loading(false);
+ L.ui.dialog(
+ L.tr('Backup file list'),
+ $('')
+ .css('width', '100%')
+ .attr('rows', list.length)
+ .prop('readonly', true)
+ .addClass('form-control')
+ .val(list.join('\n')),
+ { style: 'close' }
+ );
+ });
+ });
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/view/system.users.js b/luci2/luci2/htdocs/luci2/view/system.users.js
new file mode 100644
index 000000000..b467a040e
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/view/system.users.js
@@ -0,0 +1,317 @@
+L.ui.view.extend({
+ aclTable: L.cbi.AbstractValue.extend({
+ strGlob: function(pattern, match) {
+ var re = new RegExp('^' + (pattern
+ .replace(/([.?*+^$[\]\\(){}|-])/g, '\\$1')
+ .replace(/\\\*/g, '.*?')) + '$');
+
+ return re.test(match);
+ },
+
+ aclMatch: function(list, group) {
+ for (var i = 0; i < list.length; i++)
+ {
+ var x = list[i].replace(/^\s*!\s*/, '');
+ if (x == list[i])
+ continue;
+
+ if (this.strGlob(x, group))
+ return false;
+ }
+
+ for (var i = 0; i < list.length; i++)
+ {
+ var x = list[i].replace(/^\s*!\s*/, '');
+ if (x != list[i])
+ continue;
+
+ if (this.strGlob(x, group))
+ return true;
+ }
+
+ return false;
+ },
+
+ aclTest: function(list, group) {
+ for (var i = 0; i < list.length; i++)
+ if (list[i] == group)
+ return true;
+
+ return false;
+ },
+
+ aclEqual: function(list1, list2) {
+ if (list1.length != list2.length)
+ return false;
+
+ for (var i = 0; i < list1.length; i++)
+ if (list1[i] != list2[i])
+ return false;
+
+ return true;
+ },
+
+ aclFromUCI: function(value) {
+ var list;
+ if (typeof(value) == 'string')
+ list = value.split(/\s+/);
+ else if ($.isArray(value))
+ list = value;
+ else
+ list = [ ];
+
+ var rv = [ ];
+ if (this.choices)
+ for (var i = 0; i < this.choices.length; i++)
+ if (this.aclMatch(list, this.choices[i][0]))
+ rv.push(this.choices[i][0]);
+
+ return rv;
+ },
+
+ aclToUCI: function(list) {
+ if (list.length < (this.choices.length / 2))
+ return list;
+
+ var set = { };
+ for (var i = 0; i < list.length; i++)
+ set[list[i]] = true;
+
+ var rv = [ '*' ];
+ for (var i = 0; i < this.choices.length; i++)
+ if (!set[this.choices[i][0]])
+ rv.push('!' + this.choices[i][0]);
+
+ return rv;
+ },
+
+ setAll: function(ev) {
+ $(this).parents('table')
+ .find('input[value=%d]'.format(ev.data.level))
+ .prop('checked', true);
+ },
+
+ widget: function(sid)
+ {
+ var t = $('')
+ .addClass('table table-condensed table-hover')
+ .attr('id', this.id(sid))
+ .append($(' ')
+ .append($(' ')
+ .text(L.tr('ACL Group')))
+ .append($(' ')
+ .text(L.trc('No access', 'N'))
+ .attr('title', L.tr('Set all to no access'))
+ .css('cursor', 'pointer')
+ .click({ level: 0 }, this.setAll))
+ .append($(' ')
+ .text(L.trc('Read only access', 'R'))
+ .attr('title', L.tr('Set all to read only access'))
+ .css('cursor', 'pointer')
+ .click({ level: 1 }, this.setAll))
+ .append($(' ')
+ .text(L.trc('Full access', 'F'))
+ .attr('title', L.tr('Set all to full access'))
+ .css('cursor', 'pointer')
+ .click({ level: 2 }, this.setAll)));
+
+ var acl_r = this.aclFromUCI(this.ownerMap.get('rpcd', sid, 'read'));
+ var acl_w = this.aclFromUCI(this.ownerMap.get('rpcd', sid, 'write'));
+
+ if (this.choices)
+ for (var i = 0; i < this.choices.length; i++)
+ {
+ var r = t.get(0).insertRow(-1);
+ var is_r = this.aclTest(acl_r, this.choices[i][0]);
+ var is_w = this.aclTest(acl_w, this.choices[i][0]);
+
+ $(r.insertCell(-1))
+ .text(this.choices[i][1]);
+
+ for (var j = 0; j < 3; j++)
+ {
+ $(r.insertCell(-1))
+ .append($(' ')
+ .addClass('form-control')
+ .attr('type', 'radio')
+ .attr('name', '%s_%s'.format(this.id(sid), this.choices[i][0]))
+ .attr('value', j)
+ .prop('checked', (j == 0 && !is_r && !is_w) ||
+ (j == 1 && is_r && !is_w) ||
+ (j == 2 && is_w)));
+ }
+ }
+
+ return t;
+ },
+
+ textvalue: function(sid)
+ {
+ var acl_r = this.aclFromUCI(this.ownerMap.get('rpcd', sid, 'read'));
+ var acl_w = this.aclFromUCI(this.ownerMap.get('rpcd', sid, 'write'));
+
+ var htmlid = this.id(sid);
+ var radios = $('#' + htmlid + ' input');
+
+ var acls = [ ];
+
+ for (var i = 0; i < this.choices.length; i++)
+ {
+ switch (radios.filter('[name=%s_%s]:checked'.format(htmlid, this.choices[i][0])).val())
+ {
+ case '2':
+ acls.push('%s: %s'.format(this.choices[i][0], L.trc('Full access', 'F')));
+ break;
+
+ case '1':
+ acls.push('%s: %s'.format(this.choices[i][0], L.trc('Read only access', 'R')));
+ break;
+
+ case '0':
+ acls.push('%s: %s'.format(this.choices[i][0], L.trc('No access', 'N')));
+ break;
+ }
+ }
+
+ return acls.join(', ');
+ },
+
+ value: function(k, v)
+ {
+ if (!this.choices)
+ this.choices = [ ];
+
+ this.choices.push([k, v || k]);
+ return this;
+ },
+
+ save: function(sid)
+ {
+ var acl_r = this.aclFromUCI(this.ownerMap.get('rpcd', sid, 'read'));
+ var acl_w = this.aclFromUCI(this.ownerMap.get('rpcd', sid, 'write'));
+
+ var acl_r_new = [ ];
+ var acl_w_new = [ ];
+
+ var htmlid = this.id(sid);
+ var radios = $('#' + htmlid + ' input');
+
+ for (var i = 0; i < this.choices.length; i++)
+ {
+ switch (radios.filter('[name=%s_%s]:checked'.format(htmlid, this.choices[i][0])).val())
+ {
+ case '2':
+ acl_r_new.push(this.choices[i][0]);
+ acl_w_new.push(this.choices[i][0]);
+ break;
+
+ case '1':
+ acl_r_new.push(this.choices[i][0]);
+ break;
+ }
+ }
+
+ if (!this.aclEqual(acl_r, acl_r_new))
+ this.ownerMap.set('rpcd', sid, 'read', this.aclToUCI(acl_r_new));
+
+ if (!this.aclEqual(acl_w, acl_w_new))
+ this.ownerMap.set('rpcd', sid, 'write', this.aclToUCI(acl_w_new));
+ }
+ }),
+
+ execute: function() {
+ var self = this;
+ return L.ui.getAvailableACLs().then(function(acls) {
+ var m = new L.cbi.Map('rpcd', {
+ caption: L.tr('Guest Logins'),
+ description: L.tr('Manage user accounts and permissions for accessing the LuCI ui.'),
+ readonly: !self.options.acls.users
+ });
+
+ var s = m.section(L.cbi.TypedSection, 'login', {
+ caption: L.tr('Accounts'),
+ collabsible: true,
+ addremove: true,
+ add_caption: L.tr('Add account …'),
+ teasers: [ 'username', '__shadow', '__acls' ]
+ });
+
+ s.option(L.cbi.InputValue, 'username', {
+ caption: L.tr('Username'),
+ description: L.tr('Specifies the login name for the guest account'),
+ optional: false
+ });
+
+
+ var shadow = s.option(L.cbi.CheckboxValue, '__shadow', {
+ caption: L.tr('Use system account'),
+ description: L.tr('Use password from the Linux user database')
+ });
+
+ shadow.ucivalue = function(sid) {
+ var pw = this.ownerMap.get('rpcd', sid, 'password');
+ return (pw && pw.indexOf('$p$') == 0);
+ };
+
+
+ var password = s.option(L.cbi.PasswordValue, 'password', {
+ caption: L.tr('Password'),
+ description: L.tr('Specifies the password for the guest account. If you enter a plaintext password here, it will get replaced with a crypted password hash on save.'),
+ optional: false
+ });
+
+ password.depends('__shadow', false);
+
+ password.toggle = function(sid) {
+ var id = '#' + this.id(sid);
+ var pw = this.ownerMap.get('rpcd', sid, 'password');
+ var sh = this.ownerSection.fields.__shadow.formvalue(sid);
+
+ if (!sh && pw && pw.indexOf('$p$') == 0)
+ $(id).val('');
+
+ this.callSuper('toggle', sid);
+ };
+
+ shadow.save = password.save = function(sid) {
+ var sh = this.ownerSection.fields.__shadow.formvalue(sid);
+ var pw = this.ownerSection.fields.password.formvalue(sid);
+
+ if (!sh && !pw)
+ return;
+
+ if (sh)
+ pw = '$p$' + this.ownerSection.fields.username.formvalue(sid);
+
+ if (pw.match(/^\$[0-9p][a-z]?\$/))
+ {
+ if (pw != this.ownerMap.get('rpcd', sid, 'password'))
+ this.ownerMap.set('rpcd', sid, 'password', pw);
+ }
+ else
+ {
+ var map = this.ownerMap;
+ return L.ui.cryptPassword(pw).then(function(crypt) {
+ map.set('rpcd', sid, 'password', crypt);
+ });
+ }
+ };
+
+ var o = s.option(self.aclTable, '__acls', {
+ caption: L.tr('User ACLs'),
+ description: L.tr('Specifies the access levels of this account. The "N" column means no access, "R" stands for read only access and "F" for full access.')
+ });
+
+ var groups = [ ];
+ for (var group_name in acls)
+ groups.push(group_name);
+
+ groups.sort();
+
+ for (var i = 0; i < groups.length; i++)
+ o.value(groups[i], acls[groups[i]].description);
+
+ return m.insertInto('#map');
+ });
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/wireless.js b/luci2/luci2/htdocs/luci2/wireless.js
new file mode 100644
index 000000000..4d7892a45
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/wireless.js
@@ -0,0 +1,181 @@
+Class.extend({
+ listDeviceNames: L.rpc.declare({
+ object: 'iwinfo',
+ method: 'devices',
+ expect: { 'devices': [ ] },
+ filter: function(data) {
+ data.sort();
+ return data;
+ }
+ }),
+
+ getPhyName: L.rpc.declare({
+ object: 'iwinfo',
+ method: 'phyname',
+ params: [ 'section' ],
+ expect: { 'phyname': '' }
+ }),
+
+ getDeviceStatus: L.rpc.declare({
+ object: 'iwinfo',
+ method: 'info',
+ params: [ 'device' ],
+ expect: { '': { } },
+ filter: function(data, params) {
+ if (!$.isEmptyObject(data))
+ {
+ data['device'] = params['device'];
+ return data;
+ }
+ return undefined;
+ }
+ }),
+
+ getAssocList: L.rpc.declare({
+ object: 'iwinfo',
+ method: 'assoclist',
+ params: [ 'device' ],
+ expect: { results: [ ] },
+ filter: function(data, params) {
+ for (var i = 0; i < data.length; i++)
+ data[i]['device'] = params['device'];
+
+ data.sort(function(a, b) {
+ if (a.bssid < b.bssid)
+ return -1;
+ else if (a.bssid > b.bssid)
+ return 1;
+ else
+ return 0;
+ });
+
+ return data;
+ }
+ }),
+
+ getWirelessStatus: function() {
+ return this.listDeviceNames().then(function(names) {
+ L.rpc.batch();
+
+ for (var i = 0; i < names.length; i++)
+ L.wireless.getDeviceStatus(names[i]);
+
+ return L.rpc.flush();
+ }).then(function(networks) {
+ var rv = { };
+ var net_by_devname = { };
+
+ var phy_attrs = [
+ 'country', 'channel', 'frequency', 'frequency_offset',
+ 'txpower', 'txpower_offset', 'hwmodes', 'hardware', 'phy'
+ ];
+
+ var net_attrs = [
+ 'ssid', 'bssid', 'mode', 'quality', 'quality_max',
+ 'signal', 'noise', 'bitrate', 'encryption'
+ ];
+
+ for (var i = 0; i < networks.length; i++)
+ {
+ var phy = rv[networks[i].phy] || (
+ rv[networks[i].phy] = { networks: [ ] }
+ );
+
+ var net = net_by_devname[networks[i].device] = {
+ device: networks[i].device
+ };
+
+ for (var j = 0; j < phy_attrs.length; j++)
+ phy[phy_attrs[j]] = networks[i][phy_attrs[j]];
+
+ for (var j = 0; j < net_attrs.length; j++)
+ net[net_attrs[j]] = networks[i][net_attrs[j]];
+
+ /* copy parent interface properties to wds interfaces */
+ if (net.device.match(/^(.+)\.sta\d+$/) &&
+ net_by_devname[RegExp.$1])
+ {
+ var pnet = net_by_devname[RegExp.$1];
+ for (var j = 0; j < net_attrs.length; j++)
+ if (typeof(networks[i][net_attrs[j]]) === 'undefined' ||
+ net_attrs[j] == 'encryption')
+ net[net_attrs[j]] = pnet[net_attrs[j]];
+ }
+
+ phy.networks.push(net);
+ }
+
+ return rv;
+ });
+ },
+
+ getAssocLists: function()
+ {
+ return this.listDeviceNames().then(function(names) {
+ L.rpc.batch();
+
+ for (var i = 0; i < names.length; i++)
+ L.wireless.getAssocList(names[i]);
+
+ return L.rpc.flush();
+ }).then(function(assoclists) {
+ var rv = [ ];
+
+ for (var i = 0; i < assoclists.length; i++)
+ for (var j = 0; j < assoclists[i].length; j++)
+ rv.push(assoclists[i][j]);
+
+ return rv;
+ });
+ },
+
+ formatEncryption: function(enc, condensed)
+ {
+ var format_list = function(l, s)
+ {
+ var rv = [ ];
+ for (var i = 0; i < l.length; i++)
+ rv.push(l[i].toUpperCase());
+ return rv.join(s ? s : ', ');
+ }
+
+ if (!enc || !enc.enabled)
+ return L.tr('None');
+
+ if (enc.wep)
+ {
+ if (condensed)
+ return L.tr('WEP');
+ else if (enc.wep.length == 2)
+ return L.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+ else if (enc.wep[0] == 'shared')
+ return L.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+ else
+ return L.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+ }
+ else if (enc.wpa)
+ {
+ if (condensed && enc.wpa.length == 2)
+ return L.tr('WPA mixed');
+ else if (condensed)
+ return (enc.wpa[0] == 2) ? L.tr('WPA2') : L.tr('WPA');
+ else if (enc.wpa.length == 2)
+ return L.tr('mixed WPA/WPA2') + ' %s (%s)'.format(
+ format_list(enc.authentication, '/'),
+ format_list(enc.ciphers, ', ')
+ );
+ else if (enc.wpa[0] == 2)
+ return 'WPA2 %s (%s)'.format(
+ format_list(enc.authentication, '/'),
+ format_list(enc.ciphers, ', ')
+ );
+ else
+ return 'WPA %s (%s)'.format(
+ format_list(enc.authentication, '/'),
+ format_list(enc.ciphers, ', ')
+ );
+ }
+
+ return L.tr('Unknown');
+ }
+});
diff --git a/luci2/luci2/htdocs/luci2/zoneinfo.json b/luci2/luci2/htdocs/luci2/zoneinfo.json
new file mode 100644
index 000000000..5f64a448d
--- /dev/null
+++ b/luci2/luci2/htdocs/luci2/zoneinfo.json
@@ -0,0 +1 @@
+[["AFT-4:30","AFT",16200,"",0,"Asia/Kabul"],["AKST9AKDT,M3.2.0,M11.1.0","AKST",-32400,"AKDT",-28800,"America/Anchorage","America/Juneau","America/Nome","America/Sitka","America/Yakutat"],["ALMT-6","ALMT",21600,"",0,"Asia/Almaty"],["AMT-4","AMT",14400,"",0,"Asia/Yerevan"],["AMT4","AMT",-14400,"",0,"America/Boa Vista","America/Eirunepe","America/Manaus","America/Porto Velho","America/Rio Branco"],["AMT4AMST,M10.3.0/0,M2.3.0/0","AMT",-14400,"AMST",-10800,"America/Campo Grande","America/Cuiaba"],["ANAT-12","ANAT",43200,"",0,"Asia/Anadyr"],["AQTT-5","AQTT",18000,"",0,"Asia/Aqtau","Asia/Aqtobe"],["ART3","ART",-10800,"",0,"America/Argentina/Buenos Aires","America/Argentina/Catamarca","America/Argentina/Cordoba","America/Argentina/Jujuy","America/Argentina/La Rioja","America/Argentina/Mendoza","America/Argentina/Rio Gallegos","America/Argentina/Salta","America/Argentina/San Juan","America/Argentina/Tucuman","America/Argentina/Ushuaia"],["AST-3","AST",10800,"",0,"Asia/Aden","Asia/Baghdad","Asia/Bahrain","Asia/Kuwait","Asia/Qatar","Asia/Riyadh"],["AST4","AST",-14400,"",0,"America/Anguilla","America/Antigua","America/Aruba","America/Barbados","America/Blanc-Sablon","America/Curacao","America/Dominica","America/Grenada","America/Guadeloupe","America/Kralendijk","America/Lower Princes","America/Marigot","America/Martinique","America/Montserrat","America/Port of Spain","America/Puerto Rico","America/Santo Domingo","America/St Barthelemy","America/St Kitts","America/St Lucia","America/St Thomas","America/St Vincent","America/Tortola"],["AST4ADT,M3.2.0,M11.1.0","AST",-14400,"ADT",-10800,"America/Glace Bay","America/Goose Bay","America/Halifax","America/Moncton","America/Thule","Atlantic/Bermuda"],["AZOT1AZOST,M3.5.0/0,M10.5.0/1","AZOT",-3600,"AZOST",0,"Atlantic/Azores"],["AZT-4AZST,M3.5.0/4,M10.5.0/5","AZT",14400,"AZST",18000,"Asia/Baku"],["BDT-6","BDT",21600,"",0,"Asia/Dhaka"],["BNT-8","BNT",28800,"",0,"Asia/Brunei"],["BOT4","BOT",-14400,"",0,"America/La Paz"],["BRT3","BRT",-10800,"",0,"America/Araguaina","America/Belem","America/Fortaleza","America/Maceio","America/Recife","America/Santarem"],["BRT3BRST,M10.3.0/0,M2.3.0/0","BRT",-10800,"BRST",-7200,"America/Bahia","America/Sao Paulo"],["BTT-6","BTT",21600,"",0,"Asia/Thimphu"],["CAT-2","CAT",7200,"",0,"Africa/Blantyre","Africa/Bujumbura","Africa/Gaborone","Africa/Harare","Africa/Kigali","Africa/Lubumbashi","Africa/Lusaka","Africa/Maputo"],["CCT-6:30","CCT",23400,"",0,"Indian/Cocos"],["CET-1","CET",3600,"",0,"Africa/Algiers","Africa/Tunis"],["CET-1CEST,M3.5.0,M10.5.0/3","CET",3600,"CEST",7200,"Africa/Ceuta","Arctic/Longyearbyen","Europe/Amsterdam","Europe/Andorra","Europe/Belgrade","Europe/Berlin","Europe/Bratislava","Europe/Brussels","Europe/Budapest","Europe/Copenhagen","Europe/Gibraltar","Europe/Ljubljana","Europe/Luxembourg","Europe/Madrid","Europe/Malta","Europe/Monaco","Europe/Oslo","Europe/Paris","Europe/Podgorica","Europe/Prague","Europe/Rome","Europe/San Marino","Europe/Sarajevo","Europe/Skopje","Europe/Stockholm","Europe/Tirane","Europe/Vaduz","Europe/Vatican","Europe/Vienna","Europe/Warsaw","Europe/Zagreb","Europe/Zurich"],["CHAST-12:45CHADT,M9.5.0/2:45,M4.1.0/3:45","CHAST",45900,"CHADT",49500,"Pacific/Chatham"],["CHOT-8","CHOT",28800,"",0,"Asia/Choibalsan"],["CHUT-10","CHUT",36000,"",0,"Pacific/Chuuk"],["CIT-8","CIT",28800,"",0,"Asia/Makassar"],["CKT10","CKT",-36000,"",0,"Pacific/Rarotonga"],["COT5","COT",-18000,"",0,"America/Bogota"],["CST-8","CST",28800,"",0,"Asia/Chongqing","Asia/Harbin","Asia/Kashgar","Asia/Macau","Asia/Shanghai","Asia/Taipei","Asia/Urumqi"],["CST-9:30","CST",34200,"",0,"Australia/Darwin"],["CST-9:30CST,M10.1.0,M4.1.0/3","CST",34200,"CST",37800,"Australia/Adelaide","Australia/Broken Hill"],["CST5CDT,M3.2.0/0,M10.5.0/1","CST",-18000,"CDT",-14400,"America/Havana"],["CST6","CST",-21600,"",0,"America/Belize","America/Costa Rica","America/El Salvador","America/Guatemala","America/Managua","America/Regina","America/Swift Current","America/Tegucigalpa"],["CST6CDT,M3.2.0,M11.1.0","CST",-21600,"CDT",-18000,"America/Chicago","America/Indiana/Knox","America/Indiana/Tell City","America/Matamoros","America/Menominee","America/North Dakota/Beulah","America/North Dakota/Center","America/North Dakota/New Salem","America/Rainy River","America/Rankin Inlet","America/Resolute","America/Winnipeg"],["CST6CDT,M4.1.0,M10.5.0","CST",-21600,"CDT",-18000,"America/Bahia Banderas","America/Cancun","America/Merida","America/Mexico City","America/Monterrey"],["CVT1","CVT",-3600,"",0,"Atlantic/Cape Verde"],["CWST-8:45","CWST",31500,"",0,"Australia/Eucla"],["CXT-7","CXT",25200,"",0,"Indian/Christmas"],["ChST-10","",0,"",0,"Pacific/Guam","Pacific/Saipan"],["DAVT-7","DAVT",25200,"",0,"Antarctica/Davis"],["DDUT-10","DDUT",36000,"",0,"Antarctica/DumontDUrville"],["EAT-3","EAT",10800,"",0,"Africa/Addis Ababa","Africa/Asmara","Africa/Dar es Salaam","Africa/Djibouti","Africa/Juba","Africa/Kampala","Africa/Khartoum","Africa/Mogadishu","Africa/Nairobi","Indian/Antananarivo","Indian/Comoro","Indian/Mayotte"],["ECT5","ECT",-18000,"",0,"America/Guayaquil"],["EET-2","EET",7200,"",0,"Africa/Tripoli","Asia/Gaza","Asia/Hebron"],["EET-2EEST,M3.5.0/0,M10.5.0/0","EET",7200,"EEST",10800,"Asia/Beirut"],["EET-2EEST,M3.5.0/3,M10.5.0/4","EET",7200,"EEST",10800,"Asia/Nicosia","Europe/Athens","Europe/Bucharest","Europe/Chisinau","Europe/Helsinki","Europe/Istanbul","Europe/Kiev","Europe/Mariehamn","Europe/Riga","Europe/Simferopol","Europe/Sofia","Europe/Tallinn","Europe/Uzhgorod","Europe/Vilnius","Europe/Zaporozhye"],["EET-2EEST,M3.5.5/0,M10.5.5/0","EET",7200,"EEST",10800,"Asia/Damascus"],["EGT1EGST,M3.5.0/0,M10.5.0/1","EGT",-3600,"EGST",0,"America/Scoresbysund"],["EIT-9","EIT",32400,"",0,"Asia/Jayapura"],["EST-10","EST",36000,"",0,"Australia/Brisbane","Australia/Lindeman"],["EST-10EST,M10.1.0,M4.1.0/3","EST",36000,"EST",39600,"Australia/Currie","Australia/Hobart","Australia/Melbourne","Australia/Sydney"],["EST5","EST",-18000,"",0,"America/Atikokan","America/Cayman","America/Jamaica","America/Panama","America/Port-au-Prince"],["EST5EDT,M3.2.0,M11.1.0","EST",-18000,"EDT",-14400,"America/Detroit","America/Grand Turk","America/Indiana/Indianapolis","America/Indiana/Marengo","America/Indiana/Petersburg","America/Indiana/Vevay","America/Indiana/Vincennes","America/Indiana/Winamac","America/Iqaluit","America/Kentucky/Louisville","America/Kentucky/Monticello","America/Montreal","America/Nassau","America/New York","America/Nipigon","America/Pangnirtung","America/Thunder Bay","America/Toronto"],["FET-3","FET",10800,"",0,"Europe/Kaliningrad","Europe/Minsk"],["FJT-12","FJT",43200,"",0,"Pacific/Fiji"],["FKST3","FKST",-10800,"",0,"Atlantic/Stanley"],["FNT2","FNT",-7200,"",0,"America/Noronha"],["GALT6","GALT",-21600,"",0,"Pacific/Galapagos"],["GAMT9","GAMT",-32400,"",0,"Pacific/Gambier"],["GET-4","GET",14400,"",0,"Asia/Tbilisi"],["GFT3","GFT",-10800,"",0,"America/Cayenne"],["GILT-12","GILT",43200,"",0,"Pacific/Tarawa"],["GMT0","GMT",0,"",0,"Africa/Abidjan","Africa/Accra","Africa/Bamako","Africa/Banjul","Africa/Bissau","Africa/Conakry","Africa/Dakar","Africa/Freetown","Africa/Lome","Africa/Monrovia","Africa/Nouakchott","Africa/Ouagadougou","Africa/Sao Tome","America/Danmarkshavn","Atlantic/Reykjavik","Atlantic/St Helena"],["GMT0BST,M3.5.0/1,M10.5.0","GMT",0,"",0,"Europe/Guernsey","Europe/Isle of Man","Europe/Jersey","Europe/London"],["GMT0IST,M3.5.0/1,M10.5.0","GMT",0,"",0,"Europe/Dublin"],["GST-4","GST",14400,"",0,"Asia/Dubai","Asia/Muscat"],["GST2","GST",-7200,"",0,"Atlantic/South Georgia"],["GYT4","GYT",-14400,"",0,"America/Guyana"],["HAST10HADT,M3.2.0,M11.1.0","HAST",-36000,"HADT",-32400,"America/Adak"],["HKT-8","HKT",28800,"",0,"Asia/Hong Kong"],["HOVT-7","HOVT",25200,"",0,"Asia/Hovd"],["HST10","HST",-36000,"",0,"Pacific/Honolulu","Pacific/Johnston"],["ICT-7","ICT",25200,"",0,"Asia/Bangkok","Asia/Ho Chi Minh","Asia/Phnom Penh","Asia/Vientiane"],["IOT-6","IOT",21600,"",0,"Indian/Chagos"],["IRKT-9","IRKT",32400,"",0,"Asia/Irkutsk"],["IST-5:30","IST",19800,"",0,"Asia/Colombo","Asia/Kolkata"],["JST-9","JST",32400,"",0,"Asia/Tokyo"],["KGT-6","KGT",21600,"",0,"Asia/Bishkek"],["KOST-11","KOST",39600,"",0,"Pacific/Kosrae"],["KRAT-8","KRAT",28800,"",0,"Asia/Krasnoyarsk"],["KST-9","KST",32400,"",0,"Asia/Pyongyang","Asia/Seoul"],["LHST-10:30LHST-11,M10.1.0,M4.1.0","LHST",37800,"LHST",39600,"Australia/Lord Howe"],["LINT-14","LINT",50400,"",0,"Pacific/Kiritimati"],["MAGT-12","MAGT",43200,"",0,"Asia/Magadan"],["MART9:30","MART",-34200,"",0,"Pacific/Marquesas"],["MAWT-5","MAWT",18000,"",0,"Antarctica/Mawson"],["MHT-12","MHT",43200,"",0,"Pacific/Kwajalein","Pacific/Majuro"],["MIST-11","MIST",39600,"",0,"Antarctica/Macquarie"],["MMT-6:30","MMT",23400,"",0,"Asia/Rangoon"],["MSK-4","MSK",14400,"",0,"Europe/Moscow"],["MST7","MST",-25200,"",0,"America/Creston","America/Dawson Creek","America/Hermosillo","America/Phoenix"],["MST7MDT,M3.2.0,M11.1.0","MST",-25200,"MDT",-21600,"America/Boise","America/Cambridge Bay","America/Denver","America/Edmonton","America/Inuvik","America/Ojinaga","America/Shiprock","America/Yellowknife"],["MST7MDT,M4.1.0,M10.5.0","MST",-25200,"MDT",-21600,"America/Chihuahua","America/Mazatlan"],["MUT-4","MUT",14400,"",0,"Indian/Mauritius"],["MVT-5","MVT",18000,"",0,"Indian/Maldives"],["MYT-8","MYT",28800,"",0,"Asia/Kuala Lumpur","Asia/Kuching"],["MeST8","",0,"",0,"America/Metlakatla"],["NCT-11","NCT",39600,"",0,"Pacific/Noumea"],["NFT-11:30","NFT",41400,"",0,"Pacific/Norfolk"],["NOVT-7","NOVT",25200,"",0,"Asia/Novokuznetsk","Asia/Novosibirsk"],["NPT-5:45","NPT",20700,"",0,"Asia/Kathmandu"],["NRT-12","NRT",43200,"",0,"Pacific/Nauru"],["NST3:30NDT,M3.2.0,M11.1.0","NST",-12600,"NDT",-9000,"America/St Johns"],["NUT11","NUT",-39600,"",0,"Pacific/Niue"],["NZST-12NZDT,M9.5.0,M4.1.0/3","NZST",43200,"NZDT",46800,"Antarctica/McMurdo","Antarctica/South Pole","Pacific/Auckland"],["OMST-7","OMST",25200,"",0,"Asia/Omsk"],["ORAT-5","ORAT",18000,"",0,"Asia/Oral"],["PET5","PET",-18000,"",0,"America/Lima"],["PETT-12","PETT",43200,"",0,"Asia/Kamchatka"],["PGT-10","PGT",36000,"",0,"Pacific/Port Moresby"],["PHOT-13","PHOT",46800,"",0,"Pacific/Enderbury"],["PHT-8","PHT",28800,"",0,"Asia/Manila"],["PKT-5","PKT",18000,"",0,"Asia/Karachi"],["PMST3PMDT,M3.2.0,M11.1.0","PMST",-10800,"PMDT",-7200,"America/Miquelon"],["PONT-11","PONT",39600,"",0,"Pacific/Pohnpei"],["PST8","PST",-28800,"",0,"Pacific/Pitcairn"],["PST8PDT,M3.2.0,M11.1.0","PST",-28800,"PDT",-25200,"America/Dawson","America/Los Angeles","America/Tijuana","America/Vancouver","America/Whitehorse"],["PST8PDT,M4.1.0,M10.5.0","PST",-28800,"PDT",-25200,"America/Santa Isabel"],["PWT-9","PWT",32400,"",0,"Pacific/Palau"],["PYT4PYST,M10.1.0/0,M4.2.0/0","PYT",-14400,"PYST",-10800,"America/Asuncion"],["QYZT-6","QYZT",21600,"",0,"Asia/Qyzylorda"],["RET-4","RET",14400,"",0,"Indian/Reunion"],["ROTT3","ROTT",-10800,"",0,"Antarctica/Rothera"],["SAKT-11","SAKT",39600,"",0,"Asia/Sakhalin"],["SAMT-4","SAMT",14400,"",0,"Europe/Samara"],["SAST-2","SAST",7200,"",0,"Africa/Johannesburg","Africa/Maseru","Africa/Mbabane"],["SBT-11","SBT",39600,"",0,"Pacific/Guadalcanal"],["SCT-4","SCT",14400,"",0,"Indian/Mahe"],["SGT-8","SGT",28800,"",0,"Asia/Singapore"],["SRT3","SRT",-10800,"",0,"America/Paramaribo"],["SST11","SST",-39600,"",0,"Pacific/Midway","Pacific/Pago Pago"],["SYOT-3","SYOT",10800,"",0,"Antarctica/Syowa"],["TAHT10","TAHT",-36000,"",0,"Pacific/Tahiti"],["TFT-5","TFT",18000,"",0,"Indian/Kerguelen"],["TJT-5","TJT",18000,"",0,"Asia/Dushanbe"],["TKT-13","TKT",46800,"",0,"Pacific/Fakaofo"],["TLT-9","TLT",32400,"",0,"Asia/Dili"],["TMT-5","TMT",18000,"",0,"Asia/Ashgabat"],["TOT-13","TOT",46800,"",0,"Pacific/Tongatapu"],["TVT-12","TVT",43200,"",0,"Pacific/Funafuti"],["ULAT-8","ULAT",28800,"",0,"Asia/Ulaanbaatar"],["UYT3UYST,M10.1.0,M3.2.0","UYT",-10800,"UYST",-7200,"America/Montevideo"],["UZT-5","UZT",18000,"",0,"Asia/Samarkand","Asia/Tashkent"],["VET4:30","VET",-16200,"",0,"America/Caracas"],["VLAT-11","VLAT",39600,"",0,"Asia/Vladivostok"],["VOLT-4","VOLT",14400,"",0,"Europe/Volgograd"],["VOST-6","VOST",21600,"",0,"Antarctica/Vostok"],["VUT-11","VUT",39600,"",0,"Pacific/Efate"],["WAKT-12","WAKT",43200,"",0,"Pacific/Wake"],["WAT-1","WAT",3600,"",0,"Africa/Bangui","Africa/Brazzaville","Africa/Douala","Africa/Kinshasa","Africa/Lagos","Africa/Libreville","Africa/Luanda","Africa/Malabo","Africa/Ndjamena","Africa/Niamey","Africa/Porto-Novo"],["WAT-1WAST,M9.1.0,M4.1.0","WAT",3600,"WAST",7200,"Africa/Windhoek"],["WET0","WET",0,"",0,"Africa/El Aaiun"],["WET0WEST,M3.5.0/1,M10.5.0","WET",0,"",0,"Atlantic/Canary","Atlantic/Faroe","Atlantic/Madeira","Europe/Lisbon"],["WET0WEST,M4.5.0,M9.5.0/3","WET",0,"",0,"Africa/Casablanca"],["WFT-12","WFT",43200,"",0,"Pacific/Wallis"],["WIT-7","WIT",25200,"",0,"Asia/Jakarta","Asia/Pontianak"],["WST-13","WST",46800,"",0,"Pacific/Apia"],["WST-8","WST",28800,"",0,"Antarctica/Casey","Australia/Perth"],["YAKT-10","YAKT",36000,"",0,"Asia/Yakutsk"],["YEKT-6","YEKT",21600,"",0,"Asia/Yekaterinburg"]]
\ No newline at end of file
diff --git a/luci2/luci2/share/acl.d/luci2.json b/luci2/luci2/share/acl.d/luci2.json
new file mode 100644
index 000000000..30ef91c7e
--- /dev/null
+++ b/luci2/luci2/share/acl.d/luci2.json
@@ -0,0 +1,361 @@
+{
+ "unauthenticated": {
+ "description": "Functions allowed for unauthenticated requests",
+ "read": {
+ "ubus": {
+ "luci2.ui": [
+ "themes"
+ ]
+ }
+ }
+ },
+
+ "core": {
+ "description": "Core functions for LuCI",
+ "read": {
+ "ubus": {
+ "luci2.ui": [
+ "*"
+ ],
+ "session": [
+ "access",
+ "destroy"
+ ],
+ "uci": [
+ "*"
+ ]
+ }
+ }
+ },
+
+ "status": {
+ "description": "Status information display",
+ "read": {
+ "ubus": {
+ "iwinfo": [
+ "devices",
+ "info",
+ "assoclist",
+ "phyname"
+ ],
+ "system": [
+ "info",
+ "board"
+ ],
+ "network.interface": [
+ "status"
+ ],
+ "luci2.network": [
+ "conntrack_count",
+ "dhcp_leases",
+ "dhcp6_leases",
+ "arp_table",
+ "routes",
+ "routes6"
+ ],
+ "luci2.system": [
+ "diskfree",
+ "syslog",
+ "dmesg",
+ "process_list"
+ ]
+ }
+ },
+ "write": {
+ "ubus": {
+ "luci2.system": [
+ "process_signal"
+ ]
+ }
+ }
+ },
+
+ "system": {
+ "description": "General system settings",
+ "read": {
+ "ubus": {
+ "system": [
+ "info",
+ "board"
+ ],
+ "luci2.system": [
+ "init_list"
+ ]
+ },
+ "uci": [
+ "luci"
+ ]
+ },
+ "write": {
+ "ubus": {
+ "luci2.system": [
+ "init_action"
+ ]
+ },
+ "uci": [
+ "luci"
+ ]
+ }
+ },
+
+ "admin": {
+ "description": "Authentication and SSH settings",
+ "read": {
+ "ubus": {
+ "luci2.system": [
+ "sshkeys_get"
+ ]
+ },
+ "uci": [
+ "dropbear"
+ ]
+ },
+ "write": {
+ "ubus": {
+ "luci2.system": [
+ "sshkeys_set",
+ "password_set"
+ ]
+ },
+ "uci": [
+ "dropbear"
+ ]
+ }
+ },
+
+ "users": {
+ "description": "Guest login settings",
+ "read": {
+ "uci": [
+ "rpcd"
+ ]
+ },
+ "write": {
+ "uci": [
+ "rpcd"
+ ]
+ }
+ },
+
+ "software": {
+ "description": "Package management",
+ "read": {
+ "ubus": {
+ "system": [
+ "info",
+ "board"
+ ],
+ "luci2.opkg": [
+ "list",
+ "list_installed",
+ "find",
+ "config_get"
+ ]
+ }
+ },
+ "write": {
+ "ubus": {
+ "luci2.opkg": [
+ "install",
+ "remove",
+ "update",
+ "config_set"
+ ]
+ }
+ }
+ },
+
+ "upgrade": {
+ "description": "Firmware upgrade",
+ "read": {
+ "ubus": {
+ "luci2.system": [
+ "upgrade_test",
+ "reset_test"
+ ]
+ }
+ },
+ "write": {
+ "luci-io": [
+ "upload"
+ ],
+ "ubus": {
+ "luci2.system": [
+ "upgrade_start",
+ "upgrade_clean",
+ "reset_start",
+ "reboot"
+ ]
+ }
+ }
+ },
+
+ "backup": {
+ "description": "Backup and Restore",
+ "read": {
+ "luci-io": [
+ "backup"
+ ],
+ "ubus": {
+ "luci2.system": [
+ "backup_config_get",
+ "backup_list"
+ ]
+ }
+ },
+ "write": {
+ "ubus": {
+ "luci2.system": [
+ "backup_clean",
+ "backup_config_set",
+ "backup_restore",
+ "reboot"
+ ]
+ }
+ }
+ },
+
+ "startup": {
+ "description": "System boot settings",
+ "read": {
+ "ubus": {
+ "luci2.system": [
+ "init_list",
+ "rclocal_get"
+ ]
+ }
+ },
+ "write": {
+ "ubus": {
+ "luci2.system": [
+ "init_action",
+ "rclocal_set"
+ ]
+ }
+ }
+ },
+
+ "cron": {
+ "description": "Crontab management",
+ "read": {
+ "ubus": {
+ "luci2.system": [
+ "crontab_get"
+ ]
+ }
+ },
+ "write": {
+ "ubus": {
+ "luci2.system": [
+ "crontab_set"
+ ]
+ }
+ }
+ },
+
+ "leds": {
+ "description": "Hardware LED configuration",
+ "read": {
+ "ubus": {
+ "network.device": [
+ "status"
+ ],
+ "luci2.system": [
+ "led_list",
+ "usb_list"
+ ]
+ },
+ "uci": [
+ "system"
+ ]
+ },
+ "write": {
+ "uci": [
+ "system"
+ ]
+ }
+ },
+
+ "diagnostics": {
+ "description": "Network diagnostic tools",
+ "read": {
+ "ubus": {
+ "luci2.network": [
+ "ping",
+ "ping6",
+ "traceroute",
+ "traceroute6",
+ "nslookup"
+ ]
+ }
+ }
+ },
+
+ "hostnames": {
+ "description": "Host entry management",
+ "read": {
+ "uci": [
+ "dhcp"
+ ]
+ },
+ "write": {
+ "uci": [
+ "dhcp"
+ ]
+ }
+ },
+
+ "network": {
+ "description": "Network, switch and routing configuration",
+ "read": {
+ "ubus": {
+ "network": [
+ "get_proto_handlers"
+ ],
+ "network.device": [
+ "status"
+ ],
+ "network.interface": [
+ "dump"
+ ],
+ "network.wireless": [
+ "status"
+ ],
+ "luci2.network": [
+ "switch_list",
+ "switch_info",
+ "switch_status",
+ "device_list"
+ ],
+ "luci2.network.bwmon": [
+ "devices",
+ "statistics"
+ ]
+ },
+ "uci": [
+ "network",
+ "wireless"
+ ]
+ },
+ "write": {
+ "uci": [
+ "network",
+ "wireless"
+ ]
+ }
+ },
+
+ "firewall": {
+ "description": "Firewall configuration",
+ "read": {
+ "uci": [
+ "firewall"
+ ]
+ },
+ "write": {
+ "uci": [
+ "firewall"
+ ]
+ }
+ }
+}
diff --git a/luci2/luci2/share/menu.d/network.json b/luci2/luci2/share/menu.d/network.json
new file mode 100644
index 000000000..99b826bf9
--- /dev/null
+++ b/luci2/luci2/share/menu.d/network.json
@@ -0,0 +1,44 @@
+{
+ "network": {
+ "title": "Network",
+ "index": 30
+ },
+ "network/interfaces": {
+ "title": "Interfaces",
+ "acls": [ "network" ],
+ "view": "network/interfaces",
+ "index": 10
+ },
+ "network/wireless": {
+ "title": "Wireless",
+ "acls": [ "network" ],
+ "view": "network/wireless",
+ "index": 20
+ },
+ "network/switch": {
+ "title": "Switch",
+ "acls": [ "network" ],
+ "files": [ "/sbin/swconfig" ],
+ "view": "network/switch",
+ "index": 30
+ },
+ "network/hosts": {
+ "title": "Hostnames",
+ "acls": [ "hostnames" ],
+ "files": [ "/etc/config/dhcp" ],
+ "view": "network/hosts",
+ "index": 50
+ },
+ "network/routes": {
+ "title": "Routes",
+ "acls": [ "network" ],
+ "view": "network/routes",
+ "index": 70
+ },
+ "network/diagnostics": {
+ "title": "Diagnostics",
+ "acls": [ "diagnostics" ],
+ "view": "network/diagnostics",
+ "index": 80
+ }
+}
diff --git a/luci2/luci2/share/menu.d/status.json b/luci2/luci2/share/menu.d/status.json
new file mode 100644
index 000000000..b814b006f
--- /dev/null
+++ b/luci2/luci2/share/menu.d/status.json
@@ -0,0 +1,36 @@
+{
+ "status": {
+ "title": "Status",
+ "index": 10
+ },
+ "status/overview": {
+ "title": "Overview",
+ "acls": [ "status" ],
+ "view": "status/overview",
+ "index": 10
+ },
+ "status/routes": {
+ "title": "Routes",
+ "acls": [ "status" ],
+ "view": "status/routes",
+ "index": 20
+ },
+ "status/syslog": {
+ "title": "System Log",
+ "acls": [ "status" ],
+ "view": "status/syslog",
+ "index": 30
+ },
+ "status/dmesg": {
+ "title": "Kernel Log",
+ "acls": [ "status" ],
+ "view": "status/dmesg",
+ "index": 40
+ },
+ "status/processes": {
+ "title": "Processes",
+ "acls": [ "status" ],
+ "view": "status/processes",
+ "index": 50
+ }
+}
diff --git a/luci2/luci2/share/menu.d/system.json b/luci2/luci2/share/menu.d/system.json
new file mode 100644
index 000000000..3a5d45aa9
--- /dev/null
+++ b/luci2/luci2/share/menu.d/system.json
@@ -0,0 +1,56 @@
+{
+ "system": {
+ "title": "System",
+ "index": 20
+ },
+ "system/system": {
+ "title": "System",
+ "acls": [ "system" ],
+ "view": "system/system",
+ "index": 10
+ },
+ "system/admin": {
+ "title": "Administration",
+ "acls": [ "admin" ],
+ "view": "system/admin",
+ "index": 20
+ },
+ "system/users": {
+ "title": "Guest Logins",
+ "acls": [ "users" ],
+ "view": "system/users",
+ "index": 30
+ },
+ "system/software": {
+ "title": "Software",
+ "acls": [ "software" ],
+ "files": [ "/bin/opkg" ],
+ "view": "system/software",
+ "index": 40
+ },
+ "system/upgrade": {
+ "title": "Backup / Upgrade",
+ "acls": [ "backup", "upgrade" ],
+ "view": "system/upgrade",
+ "index": 50
+ },
+ "system/startup": {
+ "title": "Startup",
+ "acls": [ "startup" ],
+ "view": "system/startup",
+ "index": 60
+ },
+ "system/cron": {
+ "title": "Scheduled Tasks",
+ "acls": [ "cron" ],
+ "view": "system/cron",
+ "index": 70
+ },
+ "system/leds": {
+ "title": "LED Configuration",
+ "acls": [ "leds" ],
+ "files": [ "/etc/init.d/led" ],
+ "view": "system/leds",
+ "index": 80
+ }
+}
diff --git a/luci2/luci2/src/CMakeLists.txt b/luci2/luci2/src/CMakeLists.txt
new file mode 100644
index 000000000..6f8e928ec
--- /dev/null
+++ b/luci2/luci2/src/CMakeLists.txt
@@ -0,0 +1,6 @@
+cmake_minimum_required(VERSION 2.6)
+
+PROJECT(luci2 C)
+
+ADD_SUBDIRECTORY(io)
+ADD_SUBDIRECTORY(rpcd)
diff --git a/luci2/luci2/src/io/CMakeLists.txt b/luci2/luci2/src/io/CMakeLists.txt
new file mode 100644
index 000000000..6871db925
--- /dev/null
+++ b/luci2/luci2/src/io/CMakeLists.txt
@@ -0,0 +1,19 @@
+cmake_minimum_required(VERSION 2.6)
+
+PROJECT(luci2-io C)
+
+INCLUDE(CheckFunctionExists)
+
+ADD_DEFINITIONS(-Os -Wall -Werror --std=gnu99 -g3 -Wmissing-declarations)
+
+SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "")
+
+IF(APPLE)
+ INCLUDE_DIRECTORIES(/opt/local/include)
+ LINK_DIRECTORIES(/opt/local/lib)
+ENDIF()
+
+ADD_EXECUTABLE(luci2-io main.c multipart_parser.c)
+TARGET_LINK_LIBRARIES(luci2-io ubox ubus)
+
+INSTALL(TARGETS luci2-io RUNTIME DESTINATION sbin)
diff --git a/luci2/luci2/src/io/main.c b/luci2/luci2/src/io/main.c
new file mode 100644
index 000000000..68aaece52
--- /dev/null
+++ b/luci2/luci2/src/io/main.c
@@ -0,0 +1,644 @@
+/*
+ * luci-io - LuCI non-RPC helper
+ *
+ * Copyright (C) 2013 Jo-Philipp Wich
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "multipart_parser.h"
+
+
+enum part {
+ PART_UNKNOWN,
+ PART_SESSIONID,
+ PART_FILENAME,
+ PART_FILEMODE,
+ PART_FILEDATA
+};
+
+const char *parts[] = {
+ "(bug)",
+ "sessionid",
+ "filename",
+ "filemode",
+ "filedata",
+};
+
+struct state
+{
+ bool is_content_disposition;
+ enum part parttype;
+ char *sessionid;
+ char *filename;
+ bool filedata;
+ int filemode;
+ int filefd;
+ int tempfd;
+};
+
+enum {
+ SES_ACCESS,
+ __SES_MAX,
+};
+
+static const struct blobmsg_policy ses_policy[__SES_MAX] = {
+ [SES_ACCESS] = { .name = "access", .type = BLOBMSG_TYPE_BOOL },
+};
+
+
+static struct state st;
+
+static void
+session_access_cb(struct ubus_request *req, int type, struct blob_attr *msg)
+{
+ struct blob_attr *tb[__SES_MAX];
+ bool *allow = (bool *)req->priv;
+
+ if (!msg)
+ return;
+
+ blobmsg_parse(ses_policy, __SES_MAX, tb, blob_data(msg), blob_len(msg));
+
+ if (tb[SES_ACCESS])
+ *allow = blobmsg_get_bool(tb[SES_ACCESS]);
+}
+
+static bool
+session_access(const char *sid, const char *obj, const char *func)
+{
+ uint32_t id;
+ bool allow = false;
+ struct ubus_context *ctx;
+ static struct blob_buf req;
+
+ ctx = ubus_connect(NULL);
+
+ if (!ctx || ubus_lookup_id(ctx, "session", &id))
+ goto out;
+
+ blob_buf_init(&req, 0);
+ blobmsg_add_string(&req, "ubus_rpc_session", sid);
+ blobmsg_add_string(&req, "scope", "luci-io");
+ blobmsg_add_string(&req, "object", obj);
+ blobmsg_add_string(&req, "function", func);
+
+ ubus_invoke(ctx, id, "access", req.head, session_access_cb, &allow, 500);
+
+out:
+ if (ctx)
+ ubus_free(ctx);
+
+ return allow;
+}
+
+static char *
+md5sum(const char *file)
+{
+ pid_t pid;
+ int fds[2];
+ static char md5[33];
+
+ if (pipe(fds))
+ return NULL;
+
+ switch ((pid = fork()))
+ {
+ case -1:
+ return NULL;
+
+ case 0:
+ uloop_done();
+
+ dup2(fds[1], 1);
+
+ close(0);
+ close(2);
+ close(fds[0]);
+ close(fds[1]);
+
+ if (execl("/bin/busybox", "/bin/busybox", "md5sum", file, NULL));
+ return NULL;
+
+ break;
+
+ default:
+ memset(md5, 0, sizeof(md5));
+ read(fds[0], md5, 32);
+ waitpid(pid, NULL, 0);
+ close(fds[0]);
+ close(fds[1]);
+ }
+
+ return md5;
+}
+
+static char *
+datadup(const void *in, size_t len)
+{
+ char *out = malloc(len + 1);
+
+ if (!out)
+ return NULL;
+
+ memcpy(out, in, len);
+
+ *(out + len) = 0;
+
+ return out;
+}
+
+static bool
+urldecode(char *buf)
+{
+ char *c, *p;
+
+ if (!buf || !*buf)
+ return true;
+
+#define hex(x) \
+ (((x) <= '9') ? ((x) - '0') : \
+ (((x) <= 'F') ? ((x) - 'A' + 10) : \
+ ((x) - 'a' + 10)))
+
+ for (c = p = buf; *p; c++)
+ {
+ if (*p == '%')
+ {
+ if (!isxdigit(*(p + 1)) || !isxdigit(*(p + 2)))
+ return false;
+
+ *c = (char)(16 * hex(*(p + 1)) + hex(*(p + 2)));
+
+ p += 3;
+ }
+ else if (*p == '+')
+ {
+ *c = ' ';
+ p++;
+ }
+ else
+ {
+ *c = *p++;
+ }
+ }
+
+ *c = 0;
+
+ return true;
+}
+
+static bool
+postdecode(char **fields, int n_fields)
+{
+ char *p;
+ const char *var;
+ static char buf[1024];
+ int i, len, field, found = 0;
+
+ var = getenv("CONTENT_TYPE");
+
+ if (!var || strncmp(var, "application/x-www-form-urlencoded", 33))
+ return false;
+
+ memset(buf, 0, sizeof(buf));
+
+ if ((len = read(0, buf, sizeof(buf) - 1)) > 0)
+ {
+ for (p = buf, i = 0; i <= len; i++)
+ {
+ if (buf[i] == '=')
+ {
+ buf[i] = 0;
+
+ for (field = 0; field < (n_fields * 2); field += 2)
+ {
+ if (!strcmp(p, fields[field]))
+ {
+ fields[field + 1] = buf + i + 1;
+ found++;
+ }
+ }
+ }
+ else if (buf[i] == '&' || buf[i] == '\0')
+ {
+ buf[i] = 0;
+
+ if (found >= n_fields)
+ break;
+
+ p = buf + i + 1;
+ }
+ }
+ }
+
+ for (field = 0; field < (n_fields * 2); field += 2)
+ if (!urldecode(fields[field + 1]))
+ return false;
+
+ return (found >= n_fields);
+}
+
+static int
+response(bool success, const char *message)
+{
+ char *md5;
+ struct stat s;
+
+ printf("Status: 200 OK\r\n");
+ printf("Content-Type: text/plain\r\n\r\n{\n");
+
+ if (success)
+ {
+ if (!stat(st.filename, &s) && (md5 = md5sum(st.filename)) != NULL)
+ printf("\t\"size\": %u,\n\t\"checksum\": \"%s\"\n",
+ (unsigned int)s.st_size, md5);
+ }
+ else
+ {
+ if (message)
+ printf("\t\"message\": \"%s\",\n", message);
+
+ printf("\t\"failure\": [ %u, \"%s\" ]\n", errno, strerror(errno));
+
+ if (st.filefd > -1)
+ unlink(st.filename);
+ }
+
+ printf("}\n");
+
+ return -1;
+}
+
+static int
+failure(int e, const char *message)
+{
+ printf("Status: 500 Internal Server failure\r\n");
+ printf("Content-Type: text/plain\r\n\r\n");
+ printf("%s", message);
+
+ if (e)
+ printf(": %s", strerror(e));
+
+ return -1;
+}
+
+static int
+filecopy(void)
+{
+ int len;
+ char buf[4096];
+
+ if (!st.filedata)
+ {
+ close(st.tempfd);
+ errno = EINVAL;
+ return response(false, "No file data received");
+ }
+
+ if (lseek(st.tempfd, 0, SEEK_SET) < 0)
+ {
+ close(st.tempfd);
+ return response(false, "Failed to rewind temp file");
+ }
+
+ st.filefd = open(st.filename, O_CREAT | O_TRUNC | O_WRONLY, 0600);
+
+ if (st.filefd < 0)
+ {
+ close(st.tempfd);
+ return response(false, "Failed to open target file");
+ }
+
+ while ((len = read(st.tempfd, buf, sizeof(buf))) > 0)
+ {
+ if (write(st.filefd, buf, len) != len)
+ {
+ close(st.tempfd);
+ close(st.filefd);
+ return response(false, "I/O failure while writing target file");
+ }
+ }
+
+ close(st.tempfd);
+ close(st.filefd);
+
+ if (chmod(st.filename, st.filemode))
+ return response(false, "Failed to chmod target file");
+
+ return 0;
+}
+
+static int
+header_field(multipart_parser *p, const char *data, size_t len)
+{
+ st.is_content_disposition = !strncasecmp(data, "Content-Disposition", len);
+ return 0;
+}
+
+static int
+header_value(multipart_parser *p, const char *data, size_t len)
+{
+ int i, j;
+
+ if (!st.is_content_disposition)
+ return 0;
+
+ if (len < 10 || strncasecmp(data, "form-data", 9))
+ return 0;
+
+ for (data += 9, len -= 9; *data == ' ' || *data == ';'; data++, len--);
+
+ if (len < 8 || strncasecmp(data, "name=\"", 6))
+ return 0;
+
+ for (data += 6, len -= 6, i = 0; i <= len; i++)
+ {
+ if (*(data + i) != '"')
+ continue;
+
+ for (j = 1; j < sizeof(parts) / sizeof(parts[0]); j++)
+ if (!strncmp(data, parts[j], i))
+ st.parttype = j;
+
+ break;
+ }
+
+ return 0;
+}
+
+static int
+data_begin_cb(multipart_parser *p)
+{
+ char tmpname[24] = "/tmp/luci-upload.XXXXXX";
+
+ if (st.parttype == PART_FILEDATA)
+ {
+ if (!st.sessionid)
+ return response(false, "File data without session");
+
+ if (!st.filename)
+ return response(false, "File data without name");
+
+ st.tempfd = mkstemp(tmpname);
+
+ if (st.tempfd < 0)
+ return response(false, "Failed to create temporary file");
+
+ unlink(tmpname);
+ }
+
+ return 0;
+}
+
+static int
+data_cb(multipart_parser *p, const char *data, size_t len)
+{
+ switch (st.parttype)
+ {
+ case PART_SESSIONID:
+ st.sessionid = datadup(data, len);
+ break;
+
+ case PART_FILENAME:
+ st.filename = datadup(data, len);
+ break;
+
+ case PART_FILEMODE:
+ st.filemode = strtoul(data, NULL, 8);
+ break;
+
+ case PART_FILEDATA:
+ if (write(st.tempfd, data, len) != len)
+ {
+ close(st.tempfd);
+ return response(false, "I/O failure while writing temporary file");
+ }
+
+ if (!st.filedata)
+ st.filedata = !!len;
+
+ break;
+
+ default:
+ break;
+ }
+
+ return 0;
+}
+
+static int
+data_end_cb(multipart_parser *p)
+{
+ if (st.parttype == PART_SESSIONID)
+ {
+ if (!session_access(st.sessionid, "upload", "write"))
+ {
+ errno = EPERM;
+ return response(false, "Upload permission denied");
+ }
+ }
+ else if (st.parttype == PART_FILEDATA)
+ {
+ if (st.tempfd < 0)
+ return response(false, "Internal program failure");
+
+#if 0
+ /* prepare directory */
+ for (ptr = st.filename; *ptr; ptr++)
+ {
+ if (*ptr == '/')
+ {
+ *ptr = 0;
+
+ if (mkdir(st.filename, 0755))
+ {
+ unlink(st.tmpname);
+ return response(false, "Failed to create destination directory");
+ }
+
+ *ptr = '/';
+ }
+ }
+#endif
+
+ if (filecopy())
+ return -1;
+
+ return response(true, NULL);
+ }
+
+ st.parttype = PART_UNKNOWN;
+ return 0;
+}
+
+static multipart_parser *
+init_parser(void)
+{
+ char *boundary;
+ const char *var;
+
+ multipart_parser *p;
+ static multipart_parser_settings s = {
+ .on_part_data = data_cb,
+ .on_headers_complete = data_begin_cb,
+ .on_part_data_end = data_end_cb,
+ .on_header_field = header_field,
+ .on_header_value = header_value
+ };
+
+ var = getenv("CONTENT_TYPE");
+
+ if (!var || strncmp(var, "multipart/form-data;", 20))
+ return NULL;
+
+ for (var += 20; *var && *var != '='; var++);
+
+ if (*var++ != '=')
+ return NULL;
+
+ boundary = malloc(strlen(var) + 3);
+
+ if (!boundary)
+ return NULL;
+
+ strcpy(boundary, "--");
+ strcpy(boundary + 2, var);
+
+ st.tempfd = -1;
+ st.filefd = -1;
+ st.filemode = 0600;
+
+ p = multipart_parser_init(boundary, &s);
+
+ free(boundary);
+
+ return p;
+}
+
+static int
+main_upload(int argc, char *argv[])
+{
+ int rem, len;
+ char buf[4096];
+ multipart_parser *p;
+
+ p = init_parser();
+
+ if (!p)
+ {
+ errno = EINVAL;
+ return response(false, "Invalid request");
+ }
+
+ while ((len = read(0, buf, sizeof(buf))) > 0)
+ {
+ rem = multipart_parser_execute(p, buf, len);
+
+ if (rem < len)
+ break;
+ }
+
+ multipart_parser_free(p);
+
+ /* read remaining post data */
+ while ((len = read(0, buf, sizeof(buf))) > 0);
+
+ return 0;
+}
+
+static int
+main_backup(int argc, char **argv)
+{
+ pid_t pid;
+ time_t now;
+ int len;
+ int fds[2];
+ char buf[4096];
+ char datestr[16] = { 0 };
+ char hostname[64] = { 0 };
+ char *fields[] = { "sessionid", NULL };
+
+ if (!postdecode(fields, 1) || !session_access(fields[1], "backup", "read"))
+ return failure(0, "Backup permission denied");
+
+ if (pipe(fds))
+ return failure(errno, "Failed to spawn pipe");
+
+ switch ((pid = fork()))
+ {
+ case -1:
+ return failure(errno, "Failed to fork process");
+
+ case 0:
+ dup2(fds[1], 1);
+
+ close(0);
+ close(2);
+ close(fds[0]);
+ close(fds[1]);
+
+ chdir("/");
+
+ execl("/sbin/sysupgrade", "/sbin/sysupgrade",
+ "--create-backup", "-", NULL);
+
+ return -1;
+
+ default:
+ now = time(NULL);
+ strftime(datestr, sizeof(datestr) - 1, "%Y-%m-%d", localtime(&now));
+
+ if (gethostname(hostname, sizeof(hostname) - 1))
+ sprintf(hostname, "OpenWrt");
+
+ printf("Status: 200 OK\r\n");
+ printf("Content-Type: application/x-targz\r\n");
+ printf("Content-Disposition: attachment; "
+ "filename=\"backup-%s-%s.tar.gz\"\r\n\r\n", hostname, datestr);
+
+ while ((len = read(fds[0], buf, sizeof(buf))) > 0)
+ fwrite(buf, len, 1, stdout);
+
+ waitpid(pid, NULL, 0);
+
+ close(fds[0]);
+ close(fds[1]);
+
+ return 0;
+ }
+}
+
+int main(int argc, char **argv)
+{
+ if (strstr(argv[0], "luci-upload"))
+ return main_upload(argc, argv);
+ else if (strstr(argv[0], "luci-backup"))
+ return main_backup(argc, argv);
+
+ return -1;
+}
diff --git a/luci2/luci2/src/io/multipart_parser.c b/luci2/luci2/src/io/multipart_parser.c
new file mode 100644
index 000000000..ee82c82c8
--- /dev/null
+++ b/luci2/luci2/src/io/multipart_parser.c
@@ -0,0 +1,309 @@
+/* Based on node-formidable by Felix Geisendörfer
+ * Igor Afonov - afonov@gmail.com - 2012
+ * MIT License - http://www.opensource.org/licenses/mit-license.php
+ */
+
+#include "multipart_parser.h"
+
+#include
+#include
+#include
+
+static void multipart_log(const char * format, ...)
+{
+#ifdef DEBUG_MULTIPART
+ va_list args;
+ va_start(args, format);
+
+ fprintf(stderr, "[HTTP_MULTIPART_PARSER] %s:%d: ", __FILE__, __LINE__);
+ vfprintf(stderr, format, args);
+ fprintf(stderr, "\n");
+#endif
+}
+
+#define NOTIFY_CB(FOR) \
+do { \
+ if (p->settings->on_##FOR) { \
+ if (p->settings->on_##FOR(p) != 0) { \
+ return i; \
+ } \
+ } \
+} while (0)
+
+#define EMIT_DATA_CB(FOR, ptr, len) \
+do { \
+ if (p->settings->on_##FOR) { \
+ if (p->settings->on_##FOR(p, ptr, len) != 0) { \
+ return i; \
+ } \
+ } \
+} while (0)
+
+
+#define LF 10
+#define CR 13
+
+struct multipart_parser {
+ void * data;
+
+ size_t index;
+ size_t boundary_length;
+
+ unsigned char state;
+
+ const multipart_parser_settings* settings;
+
+ char* lookbehind;
+ char multipart_boundary[1];
+};
+
+enum state {
+ s_uninitialized = 1,
+ s_start,
+ s_start_boundary,
+ s_header_field_start,
+ s_header_field,
+ s_headers_almost_done,
+ s_header_value_start,
+ s_header_value,
+ s_header_value_almost_done,
+ s_part_data_start,
+ s_part_data,
+ s_part_data_almost_boundary,
+ s_part_data_boundary,
+ s_part_data_almost_end,
+ s_part_data_end,
+ s_part_data_final_hyphen,
+ s_end
+};
+
+multipart_parser* multipart_parser_init
+ (const char *boundary, const multipart_parser_settings* settings) {
+
+ multipart_parser* p = malloc(sizeof(multipart_parser) +
+ strlen(boundary) +
+ strlen(boundary) + 9);
+
+ strcpy(p->multipart_boundary, boundary);
+ p->boundary_length = strlen(boundary);
+
+ p->lookbehind = (p->multipart_boundary + p->boundary_length + 1);
+
+ p->index = 0;
+ p->state = s_start;
+ p->settings = settings;
+
+ return p;
+}
+
+void multipart_parser_free(multipart_parser* p) {
+ free(p);
+}
+
+void multipart_parser_set_data(multipart_parser *p, void *data) {
+ p->data = data;
+}
+
+void *multipart_parser_get_data(multipart_parser *p) {
+ return p->data;
+}
+
+size_t multipart_parser_execute(multipart_parser* p, const char *buf, size_t len) {
+ size_t i = 0;
+ size_t mark = 0;
+ char c, cl;
+ int is_last = 0;
+
+ while(i < len) {
+ c = buf[i];
+ is_last = (i == (len - 1));
+ switch (p->state) {
+ case s_start:
+ multipart_log("s_start");
+ p->index = 0;
+ p->state = s_start_boundary;
+
+ /* fallthrough */
+ case s_start_boundary:
+ multipart_log("s_start_boundary");
+ if (p->index == p->boundary_length) {
+ if (c != CR) {
+ return i;
+ }
+ p->index++;
+ break;
+ } else if (p->index == (p->boundary_length + 1)) {
+ if (c != LF) {
+ return i;
+ }
+ p->index = 0;
+ NOTIFY_CB(part_data_begin);
+ p->state = s_header_field_start;
+ break;
+ }
+ if (c != p->multipart_boundary[p->index]) {
+ return i;
+ }
+ p->index++;
+ break;
+
+ case s_header_field_start:
+ multipart_log("s_header_field_start");
+ mark = i;
+ p->state = s_header_field;
+
+ /* fallthrough */
+ case s_header_field:
+ multipart_log("s_header_field");
+ if (c == CR) {
+ p->state = s_headers_almost_done;
+ break;
+ }
+
+ if (c == '-') {
+ break;
+ }
+
+ if (c == ':') {
+ EMIT_DATA_CB(header_field, buf + mark, i - mark);
+ p->state = s_header_value_start;
+ break;
+ }
+
+ cl = tolower(c);
+ if (cl < 'a' || cl > 'z') {
+ multipart_log("invalid character in header name");
+ return i;
+ }
+ if (is_last)
+ EMIT_DATA_CB(header_field, buf + mark, (i - mark) + 1);
+ break;
+
+ case s_headers_almost_done:
+ multipart_log("s_headers_almost_done");
+ if (c != LF) {
+ return i;
+ }
+
+ p->state = s_part_data_start;
+ break;
+
+ case s_header_value_start:
+ multipart_log("s_header_value_start");
+ if (c == ' ') {
+ break;
+ }
+
+ mark = i;
+ p->state = s_header_value;
+
+ /* fallthrough */
+ case s_header_value:
+ multipart_log("s_header_value");
+ if (c == CR) {
+ EMIT_DATA_CB(header_value, buf + mark, i - mark);
+ p->state = s_header_value_almost_done;
+ }
+ if (is_last)
+ EMIT_DATA_CB(header_value, buf + mark, (i - mark) + 1);
+ break;
+
+ case s_header_value_almost_done:
+ multipart_log("s_header_value_almost_done");
+ if (c != LF) {
+ return i;
+ }
+ p->state = s_header_field_start;
+ break;
+
+ case s_part_data_start:
+ multipart_log("s_part_data_start");
+ NOTIFY_CB(headers_complete);
+ mark = i;
+ p->state = s_part_data;
+
+ /* fallthrough */
+ case s_part_data:
+ multipart_log("s_part_data");
+ if (c == CR) {
+ EMIT_DATA_CB(part_data, buf + mark, i - mark);
+ mark = i;
+ p->state = s_part_data_almost_boundary;
+ p->lookbehind[0] = CR;
+ break;
+ }
+ if (is_last)
+ EMIT_DATA_CB(part_data, buf + mark, (i - mark) + 1);
+ break;
+
+ case s_part_data_almost_boundary:
+ multipart_log("s_part_data_almost_boundary");
+ if (c == LF) {
+ p->state = s_part_data_boundary;
+ p->lookbehind[1] = LF;
+ p->index = 0;
+ break;
+ }
+ EMIT_DATA_CB(part_data, p->lookbehind, 1);
+ p->state = s_part_data;
+ mark = i --;
+ break;
+
+ case s_part_data_boundary:
+ multipart_log("s_part_data_boundary");
+ if (p->multipart_boundary[p->index] != c) {
+ EMIT_DATA_CB(part_data, p->lookbehind, 2 + p->index);
+ p->state = s_part_data;
+ mark = i --;
+ break;
+ }
+ p->lookbehind[2 + p->index] = c;
+ if ((++ p->index) == p->boundary_length) {
+ NOTIFY_CB(part_data_end);
+ p->state = s_part_data_almost_end;
+ }
+ break;
+
+ case s_part_data_almost_end:
+ multipart_log("s_part_data_almost_end");
+ if (c == '-') {
+ p->state = s_part_data_final_hyphen;
+ break;
+ }
+ if (c == CR) {
+ p->state = s_part_data_end;
+ break;
+ }
+ return i;
+
+ case s_part_data_final_hyphen:
+ multipart_log("s_part_data_final_hyphen");
+ if (c == '-') {
+ NOTIFY_CB(body_end);
+ p->state = s_end;
+ break;
+ }
+ return i;
+
+ case s_part_data_end:
+ multipart_log("s_part_data_end");
+ if (c == LF) {
+ p->state = s_header_field_start;
+ NOTIFY_CB(part_data_begin);
+ break;
+ }
+ return i;
+
+ case s_end:
+ multipart_log("s_end: %02X", (int) c);
+ break;
+
+ default:
+ multipart_log("Multipart parser unrecoverable error");
+ return 0;
+ }
+ ++ i;
+ }
+
+ return len;
+}
diff --git a/luci2/luci2/src/io/multipart_parser.h b/luci2/luci2/src/io/multipart_parser.h
new file mode 100644
index 000000000..87e67f41b
--- /dev/null
+++ b/luci2/luci2/src/io/multipart_parser.h
@@ -0,0 +1,48 @@
+/* Based on node-formidable by Felix Geisendörfer
+ * Igor Afonov - afonov@gmail.com - 2012
+ * MIT License - http://www.opensource.org/licenses/mit-license.php
+ */
+#ifndef _multipart_parser_h
+#define _multipart_parser_h
+
+#ifdef __cplusplus
+extern "C"
+{
+#endif
+
+#include
+#include
+
+typedef struct multipart_parser multipart_parser;
+typedef struct multipart_parser_settings multipart_parser_settings;
+typedef struct multipart_parser_state multipart_parser_state;
+
+typedef int (*multipart_data_cb) (multipart_parser*, const char *at, size_t length);
+typedef int (*multipart_notify_cb) (multipart_parser*);
+
+struct multipart_parser_settings {
+ multipart_data_cb on_header_field;
+ multipart_data_cb on_header_value;
+ multipart_data_cb on_part_data;
+
+ multipart_notify_cb on_part_data_begin;
+ multipart_notify_cb on_headers_complete;
+ multipart_notify_cb on_part_data_end;
+ multipart_notify_cb on_body_end;
+};
+
+multipart_parser* multipart_parser_init
+ (const char *boundary, const multipart_parser_settings* settings);
+
+void multipart_parser_free(multipart_parser* p);
+
+size_t multipart_parser_execute(multipart_parser* p, const char *buf, size_t len);
+
+void multipart_parser_set_data(multipart_parser* p, void* data);
+void * multipart_parser_get_data(multipart_parser* p);
+
+#ifdef __cplusplus
+} /* extern "C" */
+#endif
+
+#endif
diff --git a/luci2/luci2/src/rpcd/CMakeLists.txt b/luci2/luci2/src/rpcd/CMakeLists.txt
new file mode 100644
index 000000000..36219950b
--- /dev/null
+++ b/luci2/luci2/src/rpcd/CMakeLists.txt
@@ -0,0 +1,26 @@
+cmake_minimum_required(VERSION 2.6)
+
+PROJECT(luci2-plugin C)
+
+ADD_DEFINITIONS(-Os -Wall -Werror --std=gnu99 -g3 -Wmissing-declarations -Iinclude)
+
+SET(CMAKE_SHARED_LIBRARY_LINK_C_FLAGS "")
+
+IF(APPLE)
+ INCLUDE_DIRECTORIES(/opt/local/include)
+ LINK_DIRECTORIES(/opt/local/lib)
+ENDIF()
+
+FIND_LIBRARY(crypt NAMES crypt)
+IF(crypt STREQUAL "LIBS-NOTFOUND")
+ SET(crypt "")
+ENDIF()
+
+ADD_LIBRARY(luci2-plugin MODULE luci2.c)
+TARGET_LINK_LIBRARIES(luci2-plugin ubox ubus ${crypt})
+SET_TARGET_PROPERTIES(luci2-plugin PROPERTIES OUTPUT_NAME luci2 PREFIX "")
+
+ADD_LIBRARY(bwmon-plugin MODULE bwmon.c)
+SET_TARGET_PROPERTIES(bwmon-plugin PROPERTIES OUTPUT_NAME bwmon PREFIX "")
+
+INSTALL(TARGETS luci2-plugin bwmon-plugin LIBRARY DESTINATION lib)
diff --git a/luci2/luci2/src/rpcd/bwmon.c b/luci2/luci2/src/rpcd/bwmon.c
new file mode 100644
index 000000000..bb0cf3592
--- /dev/null
+++ b/luci2/luci2/src/rpcd/bwmon.c
@@ -0,0 +1,283 @@
+/*
+ * rpcd - UBUS RPC server
+ *
+ * Copyright (C) 2013 Jo-Philipp Wich
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#define RPC_BWMON_HISTORY 120
+
+
+enum {
+ RX_BYTES = 0,
+ TX_BYTES = 1,
+ RX_PACKETS = 2,
+ TX_PACKETS = 3
+};
+
+const char *types[] = {
+ "rx_bytes",
+ "tx_bytes",
+ "rx_packets",
+ "tx_packets"
+};
+
+struct rpc_bwmon_device {
+ struct avl_node avl;
+ char ifname[IF_NAMESIZE];
+ int pos;
+ uint64_t prev[4];
+ uint32_t values[4][RPC_BWMON_HISTORY];
+};
+
+static struct blob_buf buf;
+static struct avl_tree devices;
+
+
+enum {
+ RPC_STATS_DEVICE,
+ __RPC_STATS_MAX,
+};
+
+static const struct blobmsg_policy rpc_stats_policy[__RPC_STATS_MAX] = {
+ [RPC_STATS_DEVICE] = { .name = "device", .type = BLOBMSG_TYPE_STRING },
+};
+
+
+static int
+rpc_bwmon_devices(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ void *c;
+ struct rpc_bwmon_device *dev;
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "devices");
+
+ avl_for_each_element(&devices, dev, avl)
+ blobmsg_add_string(&buf, NULL, dev->ifname);
+
+ blobmsg_close_array(&buf, c);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+
+static void
+rpc_bwmon_dump_stats(struct rpc_bwmon_device *dev)
+{
+ void *c;
+ int i, j;
+
+ for (i = 0; i < 4; i++)
+ {
+ c = blobmsg_open_array(&buf, types[i]);
+
+ for (j = 0; j < RPC_BWMON_HISTORY; j++)
+ blobmsg_add_u32(&buf, NULL,
+ dev->values[i][(dev->pos + j) % RPC_BWMON_HISTORY]);
+
+ blobmsg_close_array(&buf, c);
+ }
+}
+
+static int
+rpc_bwmon_stats(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ void *c, *d;
+ struct rpc_bwmon_device *dev;
+ struct blob_attr *tb[__RPC_STATS_MAX];
+
+ blobmsg_parse(rpc_stats_policy, __RPC_STATS_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ blob_buf_init(&buf, 0);
+
+ if (tb[RPC_STATS_DEVICE])
+ {
+ dev = avl_find_element(&devices,
+ blobmsg_get_string(tb[RPC_STATS_DEVICE]),
+ dev, avl);
+
+ if (!dev)
+ return UBUS_STATUS_NOT_FOUND;
+
+ c = blobmsg_open_table(&buf, "statistics");
+ rpc_bwmon_dump_stats(dev);
+ blobmsg_close_table(&buf, c);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+ }
+
+ c = blobmsg_open_table(&buf, "statistics");
+
+ avl_for_each_element(&devices, dev, avl)
+ {
+ d = blobmsg_open_table(&buf, dev->ifname);
+ rpc_bwmon_dump_stats(dev);
+ blobmsg_close_table(&buf, d);
+ }
+
+ blobmsg_close_table(&buf, c);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+
+static uint64_t
+read_int(const char *ifname, const char *name)
+{
+ FILE *file;
+ uint64_t val = 0;
+ char buf[32] = { }, path[PATH_MAX] = { };
+
+ snprintf(path, sizeof(path) - 1, "/sys/class/net/%s/%s", ifname, name);
+
+ if ((file = fopen(path, "r")) != NULL)
+ {
+ if (fread(buf, 1, sizeof(buf) - 1, file) > 0)
+ val = strtoull(buf, NULL, 0);
+
+ fclose(file);
+ }
+
+ return val;
+}
+
+static struct rpc_bwmon_device *
+get_device(const char *ifname, bool create)
+{
+ struct rpc_bwmon_device *dev;
+
+ dev = avl_find_element(&devices, ifname, dev, avl);
+
+ if (!dev && create)
+ {
+ dev = calloc(1, sizeof(*dev));
+
+ if (!dev)
+ return NULL;
+
+ dev->pos = -1;
+ dev->avl.key = strcpy(dev->ifname, ifname);
+
+ avl_insert(&devices, &dev->avl);
+ }
+
+ return dev;
+}
+
+static void
+put_value(struct rpc_bwmon_device *dev, int type, uint64_t value)
+{
+ if (dev->pos >= 0)
+ dev->values[type][dev->pos] = (uint32_t)(value - dev->prev[type]);
+
+ dev->prev[type] = value;
+}
+
+static void
+rpc_bwmon_collect(struct uloop_timeout *t)
+{
+ DIR *dir;
+ bool up;
+ struct dirent *e;
+ struct rpc_bwmon_device *dev;
+
+ if ((dir = opendir("/sys/class/net")))
+ {
+ while ((e = readdir(dir)) != NULL)
+ {
+ up = read_int(e->d_name, "flags") & 1;
+ dev = get_device(e->d_name, up);
+
+ if (!dev)
+ continue;
+
+ put_value(dev, RX_BYTES,
+ read_int(e->d_name, "statistics/rx_bytes"));
+
+ put_value(dev, TX_BYTES,
+ read_int(e->d_name, "statistics/tx_bytes"));
+
+ put_value(dev, RX_PACKETS,
+ read_int(e->d_name, "statistics/rx_packets"));
+
+ put_value(dev, TX_PACKETS,
+ read_int(e->d_name, "statistics/tx_packets"));
+
+ dev->pos = (dev->pos + 1) % RPC_BWMON_HISTORY;
+ }
+
+ closedir(dir);
+ }
+
+ uloop_timeout_set(t, 1000);
+}
+
+
+static int
+rpc_bwmon_api_init(const struct rpc_daemon_ops *o, struct ubus_context *ctx)
+{
+ static const struct ubus_method bwmon_methods[] = {
+ UBUS_METHOD_NOARG("devices", rpc_bwmon_devices),
+ UBUS_METHOD("statistics", rpc_bwmon_stats, rpc_stats_policy)
+ };
+
+ static struct ubus_object_type bwmon_type =
+ UBUS_OBJECT_TYPE("luci-rpc-bwmon", bwmon_methods);
+
+ static struct ubus_object bwmon_obj = {
+ .name = "luci2.network.bwmon",
+ .type = &bwmon_type,
+ .methods = bwmon_methods,
+ .n_methods = ARRAY_SIZE(bwmon_methods),
+ };
+
+ static struct uloop_timeout t = {
+ .cb = rpc_bwmon_collect
+ };
+
+ avl_init(&devices, avl_strcmp, false, NULL);
+
+ uloop_timeout_set(&t, 1000);
+
+ return ubus_add_object(ctx, &bwmon_obj);
+}
+
+struct rpc_plugin rpc_plugin = {
+ .init = rpc_bwmon_api_init
+};
diff --git a/luci2/luci2/src/rpcd/luci2.c b/luci2/luci2/src/rpcd/luci2.c
new file mode 100644
index 000000000..c8ef9bf37
--- /dev/null
+++ b/luci2/luci2/src/rpcd/luci2.c
@@ -0,0 +1,2906 @@
+/*
+ * rpcd - UBUS RPC server
+ *
+ * Copyright (C) 2013 Jo-Philipp Wich
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#define _GNU_SOURCE /* crypt() */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+/* limit of log size buffer */
+#define RPC_LUCI2_MAX_LOGSIZE (128 * 1024)
+#define RPC_LUCI2_DEF_LOGSIZE (16 * 1024)
+
+/* location of menu definitions */
+#define RPC_LUCI2_MENU_FILES "/usr/share/rpcd/menu.d/*.json" /* */
+
+
+static const struct rpc_daemon_ops *ops;
+
+static struct blob_buf buf;
+static struct uci_context *cursor;
+
+enum {
+ RPC_S_PID,
+ RPC_S_SIGNAL,
+ __RPC_S_MAX,
+};
+
+static const struct blobmsg_policy rpc_signal_policy[__RPC_S_MAX] = {
+ [RPC_S_PID] = { .name = "pid", .type = BLOBMSG_TYPE_INT32 },
+ [RPC_S_SIGNAL] = { .name = "signal", .type = BLOBMSG_TYPE_INT32 },
+};
+
+enum {
+ RPC_I_NAME,
+ RPC_I_ACTION,
+ __RPC_I_MAX,
+};
+
+static const struct blobmsg_policy rpc_init_policy[__RPC_I_MAX] = {
+ [RPC_I_NAME] = { .name = "name", .type = BLOBMSG_TYPE_STRING },
+ [RPC_I_ACTION] = { .name = "action", .type = BLOBMSG_TYPE_STRING },
+};
+
+enum {
+ RPC_D_DATA,
+ __RPC_D_MAX
+};
+
+static const struct blobmsg_policy rpc_data_policy[__RPC_D_MAX] = {
+ [RPC_D_DATA] = { .name = "data", .type = BLOBMSG_TYPE_STRING },
+};
+
+enum {
+ RPC_K_KEYS,
+ __RPC_K_MAX
+};
+
+static const struct blobmsg_policy rpc_sshkey_policy[__RPC_K_MAX] = {
+ [RPC_K_KEYS] = { .name = "keys", .type = BLOBMSG_TYPE_ARRAY },
+};
+
+enum {
+ RPC_P_USER,
+ RPC_P_PASSWORD,
+ __RPC_P_MAX
+};
+
+static const struct blobmsg_policy rpc_password_policy[__RPC_P_MAX] = {
+ [RPC_P_USER] = { .name = "user", .type = BLOBMSG_TYPE_STRING },
+ [RPC_P_PASSWORD] = { .name = "password", .type = BLOBMSG_TYPE_STRING },
+};
+
+enum {
+ RPC_OM_LIMIT,
+ RPC_OM_OFFSET,
+ RPC_OM_PATTERN,
+ __RPC_OM_MAX
+};
+
+static const struct blobmsg_policy rpc_opkg_match_policy[__RPC_OM_MAX] = {
+ [RPC_OM_LIMIT] = { .name = "limit", .type = BLOBMSG_TYPE_INT32 },
+ [RPC_OM_OFFSET] = { .name = "offset", .type = BLOBMSG_TYPE_INT32 },
+ [RPC_OM_PATTERN] = { .name = "pattern", .type = BLOBMSG_TYPE_STRING },
+};
+
+enum {
+ RPC_OP_PACKAGE,
+ __RPC_OP_MAX
+};
+
+static const struct blobmsg_policy rpc_opkg_package_policy[__RPC_OP_MAX] = {
+ [RPC_OP_PACKAGE] = { .name = "package", .type = BLOBMSG_TYPE_STRING },
+};
+
+enum {
+ RPC_UPGRADE_KEEP,
+ __RPC_UPGRADE_MAX
+};
+
+static const struct blobmsg_policy rpc_upgrade_policy[__RPC_UPGRADE_MAX] = {
+ [RPC_UPGRADE_KEEP] = { .name = "keep", .type = BLOBMSG_TYPE_BOOL },
+};
+
+enum {
+ RPC_MENU_SESSION,
+ __RPC_MENU_MAX
+};
+
+static const struct blobmsg_policy rpc_menu_policy[__RPC_MENU_MAX] = {
+ [RPC_MENU_SESSION] = { .name = "ubus_rpc_session",
+ .type = BLOBMSG_TYPE_STRING },
+};
+
+enum {
+ RPC_SWITCH_NAME,
+ __RPC_SWITCH_MAX
+};
+
+static const struct blobmsg_policy rpc_switch_policy[__RPC_SWITCH_MAX] = {
+ [RPC_SWITCH_NAME] = { .name = "switch", .type = BLOBMSG_TYPE_STRING },
+};
+
+
+static int
+rpc_errno_status(void)
+{
+ switch (errno)
+ {
+ case EACCES:
+ return UBUS_STATUS_PERMISSION_DENIED;
+
+ case ENOTDIR:
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ case ENOENT:
+ return UBUS_STATUS_NOT_FOUND;
+
+ case EINVAL:
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ default:
+ return UBUS_STATUS_UNKNOWN_ERROR;
+ }
+}
+
+static void
+log_read(FILE *log, int logsize)
+{
+ int len;
+ char *logbuf;
+
+ if (logsize == 0)
+ logsize = RPC_LUCI2_DEF_LOGSIZE;
+
+ len = (logsize > RPC_LUCI2_MAX_LOGSIZE) ? RPC_LUCI2_MAX_LOGSIZE : logsize;
+ logbuf = blobmsg_alloc_string_buffer(&buf, "log", len + 1);
+
+ if (!logbuf)
+ return;
+
+ while (logsize > RPC_LUCI2_MAX_LOGSIZE)
+ {
+ len = logsize % RPC_LUCI2_MAX_LOGSIZE;
+
+ if (len == 0)
+ len = RPC_LUCI2_MAX_LOGSIZE;
+
+ fread(logbuf, 1, len, log);
+ logsize -= len;
+ }
+
+ len = fread(logbuf, 1, logsize, log);
+ *(logbuf + len) = 0;
+
+ blobmsg_add_string_buffer(&buf);
+}
+
+static int
+rpc_luci2_system_log(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *log;
+ int logsize = 0;
+ const char *logfile = NULL;
+ struct stat st;
+ struct uci_package *p;
+ struct uci_element *e;
+ struct uci_section *s;
+ struct uci_ptr ptr = { .package = "system" };
+
+ uci_load(cursor, ptr.package, &p);
+
+ if (!p)
+ return UBUS_STATUS_NOT_FOUND;
+
+ uci_foreach_element(&p->sections, e)
+ {
+ s = uci_to_section(e);
+
+ if (strcmp(s->type, "system"))
+ continue;
+
+ ptr.o = NULL;
+ ptr.option = "log_type";
+ ptr.section = e->name;
+ uci_lookup_ptr(cursor, &ptr, NULL, true);
+ break;
+ }
+
+ if (ptr.o && ptr.o->type == UCI_TYPE_STRING &&
+ !strcmp(ptr.o->v.string, "file"))
+ {
+ ptr.o = NULL;
+ ptr.option = "log_file";
+ uci_lookup_ptr(cursor, &ptr, NULL, true);
+
+ if (ptr.o && ptr.o->type == UCI_TYPE_STRING)
+ logfile = ptr.o->v.string;
+ else
+ logfile = "/var/log/messages";
+
+ if (stat(logfile, &st) || !(log = fopen(logfile, "r")))
+ goto fail;
+
+ logsize = st.st_size;
+ }
+ else
+ {
+ ptr.o = NULL;
+ ptr.option = "log_size";
+ uci_lookup_ptr(cursor, &ptr, NULL, true);
+
+ if (ptr.o && ptr.o->type == UCI_TYPE_STRING)
+ logsize = atoi(ptr.o->v.string) * 1024;
+
+ if (!(log = popen("logread", "r")))
+ goto fail;
+ }
+
+ blob_buf_init(&buf, 0);
+
+ log_read(log, logsize);
+ fclose(log);
+
+ uci_unload(cursor, p);
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+
+fail:
+ uci_unload(cursor, p);
+ return rpc_errno_status();
+}
+
+static int
+rpc_luci2_system_dmesg(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *log;
+
+ if (!(log = popen("dmesg", "r")))
+ return rpc_errno_status();
+
+ blob_buf_init(&buf, 0);
+
+ log_read(log, RPC_LUCI2_MAX_LOGSIZE);
+ fclose(log);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_system_diskfree(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ int i;
+ void *c;
+ struct statvfs s;
+ const char *fslist[] = {
+ "/", "root",
+ "/tmp", "tmp",
+ };
+
+ blob_buf_init(&buf, 0);
+
+ for (i = 0; i < sizeof(fslist) / sizeof(fslist[0]); i += 2)
+ {
+ if (statvfs(fslist[i], &s))
+ continue;
+
+ c = blobmsg_open_table(&buf, fslist[i+1]);
+
+ blobmsg_add_u32(&buf, "total", s.f_blocks * s.f_frsize);
+ blobmsg_add_u32(&buf, "free", s.f_bfree * s.f_frsize);
+ blobmsg_add_u32(&buf, "used", (s.f_blocks - s.f_bfree) * s.f_frsize);
+
+ blobmsg_close_table(&buf, c);
+ }
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_process_list(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *top;
+ void *c, *d;
+ char line[1024];
+ char *pid, *ppid, *user, *stat, *vsz, *pvsz, *pcpu, *cmd;
+
+ if (!(top = popen("/bin/busybox top -bn1", "r")))
+ return rpc_errno_status();
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "processes");
+
+ while (fgets(line, sizeof(line) - 1, top))
+ {
+ pid = strtok(line, " ");
+
+ if (*pid < '0' || *pid > '9')
+ continue;
+
+ ppid = strtok(NULL, " ");
+ user = strtok(NULL, " ");
+ stat = strtok(NULL, " ");
+
+ if (!stat)
+ continue;
+
+ if (!*(stat + 1))
+ *(stat + 1) = ' ';
+
+ if (!*(stat + 2))
+ *(stat + 2) = ' ';
+
+ *(stat + 3) = 0;
+
+ vsz = strtok(stat + 4, " ");
+ pvsz = strtok(NULL, " ");
+ pcpu = strtok(NULL, " ");
+ cmd = strtok(NULL, "\n");
+
+ if (!cmd)
+ continue;
+
+ d = blobmsg_open_table(&buf, NULL);
+
+ blobmsg_add_u32(&buf, "pid", atoi(pid));
+ blobmsg_add_u32(&buf, "ppid", atoi(ppid));
+ blobmsg_add_string(&buf, "user", user);
+ blobmsg_add_string(&buf, "stat", stat);
+ blobmsg_add_u32(&buf, "vsize", atoi(vsz) * 1024);
+ blobmsg_add_u32(&buf, "vsize_percent", atoi(pvsz));
+ blobmsg_add_u32(&buf, "cpu_percent", atoi(pcpu));
+ blobmsg_add_string(&buf, "command", cmd);
+
+ blobmsg_close_table(&buf, d);
+ }
+
+ fclose(top);
+ blobmsg_close_array(&buf, c);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_process_signal(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ int pid, sig;
+ struct blob_attr *tb[__RPC_S_MAX];
+
+ blobmsg_parse(rpc_signal_policy, __RPC_S_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_S_SIGNAL] || !tb[RPC_S_PID])
+ {
+ errno = EINVAL;
+ return rpc_errno_status();
+ }
+
+ pid = blobmsg_get_u32(tb[RPC_S_PID]);
+ sig = blobmsg_get_u32(tb[RPC_S_SIGNAL]);
+
+ if (kill(pid, sig))
+ return rpc_errno_status();
+
+ return 0;
+}
+
+static int
+rpc_luci2_init_list(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ int n;
+ void *c, *t;
+ char *p, path[PATH_MAX];
+ struct stat s;
+ struct dirent *e;
+ FILE *f;
+ DIR *d;
+
+ if (!(d = opendir("/etc/init.d")))
+ return rpc_errno_status();
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "initscripts");
+
+ while ((e = readdir(d)) != NULL)
+ {
+ snprintf(path, sizeof(path) - 1, "/etc/init.d/%s", e->d_name);
+
+ if (stat(path, &s) || !S_ISREG(s.st_mode) || !(s.st_mode & S_IXUSR))
+ continue;
+
+ if ((f = fopen(path, "r")) != NULL)
+ {
+ n = -1;
+ p = fgets(path, sizeof(path) - 1, f);
+
+ if (!p || !strstr(p, "/etc/rc.common"))
+ goto skip;
+
+ t = blobmsg_open_table(&buf, NULL);
+
+ blobmsg_add_string(&buf, "name", e->d_name);
+
+ while (fgets(path, sizeof(path) - 1, f))
+ {
+ p = strtok(path, "= \t");
+
+ if (!strcmp(p, "START") && !!(p = strtok(NULL, "= \t\n")))
+ {
+ n = atoi(p);
+ blobmsg_add_u32(&buf, "start", n);
+ }
+ else if (!strcmp(p, "STOP") && !!(p = strtok(NULL, "= \t\n")))
+ {
+ blobmsg_add_u32(&buf, "stop", atoi(p));
+ break;
+ }
+ }
+
+ if (n > -1)
+ {
+ snprintf(path, sizeof(path) - 1, "/etc/rc.d/S%02d%s",
+ n, e->d_name);
+
+ blobmsg_add_u8(&buf, "enabled",
+ (!stat(path, &s) && (s.st_mode & S_IXUSR)));
+ }
+ else
+ {
+ blobmsg_add_u8(&buf, "enabled", 0);
+ }
+
+ blobmsg_close_table(&buf, t);
+
+skip:
+ fclose(f);
+ }
+ }
+
+ closedir(d);
+ blobmsg_close_array(&buf, c);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_init_action(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ int fd;
+ pid_t pid;
+ struct stat s;
+ char path[PATH_MAX];
+ const char *action;
+ struct blob_attr *tb[__RPC_I_MAX];
+
+ blobmsg_parse(rpc_init_policy, __RPC_I_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_I_NAME] || !tb[RPC_I_ACTION])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ action = blobmsg_data(tb[RPC_I_ACTION]);
+
+ if (strcmp(action, "start") && strcmp(action, "stop") &&
+ strcmp(action, "reload") && strcmp(action, "restart") &&
+ strcmp(action, "enable") && strcmp(action, "disable"))
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ snprintf(path, sizeof(path) - 1, "/etc/init.d/%s",
+ (char *)blobmsg_data(tb[RPC_I_NAME]));
+
+ if (stat(path, &s))
+ return rpc_errno_status();
+
+ if (!(s.st_mode & S_IXUSR))
+ return UBUS_STATUS_PERMISSION_DENIED;
+
+ switch ((pid = fork()))
+ {
+ case -1:
+ return rpc_errno_status();
+
+ case 0:
+ uloop_done();
+
+ if ((fd = open("/dev/null", O_RDWR)) > -1)
+ {
+ dup2(fd, 0);
+ dup2(fd, 1);
+ dup2(fd, 2);
+
+ close(fd);
+ }
+
+ chdir("/");
+
+ if (execl(path, path, action, NULL))
+ return rpc_errno_status();
+
+ default:
+ return 0;
+ }
+}
+
+static int
+rpc_luci2_rclocal_get(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ char data[4096] = { 0 };
+
+ if (!(f = fopen("/etc/rc.local", "r")))
+ return rpc_errno_status();
+
+ fread(data, sizeof(data) - 1, 1, f);
+ fclose(f);
+
+ blob_buf_init(&buf, 0);
+ blobmsg_add_string(&buf, "data", data);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_rclocal_set(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ struct blob_attr *tb[__RPC_D_MAX];
+
+ blobmsg_parse(rpc_data_policy, __RPC_D_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_D_DATA] || blobmsg_data_len(tb[RPC_D_DATA]) >= 4096)
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ if (!(f = fopen("/etc/rc.local", "w")))
+ return rpc_errno_status();
+
+ fwrite(blobmsg_data(tb[RPC_D_DATA]),
+ blobmsg_data_len(tb[RPC_D_DATA]) - 1, 1, f);
+
+ fclose(f);
+ return 0;
+}
+
+static int
+rpc_luci2_crontab_get(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ char data[4096] = { 0 };
+
+ if (!(f = fopen("/etc/crontabs/root", "r")))
+ return rpc_errno_status();
+
+ fread(data, sizeof(data) - 1, 1, f);
+ fclose(f);
+
+ blob_buf_init(&buf, 0);
+ blobmsg_add_string(&buf, "data", data);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_crontab_set(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ struct stat s;
+ struct blob_attr *tb[__RPC_D_MAX];
+
+ blobmsg_parse(rpc_data_policy, __RPC_D_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_D_DATA] || blobmsg_data_len(tb[RPC_D_DATA]) >= 4096)
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ if (stat("/etc/crontabs", &s) && mkdir("/etc/crontabs", 0755))
+ return rpc_errno_status();
+
+ if (!(f = fopen("/etc/crontabs/root", "w")))
+ return rpc_errno_status();
+
+ fwrite(blobmsg_data(tb[RPC_D_DATA]),
+ blobmsg_data_len(tb[RPC_D_DATA]) - 1, 1, f);
+
+ fclose(f);
+ return 0;
+}
+
+static int
+rpc_luci2_sshkeys_get(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ void *c;
+ char *p, line[4096];
+
+ if (!(f = fopen("/etc/dropbear/authorized_keys", "r")))
+ return rpc_errno_status();
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "keys");
+
+ while (fgets(line, sizeof(line) - 1, f))
+ {
+ for (p = line + strlen(line) - 1; (p > line) && isspace(*p); p--)
+ *p = 0;
+
+ for (p = line; isspace(*p); p++)
+ *p = 0;
+
+ if (*p)
+ blobmsg_add_string(&buf, NULL, p);
+ }
+
+ blobmsg_close_array(&buf, c);
+ fclose(f);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_sshkeys_set(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ int rem;
+ struct blob_attr *cur, *tb[__RPC_K_MAX];
+
+ blobmsg_parse(rpc_sshkey_policy, __RPC_K_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_K_KEYS])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ if (!(f = fopen("/etc/dropbear/authorized_keys", "w")))
+ return rpc_errno_status();
+
+ blobmsg_for_each_attr(cur, tb[RPC_K_KEYS], rem)
+ {
+ if (blobmsg_type(cur) != BLOBMSG_TYPE_STRING)
+ continue;
+
+ fwrite(blobmsg_data(cur), blobmsg_data_len(cur) - 1, 1, f);
+ fwrite("\n", 1, 1, f);
+ }
+
+ fclose(f);
+ return 0;
+}
+
+static int
+rpc_luci2_password_set(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ pid_t pid;
+ int fd, fds[2];
+ struct stat s;
+ struct blob_attr *tb[__RPC_P_MAX];
+
+ blobmsg_parse(rpc_password_policy, __RPC_P_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_P_USER] || !tb[RPC_P_PASSWORD])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ if (stat("/usr/bin/passwd", &s))
+ return UBUS_STATUS_NOT_FOUND;
+
+ if (!(s.st_mode & S_IXUSR))
+ return UBUS_STATUS_PERMISSION_DENIED;
+
+ if (pipe(fds))
+ return rpc_errno_status();
+
+ switch ((pid = fork()))
+ {
+ case -1:
+ close(fds[0]);
+ close(fds[1]);
+ return rpc_errno_status();
+
+ case 0:
+ uloop_done();
+
+ dup2(fds[0], 0);
+ close(fds[0]);
+ close(fds[1]);
+
+ if ((fd = open("/dev/null", O_RDWR)) > -1)
+ {
+ dup2(fd, 1);
+ dup2(fd, 2);
+ close(fd);
+ }
+
+ chdir("/");
+
+ if (execl("/usr/bin/passwd", "/usr/bin/passwd",
+ blobmsg_data(tb[RPC_P_USER]), NULL))
+ return rpc_errno_status();
+
+ default:
+ close(fds[0]);
+
+ write(fds[1], blobmsg_data(tb[RPC_P_PASSWORD]),
+ blobmsg_data_len(tb[RPC_P_PASSWORD]) - 1);
+ write(fds[1], "\n", 1);
+
+ usleep(100 * 1000);
+
+ write(fds[1], blobmsg_data(tb[RPC_P_PASSWORD]),
+ blobmsg_data_len(tb[RPC_P_PASSWORD]) - 1);
+ write(fds[1], "\n", 1);
+
+ close(fds[1]);
+
+ waitpid(pid, NULL, 0);
+
+ return 0;
+ }
+}
+
+static int
+rpc_luci2_led_list(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ DIR *d;
+ FILE *f;
+ void *list, *led, *trigger;
+ char *p, *active_trigger, line[512];
+ struct dirent *e;
+
+ if (!(d = opendir("/sys/class/leds")))
+ return rpc_errno_status();
+
+ blob_buf_init(&buf, 0);
+ list = blobmsg_open_array(&buf, "leds");
+
+ while ((e = readdir(d)) != NULL)
+ {
+ snprintf(line, sizeof(line) - 1, "/sys/class/leds/%s/trigger",
+ e->d_name);
+
+ if (!(f = fopen(line, "r")))
+ continue;
+
+ led = blobmsg_open_table(&buf, NULL);
+
+ blobmsg_add_string(&buf, "name", e->d_name);
+
+ if (fgets(line, sizeof(line) - 1, f))
+ {
+ trigger = blobmsg_open_array(&buf, "triggers");
+
+ for (p = strtok(line, " \n"), active_trigger = NULL;
+ p != NULL;
+ p = strtok(NULL, " \n"))
+ {
+ if (*p == '[')
+ {
+ *(p + strlen(p) - 1) = 0;
+ *p++ = 0;
+ active_trigger = p;
+ }
+
+ blobmsg_add_string(&buf, NULL, p);
+ }
+
+ blobmsg_close_array(&buf, trigger);
+
+ if (active_trigger)
+ blobmsg_add_string(&buf, "active_trigger", active_trigger);
+ }
+
+ fclose(f);
+
+ snprintf(line, sizeof(line) - 1, "/sys/class/leds/%s/brightness",
+ e->d_name);
+
+ if ((f = fopen(line, "r")) != NULL)
+ {
+ if (fgets(line, sizeof(line) - 1, f))
+ blobmsg_add_u32(&buf, "brightness", atoi(line));
+
+ fclose(f);
+ }
+
+ snprintf(line, sizeof(line) - 1, "/sys/class/leds/%s/max_brightness",
+ e->d_name);
+
+ if ((f = fopen(line, "r")) != NULL)
+ {
+ if (fgets(line, sizeof(line) - 1, f))
+ blobmsg_add_u32(&buf, "max_brightness", atoi(line));
+
+ fclose(f);
+ }
+
+ blobmsg_close_table(&buf, led);
+ }
+
+ closedir(d);
+
+ blobmsg_close_array(&buf, list);
+ ubus_send_reply(ctx, req, buf.head);
+
+ return 0;
+}
+
+static int
+rpc_luci2_usb_list(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ DIR *d;
+ FILE *f;
+ int i;
+ void *list, *device;
+ char *p, line[512];
+ struct stat s;
+ struct dirent *e;
+
+ const char *attributes[] = {
+ "manufacturer", "vendor_name", "s",
+ "product", "product_name", "s",
+ "idVendor", "vendor_id", "x",
+ "idProduct", "product_id", "x",
+ "serial", "serial", "s",
+ "speed", "speed", "d",
+ };
+
+ if (!(d = opendir("/sys/bus/usb/devices")))
+ return rpc_errno_status();
+
+ blob_buf_init(&buf, 0);
+ list = blobmsg_open_array(&buf, "devices");
+
+ while ((e = readdir(d)) != NULL)
+ {
+ if (e->d_name[0] < '0' || e->d_name[0] > '9')
+ continue;
+
+ snprintf(line, sizeof(line) - 1,
+ "/sys/bus/usb/devices/%s/%s", e->d_name, attributes[0]);
+
+ if (stat(line, &s))
+ continue;
+
+ device = blobmsg_open_table(&buf, NULL);
+
+ blobmsg_add_string(&buf, "name", e->d_name);
+
+ for (i = 0; i < sizeof(attributes) / sizeof(attributes[0]); i += 3)
+ {
+ snprintf(line, sizeof(line) - 1,
+ "/sys/bus/usb/devices/%s/%s", e->d_name, attributes[i]);
+
+ if (!(f = fopen(line, "r")))
+ continue;
+
+ if (fgets(line, sizeof(line) - 1, f))
+ {
+ switch (*attributes[i+2])
+ {
+ case 'x':
+ blobmsg_add_u32(&buf, attributes[i+1],
+ strtoul(line, NULL, 16));
+ break;
+
+ case 'd':
+ blobmsg_add_u32(&buf, attributes[i+1],
+ strtoul(line, NULL, 10));
+ break;
+
+ default:
+ if ((p = strchr(line, '\n')) != NULL)
+ while (p > line && isspace(*p))
+ *p-- = 0;
+
+ blobmsg_add_string(&buf, attributes[i+1], line);
+ break;
+ }
+ }
+
+ fclose(f);
+ }
+
+ blobmsg_close_table(&buf, device);
+ }
+
+ closedir(d);
+
+ blobmsg_close_array(&buf, list);
+ ubus_send_reply(ctx, req, buf.head);
+
+ return 0;
+}
+
+static int
+rpc_luci2_upgrade_test(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ const char *cmd[4] = { "sysupgrade", "--test", "/tmp/firmware.bin", NULL };
+ return ops->exec(cmd, NULL, NULL, NULL, NULL, NULL, ctx, req);
+}
+
+static int
+rpc_luci2_upgrade_start(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return 0;
+}
+
+static int
+rpc_luci2_upgrade_clean(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ if (unlink("/tmp/firmware.bin"))
+ return rpc_errno_status();
+
+ return 0;
+}
+
+static int
+rpc_luci2_backup_restore(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ const char *cmd[4] = { "sysupgrade", "--restore-backup",
+ "/tmp/backup.tar.gz", NULL };
+
+ return ops->exec(cmd, NULL, NULL, NULL, NULL, NULL, ctx, req);
+}
+
+static int
+rpc_luci2_backup_clean(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ if (unlink("/tmp/backup.tar.gz"))
+ return rpc_errno_status();
+
+ return 0;
+}
+
+static int
+rpc_luci2_backup_config_get(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ char conf[2048] = { 0 };
+
+ if (!(f = fopen("/etc/sysupgrade.conf", "r")))
+ return rpc_errno_status();
+
+ fread(conf, sizeof(conf) - 1, 1, f);
+ fclose(f);
+
+ blob_buf_init(&buf, 0);
+ blobmsg_add_string(&buf, "config", conf);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_backup_config_set(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ struct blob_attr *tb[__RPC_D_MAX];
+
+ blobmsg_parse(rpc_data_policy, __RPC_D_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_D_DATA])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ if (blobmsg_data_len(tb[RPC_D_DATA]) >= 2048)
+ return UBUS_STATUS_NOT_SUPPORTED;
+
+ if (!(f = fopen("/etc/sysupgrade.conf", "w")))
+ return rpc_errno_status();
+
+ fwrite(blobmsg_data(tb[RPC_D_DATA]),
+ blobmsg_data_len(tb[RPC_D_DATA]) - 1, 1, f);
+
+ fclose(f);
+ return 0;
+}
+
+struct backup_state {
+ bool open;
+ void *array;
+};
+
+static int
+backup_parse_list(struct blob_buf *blob, char *buf, int len, void *priv)
+{
+ struct backup_state *s = priv;
+ char *nl = strchr(buf, '\n');
+
+ if (!nl)
+ return 0;
+
+ if (!s->open)
+ {
+ s->open = true;
+ s->array = blobmsg_open_array(blob, "files");
+ }
+
+ *nl = 0;
+ blobmsg_add_string(blob, NULL, buf);
+
+ return (nl - buf + 1);
+}
+
+static int
+backup_finish_list(struct blob_buf *blob, int status, void *priv)
+{
+ struct backup_state *s = priv;
+
+ if (!s->open)
+ return UBUS_STATUS_NO_DATA;
+
+ blobmsg_close_array(blob, s->array);
+
+ return UBUS_STATUS_OK;
+}
+
+static int
+rpc_luci2_backup_list(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct backup_state *state = NULL;
+ const char *cmd[3] = { "sysupgrade", "--list-backup", NULL };
+
+ state = malloc(sizeof(*state));
+
+ if (!state)
+ return UBUS_STATUS_UNKNOWN_ERROR;
+
+ memset(state, 0, sizeof(*state));
+
+ return ops->exec(cmd, NULL, backup_parse_list, NULL, backup_finish_list,
+ state, ctx, req);
+}
+
+static int
+rpc_luci2_reset_test(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *mtd;
+ struct stat s;
+ char line[64] = { 0 };
+ bool supported = false;
+
+ if (!stat("/sbin/mtd", &s) && (s.st_mode & S_IXUSR))
+ {
+ if ((mtd = fopen("/proc/mtd", "r")) != NULL)
+ {
+ while (fgets(line, sizeof(line) - 1, mtd))
+ {
+ if (strstr(line, "\"rootfs_data\""))
+ {
+ supported = true;
+ break;
+ }
+ }
+
+ fclose(mtd);
+ }
+ }
+
+ blob_buf_init(&buf, 0);
+ blobmsg_add_u8(&buf, "supported", supported);
+
+ ubus_send_reply(ctx, req, buf.head);
+
+ return 0;
+}
+
+static int
+rpc_luci2_reset_start(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ switch (fork())
+ {
+ case -1:
+ return rpc_errno_status();
+
+ case 0:
+ uloop_done();
+
+ chdir("/");
+
+ close(0);
+ close(1);
+ close(2);
+
+ sleep(1);
+
+ execl("/sbin/mtd", "/sbin/mtd", "-r", "erase", "rootfs_data", NULL);
+
+ return rpc_errno_status();
+
+ default:
+ return 0;
+ }
+}
+
+static int
+rpc_luci2_reboot(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ switch (fork())
+ {
+ case -1:
+ return rpc_errno_status();
+
+ case 0:
+ chdir("/");
+
+ close(0);
+ close(1);
+ close(2);
+
+ sleep(1);
+
+ execl("/sbin/reboot", "/sbin/reboot", NULL);
+
+ return rpc_errno_status();
+
+ default:
+ return 0;
+ }
+}
+
+
+static FILE *
+dnsmasq_leasefile(void)
+{
+ FILE *leases = NULL;
+ struct uci_package *p;
+ struct uci_element *e;
+ struct uci_section *s;
+ struct uci_ptr ptr = {
+ .package = "dhcp",
+ .section = NULL,
+ .option = "leasefile"
+ };
+
+ uci_load(cursor, ptr.package, &p);
+
+ if (!p)
+ return NULL;
+
+ uci_foreach_element(&p->sections, e)
+ {
+ s = uci_to_section(e);
+
+ if (strcmp(s->type, "dnsmasq"))
+ continue;
+
+ ptr.section = e->name;
+ uci_lookup_ptr(cursor, &ptr, NULL, true);
+ break;
+ }
+
+ if (ptr.o && ptr.o->type == UCI_TYPE_STRING)
+ leases = fopen(ptr.o->v.string, "r");
+
+ uci_unload(cursor, p);
+
+ return leases;
+}
+
+static int
+rpc_luci2_network_leases(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *leases;
+ void *c, *d;
+ char line[128];
+ char *ts, *mac, *addr, *name;
+ time_t now = time(NULL);
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "leases");
+
+ leases = dnsmasq_leasefile();
+
+ if (!leases)
+ goto out;
+
+ while (fgets(line, sizeof(line) - 1, leases))
+ {
+ ts = strtok(line, " \t");
+ mac = strtok(NULL, " \t");
+ addr = strtok(NULL, " \t");
+ name = strtok(NULL, " \t");
+
+ if (!ts || !mac || !addr || !name)
+ continue;
+
+ if (strchr(addr, ':'))
+ continue;
+
+ d = blobmsg_open_table(&buf, NULL);
+
+ blobmsg_add_u32(&buf, "expires", atoi(ts) - now);
+ blobmsg_add_string(&buf, "macaddr", mac);
+ blobmsg_add_string(&buf, "ipaddr", addr);
+
+ if (strcmp(name, "*"))
+ blobmsg_add_string(&buf, "hostname", name);
+
+ blobmsg_close_table(&buf, d);
+ }
+
+ fclose(leases);
+
+out:
+ blobmsg_close_array(&buf, c);
+ ubus_send_reply(ctx, req, buf.head);
+
+ return 0;
+}
+
+static int
+rpc_luci2_network_leases6(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *leases;
+ void *c, *d;
+ char line[128];
+ char *ts, *mac, *addr, *name, *duid;
+ time_t now = time(NULL);
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "leases");
+
+ leases = fopen("/tmp/hosts/6relayd", "r");
+
+ if (leases)
+ {
+ while (fgets(line, sizeof(line) - 1, leases))
+ {
+ if (strncmp(line, "# ", 2))
+ continue;
+
+ strtok(line + 2, " \t"); /* iface */
+
+ duid = strtok(NULL, " \t");
+
+ strtok(NULL, " \t"); /* iaid */
+
+ name = strtok(NULL, " \t");
+ ts = strtok(NULL, " \t");
+
+ strtok(NULL, " \t"); /* id */
+ strtok(NULL, " \t"); /* length */
+
+ addr = strtok(NULL, " \t\n");
+
+ if (!addr)
+ continue;
+
+ d = blobmsg_open_table(&buf, NULL);
+
+ blobmsg_add_u32(&buf, "expires", atoi(ts) - now);
+ blobmsg_add_string(&buf, "duid", duid);
+ blobmsg_add_string(&buf, "ip6addr", addr);
+
+ if (strcmp(name, "-"))
+ blobmsg_add_string(&buf, "hostname", name);
+
+ blobmsg_close_array(&buf, d);
+ }
+
+ fclose(leases);
+ }
+ else
+ {
+ leases = dnsmasq_leasefile();
+
+ if (!leases)
+ goto out;
+
+ while (fgets(line, sizeof(line) - 1, leases))
+ {
+ ts = strtok(line, " \t");
+ mac = strtok(NULL, " \t");
+ addr = strtok(NULL, " \t");
+ name = strtok(NULL, " \t");
+ duid = strtok(NULL, " \t\n");
+
+ if (!ts || !mac || !addr || !duid)
+ continue;
+
+ if (!strchr(addr, ':'))
+ continue;
+
+ d = blobmsg_open_table(&buf, NULL);
+
+ blobmsg_add_u32(&buf, "expires", atoi(ts) - now);
+ blobmsg_add_string(&buf, "macaddr", mac);
+ blobmsg_add_string(&buf, "ip6addr", addr);
+
+ if (strcmp(name, "*"))
+ blobmsg_add_string(&buf, "hostname", name);
+
+ if (strcmp(duid, "*"))
+ blobmsg_add_string(&buf, "duid", name);
+
+ blobmsg_close_table(&buf, d);
+ }
+
+ fclose(leases);
+ }
+
+out:
+ blobmsg_close_array(&buf, c);
+ ubus_send_reply(ctx, req, buf.head);
+
+ return 0;
+}
+
+static int
+rpc_luci2_network_ct_count(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ char line[128];
+
+ blob_buf_init(&buf, 0);
+
+ if ((f = fopen("/proc/sys/net/netfilter/nf_conntrack_count", "r")) != NULL)
+ {
+ if (fgets(line, sizeof(line) - 1, f))
+ blobmsg_add_u32(&buf, "count", atoi(line));
+
+ fclose(f);
+ }
+
+ if ((f = fopen("/proc/sys/net/netfilter/nf_conntrack_max", "r")) != NULL)
+ {
+ if (fgets(line, sizeof(line) - 1, f))
+ blobmsg_add_u32(&buf, "limit", atoi(line));
+
+ fclose(f);
+ }
+
+ ubus_send_reply(ctx, req, buf.head);
+
+ return 0;
+}
+
+static int
+rpc_luci2_network_ct_table(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ int i;
+ void *c, *d;
+ char *p, line[512];
+ bool seen[6];
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "entries");
+
+ if ((f = fopen("/proc/net/nf_conntrack", "r")) != NULL)
+ {
+ while (fgets(line, sizeof(line) - 1, f))
+ {
+ d = blobmsg_open_table(&buf, NULL);
+ memset(seen, 0, sizeof(seen));
+
+ for (i = 0, p = strtok(line, " "); p; i++, p = strtok(NULL, " "))
+ {
+ if (i == 0)
+ blobmsg_add_u8(&buf, "ipv6", !strcmp(p, "ipv6"));
+ else if (i == 3)
+ blobmsg_add_u32(&buf, "protocol", atoi(p));
+ else if (i == 4)
+ blobmsg_add_u32(&buf, "expires", atoi(p));
+ else if (i >= 5)
+ {
+ if (*p == '[')
+ continue;
+
+ if (!seen[0] && !strncmp(p, "src=", 4))
+ {
+ blobmsg_add_string(&buf, "src", p + 4);
+ seen[0] = true;
+ }
+ else if (!seen[1] && !strncmp(p, "dst=", 4))
+ {
+ blobmsg_add_string(&buf, "dest", p + 4);
+ seen[1] = true;
+ }
+ else if (!seen[2] && !strncmp(p, "sport=", 6))
+ {
+ blobmsg_add_u32(&buf, "sport", atoi(p + 6));
+ seen[2] = true;
+ }
+ else if (!seen[3] && !strncmp(p, "dport=", 6))
+ {
+ blobmsg_add_u32(&buf, "dport", atoi(p + 6));
+ seen[3] = true;
+ }
+ else if (!strncmp(p, "packets=", 8))
+ {
+ blobmsg_add_u32(&buf,
+ seen[4] ? "tx_packets" : "rx_packets",
+ atoi(p + 8));
+ seen[4] = true;
+ }
+ else if (!strncmp(p, "bytes=", 6))
+ {
+ blobmsg_add_u32(&buf,
+ seen[5] ? "tx_bytes" : "rx_bytes",
+ atoi(p + 6));
+ seen[5] = true;
+ }
+ }
+ }
+
+ blobmsg_close_table(&buf, d);
+ }
+
+ fclose(f);
+ }
+
+ blobmsg_close_array(&buf, c);
+ ubus_send_reply(ctx, req, buf.head);
+
+ return 0;
+}
+
+static int
+rpc_luci2_network_arp_table(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ void *c, *d;
+ char *addr, *mac, *dev, line[128];
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "entries");
+
+ if ((f = fopen("/proc/net/arp", "r")) != NULL)
+ {
+ /* skip header line */
+ fgets(line, sizeof(line) - 1, f);
+
+ while (fgets(line, sizeof(line) - 1, f))
+ {
+ addr = strtok(line, " \t");
+
+ strtok(NULL, " \t"); /* HW type */
+ strtok(NULL, " \t"); /* Flags */
+
+ mac = strtok(NULL, " \t");
+
+ strtok(NULL, " \t"); /* Mask */
+
+ dev = strtok(NULL, " \t\n");
+
+ if (!dev)
+ continue;
+
+ d = blobmsg_open_table(&buf, NULL);
+ blobmsg_add_string(&buf, "ipaddr", addr);
+ blobmsg_add_string(&buf, "macaddr", mac);
+ blobmsg_add_string(&buf, "device", dev);
+ blobmsg_close_table(&buf, d);
+ }
+
+ fclose(f);
+ }
+
+ blobmsg_close_array(&buf, c);
+ ubus_send_reply(ctx, req, buf.head);
+
+ return 0;
+}
+
+static void
+put_hexaddr(const char *name, const char *s, const char *m)
+{
+ int bits;
+ struct in_addr a;
+ char as[sizeof("255.255.255.255/32\0")];
+
+ a.s_addr = strtoul(s, NULL, 16);
+ inet_ntop(AF_INET, &a, as, sizeof(as));
+
+ if (m)
+ {
+ for (a.s_addr = ntohl(strtoul(m, NULL, 16)), bits = 0;
+ a.s_addr & 0x80000000;
+ a.s_addr <<= 1)
+ bits++;
+
+ sprintf(as + strlen(as), "/%u", bits);
+ }
+
+ blobmsg_add_string(&buf, name, as);
+}
+
+static int
+rpc_luci2_network_routes(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *routes;
+ void *c, *d;
+ char *dst, *dmask, *next, *metric, *device;
+ char line[256];
+ unsigned int n;
+
+ if (!(routes = fopen("/proc/net/route", "r")))
+ return rpc_errno_status();
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "routes");
+
+ /* skip header line */
+ fgets(line, sizeof(line) - 1, routes);
+
+ while (fgets(line, sizeof(line) - 1, routes))
+ {
+ device = strtok(line, "\t ");
+ dst = strtok(NULL, "\t ");
+ next = strtok(NULL, "\t ");
+
+ strtok(NULL, "\t "); /* flags */
+ strtok(NULL, "\t "); /* refcount */
+ strtok(NULL, "\t "); /* usecount */
+
+ metric = strtok(NULL, "\t ");
+ dmask = strtok(NULL, "\t ");
+
+ if (!dmask)
+ continue;
+
+ d = blobmsg_open_table(&buf, NULL);
+
+ put_hexaddr("target", dst, dmask);
+ put_hexaddr("nexthop", next, NULL);
+
+ n = strtoul(metric, NULL, 10);
+ blobmsg_add_u32(&buf, "metric", n);
+
+ blobmsg_add_string(&buf, "device", device);
+
+ blobmsg_close_table(&buf, d);
+ }
+
+ blobmsg_close_array(&buf, c);
+ fclose(routes);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static void
+put_hex6addr(const char *name, const char *s, const char *m)
+{
+ int i;
+ struct in6_addr a;
+ char as[INET6_ADDRSTRLEN + sizeof("/128")];
+
+#define hex(x) \
+ (((x) <= '9') ? ((x) - '0') : \
+ (((x) <= 'F') ? ((x) - 'A' + 10) : \
+ ((x) - 'a' + 10)))
+
+ for (i = 0; i < 16; i++, s += 2)
+ a.s6_addr[i] = (16 * hex(*s)) + hex(*(s+1));
+
+ inet_ntop(AF_INET6, &a, as, sizeof(as));
+
+ if (m)
+ sprintf(as + strlen(as), "/%lu", strtoul(m, NULL, 16));
+
+ blobmsg_add_string(&buf, name, as);
+}
+
+static int
+rpc_luci2_network_routes6(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *routes;
+ void *c, *d;
+ char *src, *smask, *dst, *dmask, *next, *metric, *flags, *device;
+ char line[256];
+ unsigned int n;
+
+ if (!(routes = fopen("/proc/net/ipv6_route", "r")))
+ return rpc_errno_status();
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "routes");
+
+ while (fgets(line, sizeof(line) - 1, routes))
+ {
+ dst = strtok(line, " ");
+ dmask = strtok(NULL, " ");
+ src = strtok(NULL, " ");
+ smask = strtok(NULL, " ");
+ next = strtok(NULL, " ");
+ metric = strtok(NULL, " ");
+
+ strtok(NULL, " "); /* refcount */
+ strtok(NULL, " "); /* usecount */
+
+ flags = strtok(NULL, " ");
+ device = strtok(NULL, " \n");
+
+ if (!device)
+ continue;
+
+ n = strtoul(flags, NULL, 16);
+
+ if (!(n & 1))
+ continue;
+
+ d = blobmsg_open_table(&buf, NULL);
+
+ put_hex6addr("target", dst, dmask);
+ put_hex6addr("source", src, smask);
+ put_hex6addr("nexthop", next, NULL);
+
+ n = strtoul(metric, NULL, 16);
+ blobmsg_add_u32(&buf, "metric", n);
+
+ blobmsg_add_string(&buf, "device", device);
+
+ blobmsg_close_table(&buf, d);
+ }
+
+ blobmsg_close_array(&buf, c);
+ fclose(routes);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+
+struct swconfig_state {
+ bool open;
+ void *array;
+ bool open2;
+ void *array2;
+ int port;
+};
+
+static int
+swconfig_parse_list(struct blob_buf *blob, char *buf, int len, void *priv)
+{
+ char *p;
+ char *nl = strchr(buf, '\n');
+ struct swconfig_state *s = priv;
+
+ if (!nl)
+ return 0;
+
+ if (!s->open)
+ {
+ s->open = true;
+ s->array = blobmsg_open_array(blob, "switches");
+ }
+
+ strtok(buf, "-");
+ p = strtok(NULL, " \n");
+
+ if (p)
+ blobmsg_add_string(blob, NULL, p);
+
+ return (nl - buf + 1);
+}
+
+static int
+swconfig_finish_list(struct blob_buf *blob, int status, void *priv)
+{
+ struct swconfig_state *s = priv;
+
+ if (!s->open)
+ return UBUS_STATUS_NO_DATA;
+
+ blobmsg_close_array(blob, s->array);
+
+ return UBUS_STATUS_OK;
+}
+
+static int
+rpc_luci2_network_sw_list(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct swconfig_state *state = NULL;
+ const char *cmd[3] = { "swconfig", "list", NULL };
+
+ state = malloc(sizeof(*state));
+
+ if (!state)
+ return UBUS_STATUS_UNKNOWN_ERROR;
+
+ memset(state, 0, sizeof(*state));
+
+ return ops->exec(cmd, NULL, swconfig_parse_list, NULL, swconfig_finish_list,
+ state, ctx, req);
+}
+
+
+static int
+swconfig_parse_help(struct blob_buf *blob, char *buf, int len, void *priv)
+{
+ void *c;
+ char *p;
+ char *nl = strchr(buf, '\n');
+ struct swconfig_state *s = priv;
+
+ if (!nl)
+ return 0;
+
+ if (!s->open)
+ {
+ s->open = true;
+ s->array = blobmsg_open_table(blob, "info");
+ }
+
+ switch (*buf)
+ {
+ case ' ':
+ strtok(buf, "-");
+ p = strtok(NULL, "-\n");
+
+ if (p)
+ {
+ if (s->open2)
+ blobmsg_close_array(blob, s->array2);
+
+ s->array2 = blobmsg_open_array(blob, p);
+ s->open2 = true;
+ }
+
+ break;
+
+ case '\t':
+ c = blobmsg_open_table(blob, NULL);
+
+ strtok(buf, "(");
+ p = strtok(NULL, ")");
+
+ if (p)
+ blobmsg_add_string(blob, "type", p);
+
+ p = strtok(NULL, ":( ");
+
+ if (p)
+ blobmsg_add_string(blob, "name", p);
+
+ p = strtok(NULL, "\n");
+ *(nl - 1) = 0;
+
+ if (p)
+ blobmsg_add_string(blob, "description", p + 1);
+
+ blobmsg_close_table(blob, c);
+ break;
+
+ default:
+ strtok(buf, "(");
+ p = strtok(NULL, ")");
+
+ if (p)
+ blobmsg_add_string(blob, "model", p);
+
+ strtok(NULL, ":");
+ p = strtok(NULL, "(");
+
+ if (p)
+ blobmsg_add_u32(blob, "num_ports", atoi(p));
+
+ strtok(NULL, "@");
+ p = strtok(NULL, ")");
+
+ if (p)
+ blobmsg_add_u32(blob, "cpu_port", atoi(p));
+
+ strtok(NULL, ":");
+ p = strtok(NULL, "\n");
+
+ if (p)
+ blobmsg_add_u32(blob, "num_vlans", atoi(p));
+
+ break;
+ }
+
+ return (nl - buf + 1);
+}
+
+static int
+swconfig_finish_help(struct blob_buf *blob, int status, void *priv)
+{
+ struct swconfig_state *s = priv;
+
+ if (!s->open)
+ return UBUS_STATUS_NO_DATA;
+
+ if (s->open2)
+ blobmsg_close_array(blob, s->array2);
+
+ blobmsg_close_table(blob, s->array);
+
+ return UBUS_STATUS_OK;
+}
+
+static int
+rpc_luci2_network_sw_info(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct swconfig_state *state = NULL;
+ struct blob_attr *tb[__RPC_SWITCH_MAX];
+ const char *cmd[5] = { "swconfig", "dev", NULL, "help", NULL };
+
+ blobmsg_parse(rpc_switch_policy, __RPC_SWITCH_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_SWITCH_NAME])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ state = malloc(sizeof(*state));
+
+ if (!state)
+ return UBUS_STATUS_UNKNOWN_ERROR;
+
+ memset(state, 0, sizeof(*state));
+
+ cmd[2] = blobmsg_get_string(tb[RPC_SWITCH_NAME]);
+
+ return ops->exec(cmd, NULL, swconfig_parse_help, NULL, swconfig_finish_help,
+ state, ctx, req);
+}
+
+
+static void
+swconfig_parse_link(struct blob_buf *blob, char *val)
+{
+ char *p;
+
+ int speed = 0;
+
+ bool rxflow = false;
+ bool txflow = false;
+ bool duplex = false;
+ bool aneg = false;
+ bool up = false;
+
+ for (p = strtok(val, " "); p; p = strtok(NULL, " "))
+ {
+ if (!strncmp(p, "speed:", 6))
+ speed = atoi(p + 6);
+ else if (!strcmp(p, "link:up"))
+ up = true;
+ else if (!strcmp(p, "txflow"))
+ txflow = true;
+ else if (!strcmp(p, "rxflow"))
+ rxflow = true;
+ else if (!strcmp(p, "full-duplex"))
+ duplex = true;
+ else if (!strcmp(p, "auto"))
+ aneg = true;
+ }
+
+ blobmsg_add_u8(blob, "link", up);
+ blobmsg_add_u8(blob, "rx_flow_control", rxflow);
+ blobmsg_add_u8(blob, "tx_flow_control", txflow);
+ blobmsg_add_u8(blob, "full_duplex", duplex);
+ blobmsg_add_u8(blob, "auto_negotiation", aneg);
+ blobmsg_add_u32(blob, "speed", speed);
+}
+
+static int
+swconfig_parse_stat(struct blob_buf *blob, char *buf, int len, void *priv)
+{
+ char *p, *v;
+ char *nl = strchr(buf, '\n');
+ struct swconfig_state *s = priv;
+
+ if (!nl)
+ return 0;
+
+ if (nl == buf)
+ return 1;
+
+ if (!s->open)
+ {
+ s->open = true;
+ s->array = blobmsg_open_array(blob, "ports");
+ }
+
+ p = strtok(buf, " :\t");
+
+ if (p)
+ {
+ if (!strcmp(p, "Port"))
+ {
+ if (s->open2)
+ blobmsg_close_table(blob, s->array2);
+
+ s->array2 = blobmsg_open_table(blob, NULL);
+ s->open2 = true;
+ }
+ else if (s->open2)
+ {
+ v = strtok(NULL, "\n");
+
+ if (v)
+ {
+ if (!strcmp(p, "link"))
+ swconfig_parse_link(blob, v);
+ }
+ }
+ }
+
+ return (nl - buf + 1);
+}
+
+static int
+swconfig_finish_stat(struct blob_buf *blob, int status, void *priv)
+{
+ struct swconfig_state *s = priv;
+
+ if (!s->open)
+ return UBUS_STATUS_NO_DATA;
+
+ if (s->open2)
+ blobmsg_close_table(blob, s->array2);
+
+ blobmsg_close_array(blob, s->array);
+
+ return UBUS_STATUS_OK;
+}
+
+static int
+rpc_luci2_network_sw_status(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct swconfig_state *state = NULL;
+ struct blob_attr *tb[__RPC_SWITCH_MAX];
+ const char *cmd[5] = { "swconfig", "dev", NULL, "show", NULL };
+
+ blobmsg_parse(rpc_switch_policy, __RPC_SWITCH_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_SWITCH_NAME])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ state = malloc(sizeof(*state));
+
+ if (!state)
+ return UBUS_STATUS_UNKNOWN_ERROR;
+
+ memset(state, 0, sizeof(*state));
+
+ cmd[2] = blobmsg_get_string(tb[RPC_SWITCH_NAME]);
+
+ return ops->exec(cmd, NULL, swconfig_parse_stat, NULL, swconfig_finish_stat,
+ state, ctx, req);
+}
+
+enum {
+ NETWORK_CMD_PING,
+ NETWORK_CMD_PING6,
+ NETWORK_CMD_TRACEROUTE,
+ NETWORK_CMD_TRACEROUTE6,
+ NETWORK_CMD_NSLOOKUP
+};
+
+static int
+network_cmd(struct ubus_context *ctx, struct ubus_request_data *req,
+ struct blob_attr *msg, int which)
+{
+ char *arg;
+ struct blob_attr *tb[__RPC_D_MAX];
+
+ blobmsg_parse(rpc_data_policy, __RPC_D_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_D_DATA])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ arg = blobmsg_get_string(tb[RPC_D_DATA]);
+
+ const char *cmds[][8] = {
+ [NETWORK_CMD_PING] = {
+ "ping", "-c", "5", "-W", "1", arg
+ },
+ [NETWORK_CMD_PING6] = {
+ "ping6", "-c", "5", "-W", "1", arg
+ },
+ [NETWORK_CMD_TRACEROUTE] = {
+ "traceroute", "-q", "1", "-w", "1", "-n", arg
+ },
+ [NETWORK_CMD_TRACEROUTE6] = {
+ "traceroute6", "-q", "1", "-w", "2", "-n", arg
+ },
+ [NETWORK_CMD_NSLOOKUP] = {
+ "nslookup", arg
+ }
+ };
+
+ return ops->exec(cmds[which], NULL, NULL, NULL, NULL, NULL, ctx, req);
+}
+
+static int
+rpc_luci2_network_ping(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return network_cmd(ctx, req, msg, NETWORK_CMD_PING);
+}
+
+static int
+rpc_luci2_network_ping6(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return network_cmd(ctx, req, msg, NETWORK_CMD_PING6);
+}
+
+static int
+rpc_luci2_network_traceroute(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return network_cmd(ctx, req, msg, NETWORK_CMD_TRACEROUTE);
+}
+
+static int
+rpc_luci2_network_traceroute6(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return network_cmd(ctx, req, msg, NETWORK_CMD_TRACEROUTE6);
+}
+
+static int
+rpc_luci2_network_nslookup(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return network_cmd(ctx, req, msg, NETWORK_CMD_NSLOOKUP);
+}
+
+
+static int
+network_ifupdown(struct ubus_context *ctx, struct ubus_request_data *req,
+ struct blob_attr *msg, bool up)
+{
+ const char *cmd[3] = { NULL };
+ struct blob_attr *tb[__RPC_D_MAX];
+
+ blobmsg_parse(rpc_data_policy, __RPC_D_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_D_DATA])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ cmd[0] = up ? "/sbin/ifup" : "/sbin/ifdown";
+ cmd[1] = blobmsg_get_string(tb[RPC_D_DATA]);
+
+ return ops->exec(cmd, NULL, NULL, NULL, NULL, NULL, ctx, req);
+}
+
+static int
+rpc_luci2_network_ifup(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return network_ifupdown(ctx, req, msg, true);
+}
+
+static int
+rpc_luci2_network_ifdown(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return network_ifupdown(ctx, req, msg, false);
+}
+
+static int
+rpc_luci2_network_dev_list(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ char path[PATH_MAX];
+ struct dirent *e;
+ struct stat s;
+ void *c, *t;
+ bool wireless, bridge, tuntap;
+ int type, flags;
+ DIR *d;
+ FILE *f;
+
+ if (!(d = opendir("/sys/class/net")))
+ return rpc_errno_status();
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "devices");
+
+ while ((e = readdir(d)) != NULL)
+ {
+ snprintf(path, sizeof(path) - 1, "/sys/class/net/%s/type", e->d_name);
+
+ if (stat(path, &s) || !S_ISREG(s.st_mode) || !(f = fopen(path, "r")))
+ continue;
+
+ type = 1;
+ memset(path, 0, sizeof(path));
+
+ if (fread(path, 1, sizeof(path) - 1, f) > 0)
+ type = atoi(path);
+
+ fclose(f);
+
+ snprintf(path, sizeof(path) - 1, "/sys/class/net/%s/flags", e->d_name);
+
+ if (stat(path, &s) || !S_ISREG(s.st_mode) || !(f = fopen(path, "r")))
+ continue;
+
+ flags = 0;
+ memset(path, 0, sizeof(path));
+
+ if (fread(path, 1, sizeof(path) - 1, f) > 0)
+ flags = strtoul(path, NULL, 16);
+
+ fclose(f);
+
+ snprintf(path, sizeof(path) - 1,
+ "/sys/class/net/%s/wireless", e->d_name);
+
+ wireless = (!stat(path, &s) && S_ISDIR(s.st_mode));
+
+ snprintf(path, sizeof(path) - 1,
+ "/sys/class/net/%s/phy80211", e->d_name);
+
+ wireless = (wireless || (!stat(path, &s) && S_ISLNK(s.st_mode)));
+
+ snprintf(path, sizeof(path) - 1,
+ "/sys/class/net/%s/bridge", e->d_name);
+
+ bridge = (!stat(path, &s) && S_ISDIR(s.st_mode));
+
+ snprintf(path, sizeof(path) - 1,
+ "/sys/class/net/%s/tun_flags", e->d_name);
+
+ tuntap = (!stat(path, &s) && S_ISREG(s.st_mode));
+
+ t = blobmsg_open_table(&buf, NULL);
+
+ blobmsg_add_string(&buf, "device", e->d_name);
+ blobmsg_add_u32(&buf, "type", type);
+ blobmsg_add_u8(&buf, "is_up", flags & 1);
+ blobmsg_add_u8(&buf, "is_bridge", bridge);
+ blobmsg_add_u8(&buf, "is_tuntap", tuntap);
+ blobmsg_add_u8(&buf, "is_wireless", wireless);
+
+ blobmsg_close_table(&buf, t);
+ }
+
+ blobmsg_close_array(&buf, c);
+
+ closedir(d);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_network_eap_support(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ blob_buf_init(&buf, 0);
+ blobmsg_add_u8(&buf, "master", !system("/usr/sbin/hostapd -veap"));
+ blobmsg_add_u8(&buf, "client", !system("/usr/sbin/wpa_supplicant -veap"));
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+
+struct opkg_state {
+ int cur_offset;
+ int cur_count;
+ int req_offset;
+ int req_count;
+ int total;
+ bool open;
+ void *array;
+};
+
+static int
+opkg_parse_list(struct blob_buf *blob, char *buf, int len, void *priv)
+{
+ struct opkg_state *s = priv;
+
+ char *ptr, *last;
+ char *nl = strchr(buf, '\n');
+ char *name = NULL, *vers = NULL, *desc = NULL;
+ void *c;
+
+ if (!nl)
+ return 0;
+
+ s->total++;
+
+ if (s->cur_offset++ < s->req_offset)
+ goto skip;
+
+ if (s->cur_count++ >= s->req_count)
+ goto skip;
+
+ if (!s->open)
+ {
+ s->open = true;
+ s->array = blobmsg_open_array(blob, "packages");
+ }
+
+ for (ptr = buf, last = buf, *nl = 0; ptr <= nl; ptr++)
+ {
+ if (!*ptr || (*ptr == ' ' && *(ptr+1) == '-' && *(ptr+2) == ' '))
+ {
+ if (!name)
+ {
+ name = last;
+ last = ptr + 3;
+ *ptr = 0;
+ ptr += 2;
+ }
+ else if (!vers)
+ {
+ vers = last;
+ desc = *ptr ? (ptr + 3) : NULL;
+ *ptr = 0;
+ break;
+ }
+ }
+ }
+
+ if (name && vers)
+ {
+ c = blobmsg_open_array(blob, NULL);
+
+ blobmsg_add_string(blob, NULL, name);
+ blobmsg_add_string(blob, NULL, vers);
+
+ if (desc && *desc)
+ blobmsg_add_string(blob, NULL, desc);
+
+ blobmsg_close_array(blob, c);
+ }
+
+skip:
+ return (nl - buf + 1);
+}
+
+static int
+opkg_finish_list(struct blob_buf *blob, int status, void *priv)
+{
+ struct opkg_state *s = priv;
+
+ if (!s->open)
+ return UBUS_STATUS_NO_DATA;
+
+ blobmsg_close_array(blob, s->array);
+ blobmsg_add_u32(blob, "total", s->total);
+
+ return UBUS_STATUS_OK;
+}
+
+static int
+opkg_exec_list(const char *action, struct blob_attr *msg,
+ struct ubus_context *ctx, struct ubus_request_data *req)
+{
+ struct opkg_state *state = NULL;
+ struct blob_attr *tb[__RPC_OM_MAX];
+ const char *cmd[5] = { "opkg", action, "-nocase", NULL, NULL };
+
+ blobmsg_parse(rpc_opkg_match_policy, __RPC_OM_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ state = malloc(sizeof(*state));
+
+ if (!state)
+ return UBUS_STATUS_UNKNOWN_ERROR;
+
+ memset(state, 0, sizeof(*state));
+
+ if (tb[RPC_OM_PATTERN])
+ cmd[3] = blobmsg_data(tb[RPC_OM_PATTERN]);
+
+ if (tb[RPC_OM_LIMIT])
+ state->req_count = blobmsg_get_u32(tb[RPC_OM_LIMIT]);
+
+ if (tb[RPC_OM_OFFSET])
+ state->req_offset = blobmsg_get_u32(tb[RPC_OM_OFFSET]);
+
+ if (state->req_offset < 0)
+ state->req_offset = 0;
+
+ if (state->req_count <= 0 || state->req_count > 100)
+ state->req_count = 100;
+
+ return ops->exec(cmd, NULL, opkg_parse_list, NULL, opkg_finish_list,
+ state, ctx, req);
+}
+
+
+static int
+rpc_luci2_opkg_list(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return opkg_exec_list("list", msg, ctx, req);
+}
+
+static int
+rpc_luci2_opkg_list_installed(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return opkg_exec_list("list-installed", msg, ctx, req);
+}
+
+static int
+rpc_luci2_opkg_find(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ return opkg_exec_list("find", msg, ctx, req);
+}
+
+static int
+rpc_luci2_opkg_update(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ const char *cmd[3] = { "opkg", "update", NULL };
+ return ops->exec(cmd, NULL, NULL, NULL, NULL, NULL, ctx, req);
+}
+
+static int
+rpc_luci2_opkg_install(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct blob_attr *tb[__RPC_OP_MAX];
+ const char *cmd[5] = { "opkg", "--force-overwrite",
+ "install", NULL, NULL };
+
+ blobmsg_parse(rpc_opkg_package_policy, __RPC_OP_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_OP_PACKAGE])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ cmd[3] = blobmsg_data(tb[RPC_OP_PACKAGE]);
+
+ return ops->exec(cmd, NULL, NULL, NULL, NULL, NULL, ctx, req);
+}
+
+static int
+rpc_luci2_opkg_remove(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ struct blob_attr *tb[__RPC_OP_MAX];
+ const char *cmd[5] = { "opkg", "--force-removal-of-dependent-packages",
+ "remove", NULL, NULL };
+
+ blobmsg_parse(rpc_opkg_package_policy, __RPC_OP_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_OP_PACKAGE])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ cmd[3] = blobmsg_data(tb[RPC_OP_PACKAGE]);
+
+ return ops->exec(cmd, NULL, NULL, NULL, NULL, NULL, ctx, req);
+}
+
+static int
+rpc_luci2_opkg_config_get(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ char conf[2048] = { 0 };
+
+ if (!(f = fopen("/etc/opkg.conf", "r")))
+ return rpc_errno_status();
+
+ fread(conf, sizeof(conf) - 1, 1, f);
+ fclose(f);
+
+ blob_buf_init(&buf, 0);
+ blobmsg_add_string(&buf, "config", conf);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_opkg_config_set(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ FILE *f;
+ struct blob_attr *tb[__RPC_D_MAX];
+
+ blobmsg_parse(rpc_data_policy, __RPC_D_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_D_DATA])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ if (blobmsg_data_len(tb[RPC_D_DATA]) >= 2048)
+ return UBUS_STATUS_NOT_SUPPORTED;
+
+ if (!(f = fopen("/etc/opkg.conf", "w")))
+ return rpc_errno_status();
+
+ fwrite(blobmsg_data(tb[RPC_D_DATA]),
+ blobmsg_data_len(tb[RPC_D_DATA]) - 1, 1, f);
+
+ fclose(f);
+ return 0;
+}
+
+
+static bool
+menu_access(struct blob_attr *sid, struct blob_attr *acls, struct blob_buf *e)
+{
+ int rem;
+ struct blob_attr *acl;
+ bool rv = true;
+ void *c;
+
+ c = blobmsg_open_table(e, "write");
+
+ blobmsg_for_each_attr(acl, acls, rem)
+ {
+ if (!ops->session_access(blobmsg_data(sid), "access-group",
+ blobmsg_data(acl), "read"))
+ {
+ rv = false;
+ break;
+ }
+
+ blobmsg_add_u8(e, blobmsg_data(acl),
+ ops->session_access(blobmsg_data(sid), "access-group",
+ blobmsg_data(acl), "write"));
+ }
+
+ blobmsg_close_table(e, c);
+
+ return rv;
+}
+
+static bool
+menu_files(struct blob_attr *files)
+{
+ int rem;
+ bool empty = true;
+ struct stat s;
+ struct blob_attr *file;
+
+ blobmsg_for_each_attr(file, files, rem)
+ {
+ empty = false;
+
+ if (blobmsg_type(file) != BLOBMSG_TYPE_STRING)
+ continue;
+
+ if (stat(blobmsg_get_string(file), &s) || !S_ISREG(s.st_mode))
+ continue;
+
+ return true;
+ }
+
+ return empty;
+}
+
+static int
+rpc_luci2_ui_menu(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ int i, rem, rem2;
+ glob_t gl;
+ struct blob_buf menu = { 0 };
+ struct blob_buf item = { 0 };
+ struct blob_attr *entry, *attr;
+ struct blob_attr *tb[__RPC_MENU_MAX];
+ bool access, files;
+ void *c, *d;
+
+ blobmsg_parse(rpc_menu_policy, __RPC_MENU_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_MENU_SESSION])
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_table(&buf, "menu");
+
+ if (!glob(RPC_LUCI2_MENU_FILES, 0, NULL, &gl))
+ {
+ for (i = 0; i < gl.gl_pathc; i++)
+ {
+ blob_buf_init(&menu, 0);
+
+ if (!blobmsg_add_json_from_file(&menu, gl.gl_pathv[i]))
+ goto skip;
+
+ blob_for_each_attr(entry, menu.head, rem)
+ {
+ access = files = true;
+
+ blob_buf_init(&item, 0);
+ d = blobmsg_open_table(&item, blobmsg_name(entry));
+
+ blobmsg_for_each_attr(attr, entry, rem2)
+ {
+ if (blob_id(attr) == BLOBMSG_TYPE_ARRAY &&
+ !strcmp(blobmsg_name(attr), "acls"))
+ access = menu_access(tb[RPC_MENU_SESSION], attr, &item);
+ else if (blob_id(attr) == BLOBMSG_TYPE_ARRAY &&
+ !strcmp(blobmsg_name(attr), "files"))
+ files = menu_files(attr);
+ else
+ blobmsg_add_blob(&item, attr);
+ }
+
+ blobmsg_close_table(&item, d);
+
+ if (access && files)
+ blob_for_each_attr(attr, item.head, rem2)
+ blobmsg_add_blob(&buf, attr);
+
+ blob_buf_free(&item);
+ }
+
+skip:
+ blob_buf_free(&menu);
+ }
+
+ globfree(&gl);
+ }
+
+ blobmsg_close_table(&buf, c);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+
+static void
+parse_acl_file(struct blob_buf *acls, const char *path)
+{
+ struct blob_buf acl = { 0 };
+ struct blob_attr *cur;
+ void *c;
+ int rem;
+
+ blob_buf_init(&acl, 0);
+
+ if (blobmsg_add_json_from_file(&acl, path))
+ {
+ c = blobmsg_open_table(acls, NULL);
+
+ blob_for_each_attr(cur, acl.head, rem)
+ blobmsg_add_blob(acls, cur);
+
+ blobmsg_close_table(acls, c);
+ }
+
+ blob_buf_free(&acl);
+}
+
+static int
+rpc_luci2_ui_acls(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ int i;
+ void *c;
+ glob_t gl;
+
+ if (glob(RPC_SESSION_ACL_DIR "/*.json", 0, NULL, &gl))
+ return rpc_errno_status();
+
+ blob_buf_init(&buf, 0);
+ c = blobmsg_open_array(&buf, "acls");
+
+ for (i = 0; i < gl.gl_pathc; i++)
+ parse_acl_file(&buf, gl.gl_pathv[i]);
+
+ globfree(&gl);
+ blobmsg_close_array(&buf, c);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+static int
+rpc_luci2_ui_crypt(struct ubus_context *ctx, struct ubus_object *obj,
+ struct ubus_request_data *req, const char *method,
+ struct blob_attr *msg)
+{
+ char *hash;
+ struct blob_attr *tb[__RPC_D_MAX];
+
+ blobmsg_parse(rpc_data_policy, __RPC_D_MAX, tb,
+ blob_data(msg), blob_len(msg));
+
+ if (!tb[RPC_D_DATA] || blobmsg_data_len(tb[RPC_D_DATA]) >= 128)
+ return UBUS_STATUS_INVALID_ARGUMENT;
+
+ hash = crypt(blobmsg_get_string(tb[RPC_D_DATA]), "$1$");
+
+ blob_buf_init(&buf, 0);
+ blobmsg_add_string(&buf, "crypt", hash);
+
+ ubus_send_reply(ctx, req, buf.head);
+ return 0;
+}
+
+
+static int
+rpc_luci2_api_init(const struct rpc_daemon_ops *o, struct ubus_context *ctx)
+{
+ int rv = 0;
+
+ static const struct ubus_method luci2_system_methods[] = {
+ UBUS_METHOD_NOARG("syslog", rpc_luci2_system_log),
+ UBUS_METHOD_NOARG("dmesg", rpc_luci2_system_dmesg),
+ UBUS_METHOD_NOARG("diskfree", rpc_luci2_system_diskfree),
+ UBUS_METHOD_NOARG("process_list", rpc_luci2_process_list),
+ UBUS_METHOD("process_signal", rpc_luci2_process_signal,
+ rpc_signal_policy),
+ UBUS_METHOD_NOARG("init_list", rpc_luci2_init_list),
+ UBUS_METHOD("init_action", rpc_luci2_init_action,
+ rpc_init_policy),
+ UBUS_METHOD_NOARG("rclocal_get", rpc_luci2_rclocal_get),
+ UBUS_METHOD("rclocal_set", rpc_luci2_rclocal_set,
+ rpc_data_policy),
+ UBUS_METHOD_NOARG("crontab_get", rpc_luci2_crontab_get),
+ UBUS_METHOD("crontab_set", rpc_luci2_crontab_set,
+ rpc_data_policy),
+ UBUS_METHOD_NOARG("sshkeys_get", rpc_luci2_sshkeys_get),
+ UBUS_METHOD("sshkeys_set", rpc_luci2_sshkeys_set,
+ rpc_sshkey_policy),
+ UBUS_METHOD("password_set", rpc_luci2_password_set,
+ rpc_password_policy),
+ UBUS_METHOD_NOARG("led_list", rpc_luci2_led_list),
+ UBUS_METHOD_NOARG("usb_list", rpc_luci2_usb_list),
+ UBUS_METHOD_NOARG("upgrade_test", rpc_luci2_upgrade_test),
+ UBUS_METHOD("upgrade_start", rpc_luci2_upgrade_start,
+ rpc_upgrade_policy),
+ UBUS_METHOD_NOARG("upgrade_clean", rpc_luci2_upgrade_clean),
+ UBUS_METHOD_NOARG("backup_restore", rpc_luci2_backup_restore),
+ UBUS_METHOD_NOARG("backup_clean", rpc_luci2_backup_clean),
+ UBUS_METHOD_NOARG("backup_config_get", rpc_luci2_backup_config_get),
+ UBUS_METHOD("backup_config_set", rpc_luci2_backup_config_set,
+ rpc_data_policy),
+ UBUS_METHOD_NOARG("backup_list", rpc_luci2_backup_list),
+ UBUS_METHOD_NOARG("reset_test", rpc_luci2_reset_test),
+ UBUS_METHOD_NOARG("reset_start", rpc_luci2_reset_start),
+ UBUS_METHOD_NOARG("reboot", rpc_luci2_reboot)
+ };
+
+ static struct ubus_object_type luci2_system_type =
+ UBUS_OBJECT_TYPE("luci-rpc-luci2-system", luci2_system_methods);
+
+ static struct ubus_object system_obj = {
+ .name = "luci2.system",
+ .type = &luci2_system_type,
+ .methods = luci2_system_methods,
+ .n_methods = ARRAY_SIZE(luci2_system_methods),
+ };
+
+
+ static const struct ubus_method luci2_network_methods[] = {
+ UBUS_METHOD_NOARG("conntrack_count", rpc_luci2_network_ct_count),
+ UBUS_METHOD_NOARG("conntrack_table", rpc_luci2_network_ct_table),
+ UBUS_METHOD_NOARG("arp_table", rpc_luci2_network_arp_table),
+ UBUS_METHOD_NOARG("dhcp_leases", rpc_luci2_network_leases),
+ UBUS_METHOD_NOARG("dhcp6_leases", rpc_luci2_network_leases6),
+ UBUS_METHOD_NOARG("routes", rpc_luci2_network_routes),
+ UBUS_METHOD_NOARG("routes6", rpc_luci2_network_routes6),
+ UBUS_METHOD_NOARG("switch_list", rpc_luci2_network_sw_list),
+ UBUS_METHOD("switch_info", rpc_luci2_network_sw_info,
+ rpc_switch_policy),
+ UBUS_METHOD("switch_status", rpc_luci2_network_sw_status,
+ rpc_switch_policy),
+ UBUS_METHOD("ping", rpc_luci2_network_ping,
+ rpc_data_policy),
+ UBUS_METHOD("ping6", rpc_luci2_network_ping6,
+ rpc_data_policy),
+ UBUS_METHOD("traceroute", rpc_luci2_network_traceroute,
+ rpc_data_policy),
+ UBUS_METHOD("traceroute6", rpc_luci2_network_traceroute6,
+ rpc_data_policy),
+ UBUS_METHOD("nslookup", rpc_luci2_network_nslookup,
+ rpc_data_policy),
+ UBUS_METHOD("ifup", rpc_luci2_network_ifup,
+ rpc_data_policy),
+ UBUS_METHOD("ifdown", rpc_luci2_network_ifdown,
+ rpc_data_policy),
+ UBUS_METHOD_NOARG("device_list", rpc_luci2_network_dev_list),
+ UBUS_METHOD_NOARG("eap_support", rpc_luci2_network_eap_support)
+ };
+
+ static struct ubus_object_type luci2_network_type =
+ UBUS_OBJECT_TYPE("luci-rpc-luci2-network", luci2_network_methods);
+
+ static struct ubus_object network_obj = {
+ .name = "luci2.network",
+ .type = &luci2_network_type,
+ .methods = luci2_network_methods,
+ .n_methods = ARRAY_SIZE(luci2_network_methods),
+ };
+
+
+ static const struct ubus_method luci2_opkg_methods[] = {
+ UBUS_METHOD("list", rpc_luci2_opkg_list,
+ rpc_opkg_match_policy),
+ UBUS_METHOD("list_installed", rpc_luci2_opkg_list_installed,
+ rpc_opkg_match_policy),
+ UBUS_METHOD("find", rpc_luci2_opkg_find,
+ rpc_opkg_match_policy),
+ UBUS_METHOD("install", rpc_luci2_opkg_install,
+ rpc_opkg_package_policy),
+ UBUS_METHOD("remove", rpc_luci2_opkg_remove,
+ rpc_opkg_package_policy),
+ UBUS_METHOD_NOARG("update", rpc_luci2_opkg_update),
+ UBUS_METHOD_NOARG("config_get", rpc_luci2_opkg_config_get),
+ UBUS_METHOD("config_set", rpc_luci2_opkg_config_set,
+ rpc_data_policy)
+ };
+
+ static struct ubus_object_type luci2_opkg_type =
+ UBUS_OBJECT_TYPE("luci-rpc-luci2-network", luci2_opkg_methods);
+
+ static struct ubus_object opkg_obj = {
+ .name = "luci2.opkg",
+ .type = &luci2_opkg_type,
+ .methods = luci2_opkg_methods,
+ .n_methods = ARRAY_SIZE(luci2_opkg_methods),
+ };
+
+
+ static const struct ubus_method luci2_ui_methods[] = {
+ UBUS_METHOD_NOARG("menu", rpc_luci2_ui_menu),
+ UBUS_METHOD_NOARG("acls", rpc_luci2_ui_acls),
+ UBUS_METHOD("crypt", rpc_luci2_ui_crypt,
+ rpc_data_policy)
+ };
+
+ static struct ubus_object_type luci2_ui_type =
+ UBUS_OBJECT_TYPE("luci-rpc-luci2-ui", luci2_ui_methods);
+
+ static struct ubus_object ui_obj = {
+ .name = "luci2.ui",
+ .type = &luci2_ui_type,
+ .methods = luci2_ui_methods,
+ .n_methods = ARRAY_SIZE(luci2_ui_methods),
+ };
+
+ cursor = uci_alloc_context();
+
+ if (!cursor)
+ return UBUS_STATUS_UNKNOWN_ERROR;
+
+ ops = o;
+
+ rv |= ubus_add_object(ctx, &system_obj);
+ rv |= ubus_add_object(ctx, &network_obj);
+ rv |= ubus_add_object(ctx, &opkg_obj);
+ rv |= ubus_add_object(ctx, &ui_obj);
+
+ return rv;
+}
+
+struct rpc_plugin rpc_plugin = {
+ .init = rpc_luci2_api_init
+};