From 4b11843e4ce3e7636d67cf3e987ae94ca8c8977d Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Tue, 16 Jun 2015 10:11:03 +0200 Subject: [PATCH] Add documentation converted from old Trac wiki pages Signed-off-by: Jo-Philipp Wich --- documentation/CBI.md | 248 +++++++++++++++++++++++++++ documentation/JsonRpcHowTo.md | 66 +++++++ documentation/LAR.md | 87 ++++++++++ documentation/LMO.md | 144 ++++++++++++++++ documentation/LuCI-0.10.md | 202 ++++++++++++++++++++++ documentation/Modules.md | 94 ++++++++++ documentation/ModulesHowTo.md | 153 +++++++++++++++++ documentation/SubmitPatchesHowTo:.md | 33 ++++ documentation/Templates.md | 66 +++++++ documentation/ThemesHowTo.md | 76 ++++++++ documentation/i18n.md | 17 ++ 11 files changed, 1186 insertions(+) create mode 100644 documentation/CBI.md create mode 100644 documentation/JsonRpcHowTo.md create mode 100644 documentation/LAR.md create mode 100644 documentation/LMO.md create mode 100644 documentation/LuCI-0.10.md create mode 100644 documentation/Modules.md create mode 100644 documentation/ModulesHowTo.md create mode 100644 documentation/SubmitPatchesHowTo:.md create mode 100644 documentation/Templates.md create mode 100644 documentation/ThemesHowTo.md create mode 100644 documentation/i18n.md diff --git a/documentation/CBI.md b/documentation/CBI.md new file mode 100644 index 000000000..0b47c2248 --- /dev/null +++ b/documentation/CBI.md @@ -0,0 +1,248 @@ +CBI models are Lua files describing the structure of an UCI config file and the resulting HTML form to be evaluated by the CBI parser. +All CBI model files must return an object of type *luci.cbi.Map*. For a commented example of a CBI model, see the [[Documentation/ModulesHowTo#CBImodels|Writing Modules tutorial]]. + +The scope of a CBI model file is automatically extended by the contents of the module *luci.cbi_' and the '_translate* function from luci.i18n + +This Reference covers *the basics* of the CBI system. + + + +# class Map (_config'', ''title'', ''description_) +This is the root object of the model. +* *config*: configuration name to be mapped, see uci documentation and the files in /etc/config +* *title*: title shown in the UI +* *description*: description shown in the UI + +## :section (_sectionclass_, ...) +Creates a new section +* *sectionclass*: a class object of the section +* _additional parameters passed to the constructor of the section class_ + +---- + +# class NamedSection (_name'', ''type'', ''title'', ''description_) +An object describing an UCI section selected by the name. +Use [[#A.3Asection.28.27.27sectionclass.27.27.2C....29|Map:section(NamedSection, _name'', ''type'', ''title'', ''description_)]] to instantiate. +* *name*: section name +* *type*: section type +* *title*: The title shown in the UI +* *description*: description shown in the UI + +## .addremove = false +Allows the user to remove and recreate the configuration section + +## .dynamic = false +Marks this section as dynamic. Dynamic sections can contain an undefinded number of completely userdefined options. + +## .optional = true +Parse optional options + + +## :option (_optionclass_, ...) +Creates a new option +* *optionclass*: a class object of the section +* _additional parameters passed to the constructor of the option class_ + +---- + +# class TypedSection (_type'', ''title'', ''description_) +An object describing a group of UCI sections selected by their type. +Use [[#A.3Asection.28.27.27sectionclass.27.27.2C....29|Map:section(TypedSection, _type'', ''title'', ''description_)]] to instantiate. +* *type*: section type +* *title*: The title shown in the UI +* *description*: description shown in the UI + +## .addremove = false +Allows the user to remove and recreate the configuration section + +## .dynamic = false +Marks this section as dynamic. Dynamic sections can contain an undefinded number of completely userdefined options. + +## .optional = true +Parse optional options + +## .anonymous = false +Do not show section names + + +## :depends (_key'', ''value_) +Only select those sections where the option _key'' == ''value_
+If you call this function several times the dependencies will be linked with *or* + +## .filter (_self'', ''section_) [abstract] +You can override this function to filter certain sections that will not be parsed. +The filter function will be called for every section that should be parsed and returns *nil* for sections that should be filtered. For all other sections it should return the section name as given in the second parameter. + +## :option (_optionclass_, ...) +Creates a new option + _optionclass_: a class object of the section + additional parameters passed to the constructor of the option class + +---- + +# class Value (_option'', ''title'', ''description_) +An object describing an option in a section of a UCI File. Creates a standard text field in the formular. +Use [[#A.3Aoption.28.27.27optionclass.27.27.2C....29|NamedSection:option(Value, _option'', ''title'', ''description'')]] or [[#A.3Aoption.28.27.27optionclass.27.27.2C....29-1|TypedSection:option(Value, ''option'', ''title'', ''description_)]] to instantiate. +* *option*: section name +* *title*: The title shown in the UI +* *description*: description shown in the UI + +## .default = nil +The default value + +## .maxlength = nil +The maximum length of the value + +## .optional = false +Marks this option as optional, implies .rmempty = true + +## .rmempty = true +Removes this option from the configuration file when the user enters an empty value + +## .size = nil +The size of the form field + +## :value (_key'', ''value'' = ''key_) +Convert this text field into a combobox if possible and add a selection option. + + +## :depends (_key'', ''value_) +Only show this option field if another option _key'' is set to ''value_ in the same section.
+If you call this function several times the dependencies will be linked with *or* + +---- + +# class ListValue (_option'', ''title'', ''description_) +An object describing an option in a section of a UCI File. Creates a list box in the formular. +Use [[#A.3Aoption.28.27.27optionclass.27.27.2C....29|NamedSection:option(Value, _option'', ''title'', ''description'')]] or [[#A.3Aoption.28.27.27optionclass.27.27.2C....29-1|TypedSection:option(Value, ''option'', ''title'', ''description_)]] to instantiate. +* *option*: section name +* *title*: The title shown in the UI +* *description*: description shown in the UI + + +## .default = nil +The default value + +## .optional = false +Marks this option as optional, implies .rmempty = true + +## .rmempty = true +Removes this option from the configuration file when the user enters an empty value + +## .size = nil +The size of the form field + +## .widget = "select" +selects the form widget to be used + + +## :depends (_key'', ''value_) +Only show this option field if another option _key'' is set to ''value_ in the same section.
+If you call this function several times the dependencies will be linked with *or* + +## :value (_key'', ''value'' = ''key_) +Adds an entry to the selection list + +---- + +# class Flag (_option'', ''title'', ''description_) +An object describing an option with two possible values in a section of a UCI File. Creates a checkbox field in the formular. +Use [[#A.3Aoption.28.27.27optionclass.27.27.2C....29|NamedSection:option(Value, _option'', ''title'', ''description'')]] or [[#A.3Aoption.28.27.27optionclass.27.27.2C....29-1|TypedSection:option(Value, ''option'', ''title'', ''description_)]] to instantiate. +* *option*: section name +* *title*: The title shown in the UI +* *description*: description shown in the UI + +## .default = nil +The default value + +## .disabled = 0 +the value that shoudl be set if the checkbox is unchecked + +## .enabled = 1 +the value that should be set if the checkbox is checked + +## .optional = false +Marks this option as optional, implies .rmempty = true + +## .rmempty = true +Removes this option from the configuration file when the user enters an empty value + +## .size = nil +The size of the form field + + +## :depends (_key'', ''value_) +Only show this option field if another option _key'' is set to ''value_ in the same section.
+If you call this function several times the dependencies will be linked with *or* + +---- + +# class MultiValue (_option'', ''title'', ''description_) +An object describing an option in a section of a UCI File. Creates several checkboxed as form fields. +Use [[#A.3Aoption.28.27.27optionclass.27.27.2C....29|NamedSection:option(Value, _option'', ''title'', ''description'')]] or [[#A.3Aoption.28.27.27optionclass.27.27.2C....29-1|TypedSection:option(Value, ''option'', ''title'', ''description_)]] to instantiate. +* *option*: section name +* *title*: The title shown in the UI +* *description*: description shown in the UI + + +## .default = nil +The default value + +## .delimiter = " " +The string which will be used to delimit the values + +## .optional = false +Marks this option as optional, implies .rmempty = true + +## .rmempty = true +Removes this option from the configuration file when the user enters an empty value + +## .size = nil +The size of the form field + +## .widget = "checkbox" +selects the form widget to be used + + +## :depends (_key'', ''value_) +Only show this option field if another option _key'' is set to ''value_ in the same section.
+If you call this function several times the dependencies will be linked with *or* + +## :value (_key'', ''value'' = ''key_) +Adds an entry to the checkbox list + +---- + +# class DummyValue (_option'', ''title'', ''description_) +An object describing an option in a section of a UCI File. Creates a readonly field in the form. +Use [[#A.3Aoption.28.27.27optionclass.27.27.2C....29|NamedSection:option(Value, _option'', ''title'', ''description'')]] or [[#A.3Aoption.28.27.27optionclass.27.27.2C....29-1|TypedSection:option(Value, ''option'', ''title'', ''description_)]] to instantiate. +* *option*: section name +* *title*: The title shown in the UI +* *description*: description shown in the UI + + + +## :depends (_key'', ''value_) +Only show this option field if another option _key'' is set to ''value_ in the same section.
+If you call this function several times the dependencies will be linked with *or* + +---- + + +# class TextValue (_option'', ''title'', ''description_) +An object describing a multi-line textbox in a section in a non-UCI form. + +---- + +# class Button (_option'', ''title'', ''description_) +An object describing a Button in a section in a non-UCI form. + +---- + +# class StaticList (_option'', ''title'', ''description_) +Similar to the MultiValue, but stores selected Values into a UCI list instead of a space-separated string. + +---- + +# class DynamicList (_option'', ''title'', ''description_) +A list of user-defined values. diff --git a/documentation/JsonRpcHowTo.md b/documentation/JsonRpcHowTo.md new file mode 100644 index 000000000..76d61f86e --- /dev/null +++ b/documentation/JsonRpcHowTo.md @@ -0,0 +1,66 @@ +LuCI provides some of its libraries to external applications through a JSON-RPC API. +This Howto shows how to use it and provides information about available functions. + + +# Basics +LuCI comes with an efficient JSON De-/Encoder together with a JSON-RPC-Server which implements the *JSON-RPC 1.0_' and 2.0 (partly) specifications. The LuCI JSON-RPC server offers several independent APIs. Therefore you have to use '_different URLs for every exported library*. +Assuming your LuCI-Installation can be reached through */cgi-bin/luci_' any exported library can be reached via '''/cgi-bin/luci/rpc/''LIBRARY_*. + + +# Authentication +Most exported libraries will require a valid authentication to be called with. If you get an *HTTP 403 Forbidden_' status code you are probably missing a valid authentication token. To get such a token you have to call the function '''login''' of the RPC-Library '''auth'''. Following our example from above this login function would be provided at '_/cgi-bin/luci/rpc/auth*. The function accepts 2 parameters: username and password (of a valid user account on the host system) and returns an authentication token. + +If you want to call any exported library which requires an authentication token you have to *append it as an URL parameter _auth''''' to the RPC-Server URL. So instead of calling '''/cgi-bin/luci/rpc/''LIBRARY''''' you have to call '''/cgi-bin/luci/rpc/''LIBRARY''?auth=''TOKEN_*. + +If your JSON-RPC client is Cookie-aware (like most browsers are) you will receive the authentication token also with a session cookie and probably don't have to append it to the RPC-Server URL. + + +# Exported Libraries +## uci +The UCI-Library */rpc/uci* offers functionality to interact with the Universal Configuration Interface. +*Exported Functions:* +* [(string) add(config, type)](http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.add) +* [(integer) apply(config)](http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.apply) +* [http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.changes (object) changes([config])] +* [(boolean) commit(config)](http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.commit) +* [http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.delete (boolean) delete(config, section[, option])] +* [http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.delete_all (boolean) delete_all(config[, type])] +* [http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.foreach (array) foreach(config[, type])] +* [http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.get (mixed) get(config, section[, option])] +* [http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.get_all (object) get_all(config[, section])] +* [http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.get (mixed) get_state(config, section[, option])] +* [(boolean) revert(config)](http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.revert) +* [(name) section(config, type, name, values)](http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.section) +* [(boolean) set(config, section, option, value)](http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.set) +* [(boolean) tset(config, section, values)](http://luci.subsignal.org/api/luci/modules/luci.model.uci.html#Cursor.tset) + +## uvl +The UVL-Library */rpc/uvl* offers functionality to validate UCI files and get schemes describing UCI files. +*Exported Functions:* +* [(array) get_scheme(scheme)](http://luci.subsignal.org/api/luci/modules/luci.uvl.html#UVL.get_scheme) +* [(array) validate(config, section, option)](http://luci.subsignal.org/api/luci/modules/luci.uvl.html#UVL.validate) +* [(array) validate_config(config)](http://luci.subsignal.org/api/luci/modules/luci.uvl.html#UVL.validate_config) +* [(array) validate_section(config, section)](http://luci.subsignal.org/api/luci/modules/luci.uvl.html#UVL.validate_section) +* [(array) validate(config, section, option)](http://luci.subsignal.org/api/luci/modules/luci.uvl.html#UVL.validate_option) + +## fs +The Filesystem library */rpc/fs* offers functionality to interact with the filesystem on the host machine. +*Exported Functions:* + +* [Complete luci.fs library](http://luci.subsignal.org/api/luci/modules/luci.fs.html) +*Note:* All functions are exported as they are except for _readfile'' which encodes its return value in base64 and ''writefile'' which only accepts base64 encoded data as second argument. Note that both functions will only be available when the ''luasocket_ packet is installed on the hostsystem. + +## sys +The System library */rpc/sys* offers functionality to interact with the operating system on the host machine. +*Exported Functions:* +* [Complete luci.sys library](http://luci.subsignal.org/api/luci/modules/luci.sys.html) +* [Complete luci.sys.group library](http://luci.subsignal.org/api/luci/modules/luci.sys.group.html) with prefix *group.* +* [Complete luci.sys.net library](http://luci.subsignal.org/api/luci/modules/luci.sys.net.html) with prefix *net.* +* [Complete luci.sys.process library](http://luci.subsignal.org/api/luci/modules/luci.sys.process.html) with prefix *process.* +* [Complete luci.sys.user library](http://luci.subsignal.org/api/luci/modules/luci.sys.user.html) with prefix *user.* +* [Complete luci.sys.wifi library](http://luci.subsignal.org/api/luci/modules/luci.sys.wifi.html) with prefix *wifi.* + +## ipkg +The IPKG library */rpc/ipkg* offers functionality to interact with the package manager (IPKG or OPKG) on the host machine. +*Exported Functions:* +* [Complete luci.model.ipkg library](http://luci.subsignal.org/api/luci/modules/luci.model.ipkg.html) diff --git a/documentation/LAR.md b/documentation/LAR.md new file mode 100644 index 000000000..f44d8dc7d --- /dev/null +++ b/documentation/LAR.md @@ -0,0 +1,87 @@ +LAR is a simple archive format to pack multiple lua source files and arbitary other resources into a single file. + + +# Format Specification + +A LAR archive file is divided into two parts: the payload and the index lookup table. +All segments of the archive are 4 Byte aligned to ease reading and processing of the format. +All integers are stored in network byte order, so an implementation has to use htonl() and htons() to properly read them. + +Schema: + + + + > + + + + > + + ... + + + + > + > + + + + + > + + + + + > + + ... + + + + + > + > + + + + + +# Processing + +In order to process an LAR archive, an implementation would have to do the following steps: + +## Read Index + +1. Locate and open the archive file +1. Seek to end of file - 4 bytes +1. Read 32bit index offset and swap from network to native byte order +1. Seek to index offset, calculate index length: filesize - index offset - 4 +1. Initialize a linked list for index table entries +1. Read each index entry until the index length is reached, read and byteswap 4 * 32bit int and 2 * 16bit int +1. Seek to begin of file + +## Read Member + +1. Read the archive index +1. Iterate through the linked index list, perform the following steps for each entry +1. Seek to the specified file path offset +1. Read as much bytes as specified in the file path length into a buffer +1. Compare the contents of the buffer against the path of the searched member +1. If buffer and searched path are equal, seek to the specified file data offset +1. Read data until the file data length is reached, return +1. Select the next index table entry and repeat from step 3, if there is no next entry then return + +# Reference implementation + +A reference implementation can be found here: +http://luci.subsignal.org/trac/browser/luci/trunk/contrib/lar + +The lar.pl script is a simple packer for LAR archives and cli.c provides a utility to list and dump packed LAR archives. diff --git a/documentation/LMO.md b/documentation/LMO.md new file mode 100644 index 000000000..961a45ba8 --- /dev/null +++ b/documentation/LMO.md @@ -0,0 +1,144 @@ +LMO is a simple binary format to pack language strings into a more efficient form. Although it's suitable to store any kind of key-value table, it's only used for the LuCI *.po based translation system at the moment. The abbreviation "LMO" stands for "Lua Machine Objects" in the style of the GNU gettext *.mo format. + + +# Format Specification + +A LMO file is divided into two parts: the payload and the index lookup table. +All segments of the file are 4 Byte aligned to ease reading and processing of the format. +Only unsigned 32bit integers are used and stored in network byte order, so an implementation has to use htonl() to properly read them. + +Schema: + + + + + + ... + + + > + + + + + + > + + + + + + > + + ... + + + + + + > + > + + + > + + + +# Processing + +In order to process a LMO file, an implementation would have to do the following steps: + +## Read Index + +1. Locate and open the archive file +1. Seek to end of file - 4 bytes (sizeof(uint32_t)) +1. Read 32bit index offset and swap from network to native byte order +1. Seek to index offset, calculate index length: filesize - index offset - 4 +1. Initialize a linked list for index table entries +1. Read each index entry until the index length is reached, read and byteswap 4 * uint32_t for each step +1. Seek to begin of file + +## Read Entry + +1. Calculate the unsigned 32bit hash of the entries key value (see "Hash Function" section below) +1. Obtain the archive index +1. Iterate through the linked index list, perform the following steps for each entry: + 1. Compare the entry hash value with the calculated hash from step 1 + 2. If the hash values are equal proceed with step 4 + 3. Select the next entry and repeat from step 3.1 +1. Seek to the file offset specified in the selected entry +1. Read as much bytes as specified in the entry length into a buffer +1. Return the buffer value + +# Hash Function + +The current LuCI-LMO implementation uses the "Super Fast Hash" function which was kindly put in the public domain by it's original author. See http://www.azillionmonkeys.com/qed/hash.html for details. Below is the C-Implementation of this function: + + + #if (defined(__GNUC__) && defined(__i386__)) + #define sfh_get16(d) (*((const uint16_t *) (d))) + #else + #define sfh_get16(d) ((((uint32_t)(((const uint8_t *)(d))[1])) << 8)\ + +(uint32_t)(((const uint8_t *)(d))[0]) ) + #endif + + uint32_t sfh_hash(const char * data, int len) + { + uint32_t hash = len, tmp; + int rem; + + if (len <= NULL) return 0; + + rem = len & 3; + len >>= 2; + + /* Main loop */ + for (;len > 0; len--) { + hash += sfh_get16(data); + tmp = (sfh_get16(data+2) << 11) ^ hash; + hash = (hash << 16) ^ tmp; + data += 2*sizeof(uint16_t); + hash += hash >> 11; + } + + /* Handle end cases */ + switch (rem) { + case 3: hash += sfh_get16(data); + hash ^= hash << 16; + hash ^= data[sizeof(uint16_t)] << 18; + hash += hash >> 11; + break; + case 2: hash += sfh_get16(data); + hash ^= hash << 11; + hash += hash >> 17; + break; + case 1: hash += *data; + hash ^= hash << 10; + hash += hash >> 1; + } + + /* Force "avalanching" of final 127 bits */ + hash ^= hash << 3; + hash += hash >> 5; + hash ^= hash << 4; + hash += hash >> 17; + hash ^= hash << 25; + hash += hash >> 6; + + return hash; + } + + +# Reference Implementation + +A reference implementation can be found here: +http://luci.subsignal.org/trac/browser/luci/trunk/libs/lmo/src + +The lmo_po2lmo.c executable implements a *.po to *.lmo conversation utility and lmo_lookup.c is a simple *.lmo test utility. +Lua bindings for lmo are defined in lmo_lualib.c and associated headers. diff --git a/documentation/LuCI-0.10.md b/documentation/LuCI-0.10.md new file mode 100644 index 000000000..5db9895e5 --- /dev/null +++ b/documentation/LuCI-0.10.md @@ -0,0 +1,202 @@ +[[PageOutline(2-5, Table of Contents, floated)]] + + +This document describes new features and incompatibilities to LuCI 0.9.x. +It is targeted at module authors developing external addons to LuCI. + +# I18N Changes + +## API + +The call conventions for the i18n api changed, there is no dedicated translation +key anymore and the english text is used for lookup instead. This was done to +ease the maintenance of language files. + +Code that uses _translate()'' or ''i18n()_ must be changed as follows: + + + -- old style: + translate("some_text", "Some Text") + translatef("some_format_text", "Some formatted Text: %d", 123) + + -- new style: + translate("Some Text") + translatef("Some formatted Text: %d", 123) + + +Likewise for templates: + + + + <%:some_text Some Text%> + + + <%:Some Text%> + + +If code must support both LuCI 0.9.x and 0.10.x versions, it is suggested to write the calls as follows: + + translate("Some Text", "Some Text") + + +An alternative is wrapping translate() calls into a helper function: + + function tr(key, alt) + return translate(key) or translate(alt) or alt + end + + +... which is used as follows: + + tr("some_key", "Some Text") + + +## Translation File Format + +Translation catalogs are now maintained in *.po format files. During build those get translated +into [*.lmo archives](http://luci.subsignal.org/trac/wiki/Documentation/LMO). + +LuCI ships a [utility script](http://luci.subsignal.org/trac/browser/luci/branches/luci-0.10/build/i18n-lua2po.pl) +in the build/ directory to convert old Lua translation files to the *.po format. The generated *.po files should +be placed in the appropriate subdirectories within the top po/ file in the LuCI source tree. + +### Components built within the LuCI tree + +If components using translations are built along with the LuCI tree, the newly added *.po file are automatically +compiled into *.lmo archives during the build process. In order to bundle the appropriate *.lmo files into the +corresponding *.ipk packages, component Makefiles must include a "PO" variable specifying the files to include. + +Given a module _applications/example/'' which uses ''po/en/example.po'' and ''po/en/example-extra.po_, +the _applications/example/Makefile_ must be changed as follows: + + + PO = example example-extra + + include ../../build/config.mk + include ../../build/module.mk + + +### Standalone components + +Authors who externally package LuCI components must prepare required *.lmo archives themselves. +To convert existing Lua based message catalogs to the *.po format, the build/i18n-lua2po.pl helper script can be used. +In order to convert *.po files into *.lmo files, the standalone "po2lmo" utility must be compiled as follows: + + + $ svn co http://svn.luci.subsignal.org/luci/branches/luci-0.10/libs/lmo + $ cd lmo/ + $ make + $ ./src/po2lmo translations.po translations.lmo + + +Note that at the time of writing, the utility program needs Lua headers installed on the system in order to compile properly. + +# CBI + +## Datatypes + +The server side UVL validation has been dropped to reduce space requirements on the target. +Instead it is possible to define datatypes for CBI widgets now: + + + opt = section:option(Value, "optname", "Title Text") + opt.datatype = "ip4addr" + + +User provided data is validated once on the frontend via JavaScript and on the server side prior to saving it. +A list of possible datatypes can be found in the [luci.cbi.datatypes](http://luci.subsignal.org/trac/browser/luci/branches/luci-0.10/libs/web/luasrc/cbi/datatypes.lua#L26) class. + +## Validation + +Server-sided validator function can now return custom error messages to provide better feedback on invalid input. + + + opt = section:option(Value, "optname", "Title Text") + + function opt.validate(self, value, section) + if input_is_valid(value) then + return value + else + return nil, "The value is invalid because ..." + end + end + + +## Tabs + +It is now possible to break up CBI sections into multiple tabs to better organize longer forms. +The TypedSection and NamedSection classes gained two new functions to define tabs, _tab()'' and ''taboption()_. + + + sct = map:section(TypedSection, "name", "type", "Title Text") + + sct:tab("general", "General Tab Title", "General Tab Description") + sct:tab("advanced", "Advanced Tab Title", "Advanced Tab Description") + + opt = sct:taboption("general", Value, "optname", "Title Text") + ... + + +The _tab()_ function is declares a new tab and takes up to three arguments: + * Internal name of the tab, must be unique within the section + * Title text of the tab + * Optional description text for the tab + +The _taboption()'' function wraps ''option()_ and assigns the option object to the given tab. +It takes up to five arguments: + + * Name of the tab to assign the option to + * Option type, e.g. Value or DynamicList + * Option name + * Title text of the option + * Optional description text of the option + +If tabs are used within a particular section, the _option()_ function must not be used, +doing so results in undefined behaviour. + +## Hooks + +The CBI gained support for _hooks_ which can be used to trigger additional actions during the +life-cycle of a map: + + + map = Map("config", "Title Text") + + function map.on_commit(self) + -- do something if the UCI configuration got committed + end + + +The following hooks are defined: + +|| on_cancel || The user pressed cancel within a multi-step Delegator or a SimpleForm instance || +|| on_init || The CBI is about to render the Map object || +|| on_parse || The CBI is about to read received HTTP form values || +|| on_save, on_before_save || The CBI is about to save modified UCI configuration files || +|| on_after_save || Modified UCI configuration files just got saved +|| on_before_commit || The CBI is about to commit the changes || +|| on_commit, on_after_commit, on_before_apply || Modified configurations got committed and the CBI is about to restart associated services || +|| on_apply, on_after_apply || All changes where completely applied (only works on Map instances with the apply_on_parse attribute set) || + +## Sortable Tables + +TypedSection instances which use the "cbi/tblsection" template may now use a new attribute _sortable_ to allow the user to reorder table rows. + + + sct = map:section(TypedSection, "name", "type", "Title Text") + sct.template = "cbi/tblsection" + sct.sortable = true + + ... + + +# JavaScript + +The LuCI 0.10 branch introduced a new JavaScript file _xhr.js_ which provides support routines for XMLHttpRequest operations. +Each theme must include this file in the area of the document for forms to work correctly. + +It should be included like this: + + + + \ No newline at end of file diff --git a/documentation/Modules.md b/documentation/Modules.md new file mode 100644 index 000000000..2897df948 --- /dev/null +++ b/documentation/Modules.md @@ -0,0 +1,94 @@ +# Categories + +The LuCI modules are divided into several category directories, namely: +* applications (Single applications or plugins for other modules or applications) +* i18n (Translation files) +* libs (Independent libraries) +* modules (Collections of applications) +* themes (Frontend themes) + +Each module goes into a subdirectory of any of this category-directories. + +# Module directory +The contents of a module directory are as follows: + +## Makefile +This is the module's makefile. If the module just contains Lua sourcecode or resources then the following Makefile should suffice. + + include ../../build/config.mk + include ../../build/module.mk + + +If you have C(++) code in your module your Makefile should at least contain the following things. + + include ../../build/config.mk + include ../../build/gccconfig.mk + include ../../build/module.mk + + compile: + # Commands to compile and link your C-code + # and to install them under the dist/ hierarchy + + clean: luaclean + # Commands to clean your compiled objects + + + +## src +The *src* directory is reserved for C sourcecode. + +## luasrc +*luasrc* contains all Lua sourcecode files. These will automatically be stripped or compiled depending on the Make target and are installed in the LuCI installation directory. + +## lua +*lua* is equivalent to _luasrc_ but containing Lua files will be installed in the Lua document root. + +## htdocs +All files under *htdocs* will be copied to the document root of the target webserver. + +## root +All directories and files under *root* will be copied to the installation target as they are. + +## dist +*dist* is reserved for the builder to create a working installation tree that will represent the filesystem on the target machine. +*DO NOT* put any files there as they will get deleted. + +## ipkg +*ipkg* contains IPKG package control files, like _preinst'', ''posinst'', ''prerm'', ''postrm''. ''conffiles_. +See IPKG documentation for details. + + +# OpenWRT feed integration +If you want to add your module to the LuCI OpenWRT feed you have to add several sections to the contrib/package/luci/Makefile. + +For a Web UI applications this is: + +A package description: + + define Package/luci-app-YOURMODULE + $(call Package/luci/webtemplate) + DEPENDS+=+some-package +some-other-package + TITLE:=SHORT DESCRIPTION OF YOURMODULE + endef + + + +A package installation target: + + define Package/luci-app-YOURMODULE/install + $(call Package/luci/install/template,$(1),applications/YOURMODULE) + endef + + +A module build instruction: + + ifneq ($(CONFIG_PACKAGE_luci-app-YOURMODULE),) + PKG_SELECTED_MODULES+=applications/YOURMODULE + endif + + + +A build package call: + + $(eval $(call BuildPackage,luci-app-YOURMODULE)) + diff --git a/documentation/ModulesHowTo.md b/documentation/ModulesHowTo.md new file mode 100644 index 000000000..3f70b788c --- /dev/null +++ b/documentation/ModulesHowTo.md @@ -0,0 +1,153 @@ +*Note:* If you plan to integrate your module into LuCI, you should read the [wiki:Documentation/Modules Module Reference] before. + +This tutorial describes how to write your own modules for the LuCI WebUI. +For this tutorial we refer to your LuCI installation direcotry as *lucidir_' (/usr/lib/lua/luci if you are working with an installed version) and assume your LuCI installation is reachable through your webserver via '_/cgi-bin/luci*. + +If you are working with the development environment replace *lucidir_' with '''''/path/to/your/luci/checkout''/applications/myapplication/luasrc''' (this is a default empty module you can use for your experiments) and your LuCI installation can probably be reached via http://localhost:8080/luci/ after you ran '_make runhttpd*. + + + +# Show me the way (The dispatching process) +To write a module you need to understand the basics of the dispatching process in LuCI. +LuCI uses a dispatching tree that will be built by executing the index-Function of every available controller. +The CGI-environment variable *PATH_INFO* will be used as the path in this dispatching tree, e.g.: /cgi-bin/luci/foo/bar/baz +will be resolved to foo.bar.baz + +To register a function in the dispatching tree, you can use the *entry*-function of _luci.dispatcher_. entry takes 4 arguments (2 are optional): + + entry(path, target, title=nil, order=nil) + + +* *path* is a table that describes the position in the dispatching tree: For example a path of {"foo", "bar", "baz"} would insert your node in foo.bar.baz. +* *target* describes the action that will be taken when a user requests the node. There are several predefined ones of which the 3 most important (call, template, cbi) are described later on on this page +* *title* defines the title that will be visible to the user in the menu (optional) +* *order* is a number with which nodes on the same level will be sorted in the menu (optional) + +You can assign more attributes by manipulating the node table returned by the entry-function. A few example attributes: + +* *i18n* defines which translation file should be automatically loaded when the page gets requested +* *dependent* protects plugins to be called out of their context if a parent node is missing +* *leaf* stops parsing the request at this node and goes no further in the dispatching tree +* *sysauth* requires the user to authenticate with a given system user account + + +# It's all about names (Naming and the module file) +Now that you know the basics about dispatching, we can start writing modules. But before you have to choose the category and name of your new digital child. + +We assume you want to create a new application "myapp" with a module "mymodule". + +So you have to create a new subdirectory *_lucidir''/controller/myapp''' with a file '_mymodule.lua* with the following content: + + module("luci.controller.myapp.mymodule", package.seeall) + + function index() + + end + + +The first line is required for Lua to correctly identify the module and create its scope. +The index-Function will be used to register actions in the dispatching tree. + + + +# Teaching your new child (Actions) +So it is there and has a name but it has no actions. + +We assume you want to reuse your module myapp.mymodule that you begun in the last step. + + +## Actions +Reopen *_lucidir_/controller/myapp/mymodule.lua* and just add a function to it so that its content looks like this example: + + + module("luci.controller.myapp.mymodule", package.seeall) + + function index() + entry({"click", "here", "now"}, call("action_tryme"), "Click here", 10).dependent=false + end + + function action_tryme() + luci.http.prepare_content("text/plain") + luci.http.write("Haha, rebooting now...") + luci.sys.reboot() + end + + +And now type */cgi-bin/luci/click/here/now_' ('_[http://localhost:8080/luci/click/here/now]* if you are using the development environment) in your browser. + +You see these action functions simple have to be added to a dispatching entry. + +As you might or might not know: CGI specification requires you to send a Content-Type header before you can send your content. You will find several shortcuts (like the one used above) as well as redirecting functions in the module *luci.http* + +## Views +If you only want to show the user a text or some interesting familiy photos it may be enough to use a HTML-template. These templates can also include some Lua code but be aware that writing whole office suites by only using these templates might be called "dirty" by other developers. + +Now let's create a little template *_lucidir_/view/myapp-mymodule/helloworld.htm* with the content: + + + <%+header%> +

<%:Hello World%>

+ <%+footer%> + + + +and add the following line to the index-Function of your module file. + + entry({"my", "new", "template"}, template("myapp-mymodule/helloworld"), "Hello world", 20).dependent=false + + +Now type */cgi-bin/luci/my/new/template_' ('_[http://localhost:8080/luci/my/new/template]* if you are using the development environment) in your browser. + +You may notice those fancy <% %>-Tags, these are [wiki:Documentation/Templates|template markups] used by the LuCI template processor. +It is always good to include header and footer at the beginning and end of a template as those create the default design and menu. + +## CBI models +The CBI is one of the uber coolest features of LuCI. It creates a formular based user interface and saves its contents to a specific UCI config file. You only have to describe the structure of the configuration file in a CBI model file and Luci does the rest of the work. This includes generating, parsing and validating a XHTML form and reading and writing the UCI file. + +So let's be serious at least for this paragraph and create a real pratical example *_lucidir_/model/cbi/myapp-mymodule/netifaces.lua* with the following contents: + + + m = Map("network", "Network") -- We want to edit the uci config file /etc/config/network + + s = m:section(TypedSection, "interface", "Interfaces") -- Especially the "interface"-sections + s.addremove = true -- Allow the user to create and remove the interfaces + function s:filter(value) + return value ~= "loopback" and value -- Don't touch loopback + end + s:depends("proto", "static") -- Only show those with "static" + s:depends("proto", "dhcp") -- or "dhcp" as protocol and leave PPPoE and PPTP alone + + p = s:option(ListValue, "proto", "Protocol") -- Creates an element list (select box) + p:value("static", "static") -- Key and value pairs + p:value("dhcp", "DHCP") + p.default = "static" + + s:option(Value, "ifname", "interface", "the physical interface to be used") -- This will give a simple textbox + + s:option(Value, "ipaddr", translate("ip", "IP Address")) -- Ja, das ist eine i18n-Funktion ;-) + + s:option(Value, "netmask", "Netmask"):depends("proto", "static") -- You may remember this "depends" function from above + + mtu = s:option(Value, "mtu", "MTU") + mtu.optional = true -- This one is very optional + + dns = s:option(Value, "dns", "DNS-Server") + dns:depends("proto", "static") + dns.optional = true + function dns:validate(value) -- Now, that's nifty, eh? + return value:match("[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+") -- Returns nil if it doesn't match otherwise returns match + end + + gw = s:option(Value, "gateway", "Gateway") + gw:depends("proto", "static") + gw.rmempty = true -- Remove entry if it is empty + + return m -- Returns the map + + +and of course don't forget to add something like this to your module's index-Function. + + entry({"admin", "network", "interfaces"}, cbi("myapp-mymodule/netifaces"), "Network interfaces", 30).dependent=false + + +There are many more features, see [wiki:Documentation/CBI the CBI reference] and the modules shipped with LuCI. diff --git a/documentation/SubmitPatchesHowTo:.md b/documentation/SubmitPatchesHowTo:.md new file mode 100644 index 000000000..cdd15524a --- /dev/null +++ b/documentation/SubmitPatchesHowTo:.md @@ -0,0 +1,33 @@ +# Checkout svn + + svn co http://svn.luci.subsignal.org/luci/trunk + +and change to that directory: + + cd trunk + +# Make your changes + +Edit the files you want to change. If you add some new files you need to add them to the svn tree: + + svn add + +Where are the directories/files you have added. Its possible to specify multiple files/directories here. + +# Use svn diff to generate a patch with your changes + +To check if your changes look ok first do: + + svn diff + +and check the output. Again you can specify multiple dirs/directories here. + +If everything looks like expected save the patch: + + svn diff > ./mypatch.patch + + +# Submit patches + +Use the [Ticket system](http://luci.subsignal.org/trac/newticket) to submit your patch. + diff --git a/documentation/Templates.md b/documentation/Templates.md new file mode 100644 index 000000000..6affd7fcf --- /dev/null +++ b/documentation/Templates.md @@ -0,0 +1,66 @@ +LuCI has a simple regex based template processor which parses HTML-files to Lua functions and allows to store precompiled template files. +The simplest form of a template is just an ordinary HTML-file. It will be printed out to the user as is. + +In LuCI every template is an object with an own scope. It can therefore be instanciated and each instance can has a different scope. As every template processor. LuCI supports several special markups. Those are enclosed in *<% %>-Tags*. + +By adding a *-_' right after the opening '''<%''' every whitespace before the markup will be stripped. Adding a '''-''' right before the closing '_%>* will equivalently strip every whitespace behind the markup. + +<
> + + +# Builtin functions and markups +## Including Lua code +*Markup:* + + <% code %> + + + +## Writing variables and function values +*Syntax:* + + <% write (value) %> + + +*Short-Markup:* + + <%=value%> + + +## Including templates +*Syntax:* + + <% include (templatename) %> + + +*Short-Markup:* + + <%+templatename%> + + + +## Translating +*Syntax:* + + <%= translate("Text to translate") %> + + + +*Short-Markup:* + + <%:Text to translate%> + + + +## Commenting +*Markup:* + + <%# comment %> + + +# Builtin constants +||*Name_'||'_Value*|| +||REQUEST_URI||The current URL (without server part)|| +||controller||Path to the Luci main dispatcher|| +||resource||Path to the resource directory|| +||media||Path to the active theme directory|| diff --git a/documentation/ThemesHowTo.md b/documentation/ThemesHowTo.md new file mode 100644 index 000000000..46b95fc71 --- /dev/null +++ b/documentation/ThemesHowTo.md @@ -0,0 +1,76 @@ +# HowTo: Create Themes +*Note:* You should read the [wiki:Documentation/Modules Module Reference] and the [wiki:Documentation/Templates Template Reference] before. + +We assume you want to call your new theme _mytheme_. Make sure you replace this by your module name everytime this is mentionend in this Howto. + + + +# Creating the structure +At first create a new theme directory *themes/_mytheme_*. + +Create a _Makefile_ inside your theme directory with the following content: + + include ../../build/config.mk + include ../../build/module.mk + + +Create the following directory structure inside your theme directory. +* ipkg +* htdocs + * luci-static + * _mytheme_ +* luasrc + * view + * themes + * _mytheme_ +* root + * etc + * uci-defaults + + + +# Designing +Create two LuCI HTML-Templates named _header.htm'' and ''footer.htm'' under *luasrc/view/themes/''mytheme_*. +The _header.htm'' will be included at the beginning of each rendered page and the ''footer.htm_ at the end. +So your _header.htm'' will probably contain a DOCTYPE description, headers, the menu and layout of the page and the ''footer.htm_ will close all remaining open tags and may add a footer bar but hey that's your choice you are the designer ;-). + +Just make sure your _header.htm_ *begins* with the following lines: + + <% + require("luci.http").prepare_content("text/html") + -%> + + +This makes sure your content will be sent to the client with the right content type. Of course you can adapt _text/html_ to your needs. + + +Put any stylesheets, Javascripts, images, ... into *htdocs/luci-static/_mytheme_*. +You should refer to this directory in your header and footer templates as: _<%=media%>''. That means for a stylesheet *htdocs/luci-static/''mytheme_/cascade.css* you would write: + + + + + + +# Making the theme selectable +If you are done with your work there are two last steps to do. +To make your theme OpenWRT-capable and selectable on the settings page you should now create a file *root/etc/uci-defaults/luci-theme-_mytheme_* with the following contents: + + #!/bin/sh + uci batch <<-EOF + set luci.themes.MyTheme=/luci-static/mytheme + commit luci + EOF + + +and another file *ipkg/postinst* with the following content: + + #!/bin/sh + [ -n "${IPKG_INSTROOT}" ] || { + ( . /etc/uci-defaults/luci-theme-mytheme ) && rm -f /etc/uci-defaults/luci-theme-mytheme + } + + +This is some OpenWRT magic to correctly register the template with LuCI when it gets installed. + +That's all. Now send your theme to the LuCI developers to get it into the development repository - if you like. diff --git a/documentation/i18n.md b/documentation/i18n.md new file mode 100644 index 000000000..fdacb0853 --- /dev/null +++ b/documentation/i18n.md @@ -0,0 +1,17 @@ +# General +Translations are saved in the folder po/. You find the reference in po/templates/.pot. The actual translation files can be found at po//.po . + +In order to use the commands below you need to have the _gettext'' utilities (''msgcat'', ''msgfmt'', ''msgmerge_) installed on your system. + +# Rebuild po files +If you want to rebuild the translations after you made changes to a package this is an easy way: + + + ./build/i18n-scan.pl applications/[package] > po/templates/[application].pot + ./build/i18n-update.pl po [application].po + +*Note:* Some packages share translation files, in this case you need to scan through all their folders. The first command from above should then be: + + + ./build/i18n-scan.pl applications/[package-1] applications/[package-2] applications/[package-n] > po/templates/[application].pot + -- 2.25.1