luci.js 95 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430
  1. /**
  2. * @class LuCI
  3. * @classdesc
  4. *
  5. * This is the LuCI base class. It is automatically instantiated and
  6. * accessible using the global `L` variable.
  7. *
  8. * @param {Object} env
  9. * The environment settings to use for the LuCI runtime.
  10. */
  11. (function(window, document, undefined) {
  12. 'use strict';
  13. var env = {};
  14. /* Object.assign polyfill for IE */
  15. if (typeof Object.assign !== 'function') {
  16. Object.defineProperty(Object, 'assign', {
  17. value: function assign(target, varArgs) {
  18. if (target == null)
  19. throw new TypeError('Cannot convert undefined or null to object');
  20. var to = Object(target);
  21. for (var index = 1; index < arguments.length; index++)
  22. if (arguments[index] != null)
  23. for (var nextKey in arguments[index])
  24. if (Object.prototype.hasOwnProperty.call(arguments[index], nextKey))
  25. to[nextKey] = arguments[index][nextKey];
  26. return to;
  27. },
  28. writable: true,
  29. configurable: true
  30. });
  31. }
  32. /* Promise.finally polyfill */
  33. if (typeof Promise.prototype.finally !== 'function') {
  34. Promise.prototype.finally = function(fn) {
  35. var onFinally = function(cb) {
  36. return Promise.resolve(fn.call(this)).then(cb);
  37. };
  38. return this.then(
  39. function(result) { return onFinally.call(this, function() { return result }) },
  40. function(reason) { return onFinally.call(this, function() { return Promise.reject(reason) }) }
  41. );
  42. };
  43. }
  44. /*
  45. * Class declaration and inheritance helper
  46. */
  47. var toCamelCase = function(s) {
  48. return s.replace(/(?:^|[\. -])(.)/g, function(m0, m1) { return m1.toUpperCase() });
  49. };
  50. /**
  51. * @class baseclass
  52. * @hideconstructor
  53. * @memberof LuCI
  54. * @classdesc
  55. *
  56. * `LuCI.baseclass` is the abstract base class all LuCI classes inherit from.
  57. *
  58. * It provides simple means to create subclasses of given classes and
  59. * implements prototypal inheritance.
  60. */
  61. var superContext = {}, classIndex = 0, Class = Object.assign(function() {}, {
  62. /**
  63. * Extends this base class with the properties described in
  64. * `properties` and returns a new subclassed Class instance
  65. *
  66. * @memberof LuCI.baseclass
  67. *
  68. * @param {Object<string, *>} properties
  69. * An object describing the properties to add to the new
  70. * subclass.
  71. *
  72. * @returns {LuCI.baseclass}
  73. * Returns a new LuCI.baseclass sublassed from this class, extended
  74. * by the given properties and with its prototype set to this base
  75. * class to enable inheritance. The resulting value represents a
  76. * class constructor and can be instantiated with `new`.
  77. */
  78. extend: function(properties) {
  79. var props = {
  80. __id__: { value: classIndex },
  81. __base__: { value: this.prototype },
  82. __name__: { value: properties.__name__ || 'anonymous' + classIndex++ }
  83. };
  84. var ClassConstructor = function() {
  85. if (!(this instanceof ClassConstructor))
  86. throw new TypeError('Constructor must not be called without "new"');
  87. if (Object.getPrototypeOf(this).hasOwnProperty('__init__')) {
  88. if (typeof(this.__init__) != 'function')
  89. throw new TypeError('Class __init__ member is not a function');
  90. this.__init__.apply(this, arguments)
  91. }
  92. else {
  93. this.super('__init__', arguments);
  94. }
  95. };
  96. for (var key in properties)
  97. if (!props[key] && properties.hasOwnProperty(key))
  98. props[key] = { value: properties[key], writable: true };
  99. ClassConstructor.prototype = Object.create(this.prototype, props);
  100. ClassConstructor.prototype.constructor = ClassConstructor;
  101. Object.assign(ClassConstructor, this);
  102. ClassConstructor.displayName = toCamelCase(props.__name__.value + 'Class');
  103. return ClassConstructor;
  104. },
  105. /**
  106. * Extends this base class with the properties described in
  107. * `properties`, instantiates the resulting subclass using
  108. * the additional optional arguments passed to this function
  109. * and returns the resulting subclassed Class instance.
  110. *
  111. * This function serves as a convenience shortcut for
  112. * {@link LuCI.baseclass.extend Class.extend()} and subsequent
  113. * `new`.
  114. *
  115. * @memberof LuCI.baseclass
  116. *
  117. * @param {Object<string, *>} properties
  118. * An object describing the properties to add to the new
  119. * subclass.
  120. *
  121. * @param {...*} [new_args]
  122. * Specifies arguments to be passed to the subclass constructor
  123. * as-is in order to instantiate the new subclass.
  124. *
  125. * @returns {LuCI.baseclass}
  126. * Returns a new LuCI.baseclass instance extended by the given
  127. * properties with its prototype set to this base class to
  128. * enable inheritance.
  129. */
  130. singleton: function(properties /*, ... */) {
  131. return Class.extend(properties)
  132. .instantiate(Class.prototype.varargs(arguments, 1));
  133. },
  134. /**
  135. * Calls the class constructor using `new` with the given argument
  136. * array being passed as variadic parameters to the constructor.
  137. *
  138. * @memberof LuCI.baseclass
  139. *
  140. * @param {Array<*>} params
  141. * An array of arbitrary values which will be passed as arguments
  142. * to the constructor function.
  143. *
  144. * @param {...*} [new_args]
  145. * Specifies arguments to be passed to the subclass constructor
  146. * as-is in order to instantiate the new subclass.
  147. *
  148. * @returns {LuCI.baseclass}
  149. * Returns a new LuCI.baseclass instance extended by the given
  150. * properties with its prototype set to this base class to
  151. * enable inheritance.
  152. */
  153. instantiate: function(args) {
  154. return new (Function.prototype.bind.apply(this,
  155. Class.prototype.varargs(args, 0, null)))();
  156. },
  157. /* unused */
  158. call: function(self, method) {
  159. if (typeof(this.prototype[method]) != 'function')
  160. throw new ReferenceError(method + ' is not defined in class');
  161. return this.prototype[method].apply(self, self.varargs(arguments, 1));
  162. },
  163. /**
  164. * Checks whether the given class value is a subclass of this class.
  165. *
  166. * @memberof LuCI.baseclass
  167. *
  168. * @param {LuCI.baseclass} classValue
  169. * The class object to test.
  170. *
  171. * @returns {boolean}
  172. * Returns `true` when the given `classValue` is a subclass of this
  173. * class or `false` if the given value is not a valid class or not
  174. * a subclass of this class'.
  175. */
  176. isSubclass: function(classValue) {
  177. return (classValue != null &&
  178. typeof(classValue) == 'function' &&
  179. classValue.prototype instanceof this);
  180. },
  181. prototype: {
  182. /**
  183. * Extract all values from the given argument array beginning from
  184. * `offset` and prepend any further given optional parameters to
  185. * the beginning of the resulting array copy.
  186. *
  187. * @memberof LuCI.baseclass
  188. * @instance
  189. *
  190. * @param {Array<*>} args
  191. * The array to extract the values from.
  192. *
  193. * @param {number} offset
  194. * The offset from which to extract the values. An offset of `0`
  195. * would copy all values till the end.
  196. *
  197. * @param {...*} [extra_args]
  198. * Extra arguments to add to prepend to the resultung array.
  199. *
  200. * @returns {Array<*>}
  201. * Returns a new array consisting of the optional extra arguments
  202. * and the values extracted from the `args` array beginning with
  203. * `offset`.
  204. */
  205. varargs: function(args, offset /*, ... */) {
  206. return Array.prototype.slice.call(arguments, 2)
  207. .concat(Array.prototype.slice.call(args, offset));
  208. },
  209. /**
  210. * Walks up the parent class chain and looks for a class member
  211. * called `key` in any of the parent classes this class inherits
  212. * from. Returns the member value of the superclass or calls the
  213. * member as function and returns its return value when the
  214. * optional `callArgs` array is given.
  215. *
  216. * This function has two signatures and is sensitive to the
  217. * amount of arguments passed to it:
  218. * - `super('key')` -
  219. * Returns the value of `key` when found within one of the
  220. * parent classes.
  221. * - `super('key', ['arg1', 'arg2'])` -
  222. * Calls the `key()` method with parameters `arg1` and `arg2`
  223. * when found within one of the parent classes.
  224. *
  225. * @memberof LuCI.baseclass
  226. * @instance
  227. *
  228. * @param {string} key
  229. * The name of the superclass member to retrieve.
  230. *
  231. * @param {Array<*>} [callArgs]
  232. * An optional array of function call parameters to use. When
  233. * this parameter is specified, the found member value is called
  234. * as function using the values of this array as arguments.
  235. *
  236. * @throws {ReferenceError}
  237. * Throws a `ReferenceError` when `callArgs` are specified and
  238. * the found member named by `key` is not a function value.
  239. *
  240. * @returns {*|null}
  241. * Returns the value of the found member or the return value of
  242. * the call to the found method. Returns `null` when no member
  243. * was found in the parent class chain or when the call to the
  244. * superclass method returned `null`.
  245. */
  246. super: function(key, callArgs) {
  247. if (key == null)
  248. return null;
  249. var slotIdx = this.__id__ + '.' + key,
  250. symStack = superContext[slotIdx],
  251. protoCtx = null;
  252. for (protoCtx = Object.getPrototypeOf(symStack ? symStack[0] : Object.getPrototypeOf(this));
  253. protoCtx != null && !protoCtx.hasOwnProperty(key);
  254. protoCtx = Object.getPrototypeOf(protoCtx)) {}
  255. if (protoCtx == null)
  256. return null;
  257. var res = protoCtx[key];
  258. if (arguments.length > 1) {
  259. if (typeof(res) != 'function')
  260. throw new ReferenceError(key + ' is not a function in base class');
  261. if (typeof(callArgs) != 'object')
  262. callArgs = this.varargs(arguments, 1);
  263. if (symStack)
  264. symStack.unshift(protoCtx);
  265. else
  266. superContext[slotIdx] = [ protoCtx ];
  267. res = res.apply(this, callArgs);
  268. if (symStack && symStack.length > 1)
  269. symStack.shift(protoCtx);
  270. else
  271. delete superContext[slotIdx];
  272. }
  273. return res;
  274. },
  275. /**
  276. * Returns a string representation of this class.
  277. *
  278. * @returns {string}
  279. * Returns a string representation of this class containing the
  280. * constructor functions `displayName` and describing the class
  281. * members and their respective types.
  282. */
  283. toString: function() {
  284. var s = '[' + this.constructor.displayName + ']', f = true;
  285. for (var k in this) {
  286. if (this.hasOwnProperty(k)) {
  287. s += (f ? ' {\n' : '') + ' ' + k + ': ' + typeof(this[k]) + '\n';
  288. f = false;
  289. }
  290. }
  291. return s + (f ? '' : '}');
  292. }
  293. }
  294. });
  295. /**
  296. * @class headers
  297. * @memberof LuCI
  298. * @hideconstructor
  299. * @classdesc
  300. *
  301. * The `Headers` class is an internal utility class exposed in HTTP
  302. * response objects using the `response.headers` property.
  303. */
  304. var Headers = Class.extend(/** @lends LuCI.headers.prototype */ {
  305. __name__: 'LuCI.headers',
  306. __init__: function(xhr) {
  307. var hdrs = this.headers = {};
  308. xhr.getAllResponseHeaders().split(/\r\n/).forEach(function(line) {
  309. var m = /^([^:]+):(.*)$/.exec(line);
  310. if (m != null)
  311. hdrs[m[1].trim().toLowerCase()] = m[2].trim();
  312. });
  313. },
  314. /**
  315. * Checks whether the given header name is present.
  316. * Note: Header-Names are case-insensitive.
  317. *
  318. * @instance
  319. * @memberof LuCI.headers
  320. * @param {string} name
  321. * The header name to check
  322. *
  323. * @returns {boolean}
  324. * Returns `true` if the header name is present, `false` otherwise
  325. */
  326. has: function(name) {
  327. return this.headers.hasOwnProperty(String(name).toLowerCase());
  328. },
  329. /**
  330. * Returns the value of the given header name.
  331. * Note: Header-Names are case-insensitive.
  332. *
  333. * @instance
  334. * @memberof LuCI.headers
  335. * @param {string} name
  336. * The header name to read
  337. *
  338. * @returns {string|null}
  339. * The value of the given header name or `null` if the header isn't present.
  340. */
  341. get: function(name) {
  342. var key = String(name).toLowerCase();
  343. return this.headers.hasOwnProperty(key) ? this.headers[key] : null;
  344. }
  345. });
  346. /**
  347. * @class response
  348. * @memberof LuCI
  349. * @hideconstructor
  350. * @classdesc
  351. *
  352. * The `Response` class is an internal utility class representing HTTP responses.
  353. */
  354. var Response = Class.extend({
  355. __name__: 'LuCI.response',
  356. __init__: function(xhr, url, duration, headers, content) {
  357. /**
  358. * Describes whether the response is successful (status codes `200..299`) or not
  359. * @instance
  360. * @memberof LuCI.response
  361. * @name ok
  362. * @type {boolean}
  363. */
  364. this.ok = (xhr.status >= 200 && xhr.status <= 299);
  365. /**
  366. * The numeric HTTP status code of the response
  367. * @instance
  368. * @memberof LuCI.response
  369. * @name status
  370. * @type {number}
  371. */
  372. this.status = xhr.status;
  373. /**
  374. * The HTTP status description message of the response
  375. * @instance
  376. * @memberof LuCI.response
  377. * @name statusText
  378. * @type {string}
  379. */
  380. this.statusText = xhr.statusText;
  381. /**
  382. * The HTTP headers of the response
  383. * @instance
  384. * @memberof LuCI.response
  385. * @name headers
  386. * @type {LuCI.headers}
  387. */
  388. this.headers = (headers != null) ? headers : new Headers(xhr);
  389. /**
  390. * The total duration of the HTTP request in milliseconds
  391. * @instance
  392. * @memberof LuCI.response
  393. * @name duration
  394. * @type {number}
  395. */
  396. this.duration = duration;
  397. /**
  398. * The final URL of the request, i.e. after following redirects.
  399. * @instance
  400. * @memberof LuCI.response
  401. * @name url
  402. * @type {string}
  403. */
  404. this.url = url;
  405. /* privates */
  406. this.xhr = xhr;
  407. if (content instanceof Blob) {
  408. this.responseBlob = content;
  409. this.responseJSON = null;
  410. this.responseText = null;
  411. }
  412. else if (content != null && typeof(content) == 'object') {
  413. this.responseBlob = null;
  414. this.responseJSON = content;
  415. this.responseText = null;
  416. }
  417. else if (content != null) {
  418. this.responseBlob = null;
  419. this.responseJSON = null;
  420. this.responseText = String(content);
  421. }
  422. else {
  423. this.responseJSON = null;
  424. if (xhr.responseType == 'blob') {
  425. this.responseBlob = xhr.response;
  426. this.responseText = null;
  427. }
  428. else {
  429. this.responseBlob = null;
  430. this.responseText = xhr.responseText;
  431. }
  432. }
  433. },
  434. /**
  435. * Clones the given response object, optionally overriding the content
  436. * of the cloned instance.
  437. *
  438. * @instance
  439. * @memberof LuCI.response
  440. * @param {*} [content]
  441. * Override the content of the cloned response. Object values will be
  442. * treated as JSON response data, all other types will be converted
  443. * using `String()` and treated as response text.
  444. *
  445. * @returns {LuCI.response}
  446. * The cloned `Response` instance.
  447. */
  448. clone: function(content) {
  449. var copy = new Response(this.xhr, this.url, this.duration, this.headers, content);
  450. copy.ok = this.ok;
  451. copy.status = this.status;
  452. copy.statusText = this.statusText;
  453. return copy;
  454. },
  455. /**
  456. * Access the response content as JSON data.
  457. *
  458. * @instance
  459. * @memberof LuCI.response
  460. * @throws {SyntaxError}
  461. * Throws `SyntaxError` if the content isn't valid JSON.
  462. *
  463. * @returns {*}
  464. * The parsed JSON data.
  465. */
  466. json: function() {
  467. if (this.responseJSON == null)
  468. this.responseJSON = JSON.parse(this.responseText);
  469. return this.responseJSON;
  470. },
  471. /**
  472. * Access the response content as string.
  473. *
  474. * @instance
  475. * @memberof LuCI.response
  476. * @returns {string}
  477. * The response content.
  478. */
  479. text: function() {
  480. if (this.responseText == null && this.responseJSON != null)
  481. this.responseText = JSON.stringify(this.responseJSON);
  482. return this.responseText;
  483. },
  484. /**
  485. * Access the response content as blob.
  486. *
  487. * @instance
  488. * @memberof LuCI.response
  489. * @returns {Blob}
  490. * The response content as blob.
  491. */
  492. blob: function() {
  493. return this.responseBlob;
  494. }
  495. });
  496. var requestQueue = [];
  497. function isQueueableRequest(opt) {
  498. if (!classes.rpc)
  499. return false;
  500. if (opt.method != 'POST' || typeof(opt.content) != 'object')
  501. return false;
  502. if (opt.nobatch === true)
  503. return false;
  504. var rpcBaseURL = Request.expandURL(classes.rpc.getBaseURL());
  505. return (rpcBaseURL != null && opt.url.indexOf(rpcBaseURL) == 0);
  506. }
  507. function flushRequestQueue() {
  508. if (!requestQueue.length)
  509. return;
  510. var reqopt = Object.assign({}, requestQueue[0][0], { content: [], nobatch: true }),
  511. batch = [];
  512. for (var i = 0; i < requestQueue.length; i++) {
  513. batch[i] = requestQueue[i];
  514. reqopt.content[i] = batch[i][0].content;
  515. }
  516. requestQueue.length = 0;
  517. Request.request(rpcBaseURL, reqopt).then(function(reply) {
  518. var json = null, req = null;
  519. try { json = reply.json() }
  520. catch(e) { }
  521. while ((req = batch.shift()) != null)
  522. if (Array.isArray(json) && json.length)
  523. req[2].call(reqopt, reply.clone(json.shift()));
  524. else
  525. req[1].call(reqopt, new Error('No related RPC reply'));
  526. }).catch(function(error) {
  527. var req = null;
  528. while ((req = batch.shift()) != null)
  529. req[1].call(reqopt, error);
  530. });
  531. }
  532. /**
  533. * @class request
  534. * @memberof LuCI
  535. * @hideconstructor
  536. * @classdesc
  537. *
  538. * The `Request` class allows initiating HTTP requests and provides utilities
  539. * for dealing with responses.
  540. */
  541. var Request = Class.singleton(/** @lends LuCI.request.prototype */ {
  542. __name__: 'LuCI.request',
  543. interceptors: [],
  544. /**
  545. * Turn the given relative URL into an absolute URL if necessary.
  546. *
  547. * @instance
  548. * @memberof LuCI.request
  549. * @param {string} url
  550. * The URL to convert.
  551. *
  552. * @returns {string}
  553. * The absolute URL derived from the given one, or the original URL
  554. * if it already was absolute.
  555. */
  556. expandURL: function(url) {
  557. if (!/^(?:[^/]+:)?\/\//.test(url))
  558. url = location.protocol + '//' + location.host + url;
  559. return url;
  560. },
  561. /**
  562. * @typedef {Object} RequestOptions
  563. * @memberof LuCI.request
  564. *
  565. * @property {string} [method=GET]
  566. * The HTTP method to use, e.g. `GET` or `POST`.
  567. *
  568. * @property {Object<string, Object|string>} [query]
  569. * Query string data to append to the URL. Non-string values of the
  570. * given object will be converted to JSON.
  571. *
  572. * @property {boolean} [cache=false]
  573. * Specifies whether the HTTP response may be retrieved from cache.
  574. *
  575. * @property {string} [username]
  576. * Provides a username for HTTP basic authentication.
  577. *
  578. * @property {string} [password]
  579. * Provides a password for HTTP basic authentication.
  580. *
  581. * @property {number} [timeout]
  582. * Specifies the request timeout in seconds.
  583. *
  584. * @property {boolean} [credentials=false]
  585. * Whether to include credentials such as cookies in the request.
  586. *
  587. * @property {string} [responseType=text]
  588. * Overrides the request response type. Valid values or `text` to
  589. * interpret the response as UTF-8 string or `blob` to handle the
  590. * response as binary `Blob` data.
  591. *
  592. * @property {*} [content]
  593. * Specifies the HTTP message body to send along with the request.
  594. * If the value is a function, it is invoked and the return value
  595. * used as content, if it is a FormData instance, it is used as-is,
  596. * if it is an object, it will be converted to JSON, in all other
  597. * cases it is converted to a string.
  598. *
  599. * @property {Object<string, string>} [header]
  600. * Specifies HTTP headers to set for the request.
  601. *
  602. * @property {function} [progress]
  603. * An optional request callback function which receives ProgressEvent
  604. * instances as sole argument during the HTTP request transfer.
  605. */
  606. /**
  607. * Initiate an HTTP request to the given target.
  608. *
  609. * @instance
  610. * @memberof LuCI.request
  611. * @param {string} target
  612. * The URL to request.
  613. *
  614. * @param {LuCI.request.RequestOptions} [options]
  615. * Additional options to configure the request.
  616. *
  617. * @returns {Promise<LuCI.response>}
  618. * The resulting HTTP response.
  619. */
  620. request: function(target, options) {
  621. var state = { xhr: new XMLHttpRequest(), url: this.expandURL(target), start: Date.now() },
  622. opt = Object.assign({}, options, state),
  623. content = null,
  624. contenttype = null,
  625. callback = this.handleReadyStateChange;
  626. return new Promise(function(resolveFn, rejectFn) {
  627. opt.xhr.onreadystatechange = callback.bind(opt, resolveFn, rejectFn);
  628. opt.method = String(opt.method || 'GET').toUpperCase();
  629. if ('query' in opt) {
  630. var q = (opt.query != null) ? Object.keys(opt.query).map(function(k) {
  631. if (opt.query[k] != null) {
  632. var v = (typeof(opt.query[k]) == 'object')
  633. ? JSON.stringify(opt.query[k])
  634. : String(opt.query[k]);
  635. return '%s=%s'.format(encodeURIComponent(k), encodeURIComponent(v));
  636. }
  637. else {
  638. return encodeURIComponent(k);
  639. }
  640. }).join('&') : '';
  641. if (q !== '') {
  642. switch (opt.method) {
  643. case 'GET':
  644. case 'HEAD':
  645. case 'OPTIONS':
  646. opt.url += ((/\?/).test(opt.url) ? '&' : '?') + q;
  647. break;
  648. default:
  649. if (content == null) {
  650. content = q;
  651. contenttype = 'application/x-www-form-urlencoded';
  652. }
  653. }
  654. }
  655. }
  656. if (!opt.cache)
  657. opt.url += ((/\?/).test(opt.url) ? '&' : '?') + (new Date()).getTime();
  658. if (isQueueableRequest(opt)) {
  659. requestQueue.push([opt, rejectFn, resolveFn]);
  660. requestAnimationFrame(flushRequestQueue);
  661. return;
  662. }
  663. if ('username' in opt && 'password' in opt)
  664. opt.xhr.open(opt.method, opt.url, true, opt.username, opt.password);
  665. else
  666. opt.xhr.open(opt.method, opt.url, true);
  667. opt.xhr.responseType = opt.responseType || 'text';
  668. if ('overrideMimeType' in opt.xhr)
  669. opt.xhr.overrideMimeType('application/octet-stream');
  670. if ('timeout' in opt)
  671. opt.xhr.timeout = +opt.timeout;
  672. if ('credentials' in opt)
  673. opt.xhr.withCredentials = !!opt.credentials;
  674. if (opt.content != null) {
  675. switch (typeof(opt.content)) {
  676. case 'function':
  677. content = opt.content(xhr);
  678. break;
  679. case 'object':
  680. if (!(opt.content instanceof FormData)) {
  681. content = JSON.stringify(opt.content);
  682. contenttype = 'application/json';
  683. }
  684. else {
  685. content = opt.content;
  686. }
  687. break;
  688. default:
  689. content = String(opt.content);
  690. }
  691. }
  692. if ('headers' in opt)
  693. for (var header in opt.headers)
  694. if (opt.headers.hasOwnProperty(header)) {
  695. if (header.toLowerCase() != 'content-type')
  696. opt.xhr.setRequestHeader(header, opt.headers[header]);
  697. else
  698. contenttype = opt.headers[header];
  699. }
  700. if ('progress' in opt && 'upload' in opt.xhr)
  701. opt.xhr.upload.addEventListener('progress', opt.progress);
  702. if (contenttype != null)
  703. opt.xhr.setRequestHeader('Content-Type', contenttype);
  704. try {
  705. opt.xhr.send(content);
  706. }
  707. catch (e) {
  708. rejectFn.call(opt, e);
  709. }
  710. });
  711. },
  712. handleReadyStateChange: function(resolveFn, rejectFn, ev) {
  713. var xhr = this.xhr,
  714. duration = Date.now() - this.start;
  715. if (xhr.readyState !== 4)
  716. return;
  717. if (xhr.status === 0 && xhr.statusText === '') {
  718. if (duration >= this.timeout)
  719. rejectFn.call(this, new Error('XHR request timed out'));
  720. else
  721. rejectFn.call(this, new Error('XHR request aborted by browser'));
  722. }
  723. else {
  724. var response = new Response(
  725. xhr, xhr.responseURL || this.url, duration);
  726. Promise.all(Request.interceptors.map(function(fn) { return fn(response) }))
  727. .then(resolveFn.bind(this, response))
  728. .catch(rejectFn.bind(this));
  729. }
  730. },
  731. /**
  732. * Initiate an HTTP GET request to the given target.
  733. *
  734. * @instance
  735. * @memberof LuCI.request
  736. * @param {string} target
  737. * The URL to request.
  738. *
  739. * @param {LuCI.request.RequestOptions} [options]
  740. * Additional options to configure the request.
  741. *
  742. * @returns {Promise<LuCI.response>}
  743. * The resulting HTTP response.
  744. */
  745. get: function(url, options) {
  746. return this.request(url, Object.assign({ method: 'GET' }, options));
  747. },
  748. /**
  749. * Initiate an HTTP POST request to the given target.
  750. *
  751. * @instance
  752. * @memberof LuCI.request
  753. * @param {string} target
  754. * The URL to request.
  755. *
  756. * @param {*} [data]
  757. * The request data to send, see {@link LuCI.request.RequestOptions} for details.
  758. *
  759. * @param {LuCI.request.RequestOptions} [options]
  760. * Additional options to configure the request.
  761. *
  762. * @returns {Promise<LuCI.response>}
  763. * The resulting HTTP response.
  764. */
  765. post: function(url, data, options) {
  766. return this.request(url, Object.assign({ method: 'POST', content: data }, options));
  767. },
  768. /**
  769. * Interceptor functions are invoked whenever an HTTP reply is received, in the order
  770. * these functions have been registered.
  771. * @callback LuCI.request.interceptorFn
  772. * @param {LuCI.response} res
  773. * The HTTP response object
  774. */
  775. /**
  776. * Register an HTTP response interceptor function. Interceptor
  777. * functions are useful to perform default actions on incoming HTTP
  778. * responses, such as checking for expired authentication or for
  779. * implementing request retries before returning a failure.
  780. *
  781. * @instance
  782. * @memberof LuCI.request
  783. * @param {LuCI.request.interceptorFn} interceptorFn
  784. * The interceptor function to register.
  785. *
  786. * @returns {LuCI.request.interceptorFn}
  787. * The registered function.
  788. */
  789. addInterceptor: function(interceptorFn) {
  790. if (typeof(interceptorFn) == 'function')
  791. this.interceptors.push(interceptorFn);
  792. return interceptorFn;
  793. },
  794. /**
  795. * Remove an HTTP response interceptor function. The passed function
  796. * value must be the very same value that was used to register the
  797. * function.
  798. *
  799. * @instance
  800. * @memberof LuCI.request
  801. * @param {LuCI.request.interceptorFn} interceptorFn
  802. * The interceptor function to remove.
  803. *
  804. * @returns {boolean}
  805. * Returns `true` if any function has been removed, else `false`.
  806. */
  807. removeInterceptor: function(interceptorFn) {
  808. var oldlen = this.interceptors.length, i = oldlen;
  809. while (i--)
  810. if (this.interceptors[i] === interceptorFn)
  811. this.interceptors.splice(i, 1);
  812. return (this.interceptors.length < oldlen);
  813. },
  814. /**
  815. * @class
  816. * @memberof LuCI.request
  817. * @hideconstructor
  818. * @classdesc
  819. *
  820. * The `Request.poll` class provides some convience wrappers around
  821. * {@link LuCI.poll} mainly to simplify registering repeating HTTP
  822. * request calls as polling functions.
  823. */
  824. poll: {
  825. /**
  826. * The callback function is invoked whenever an HTTP reply to a
  827. * polled request is received or when the polled request timed
  828. * out.
  829. *
  830. * @callback LuCI.request.poll~callbackFn
  831. * @param {LuCI.response} res
  832. * The HTTP response object.
  833. *
  834. * @param {*} data
  835. * The response JSON if the response could be parsed as such,
  836. * else `null`.
  837. *
  838. * @param {number} duration
  839. * The total duration of the request in milliseconds.
  840. */
  841. /**
  842. * Register a repeating HTTP request with an optional callback
  843. * to invoke whenever a response for the request is received.
  844. *
  845. * @instance
  846. * @memberof LuCI.request.poll
  847. * @param {number} interval
  848. * The poll interval in seconds.
  849. *
  850. * @param {string} url
  851. * The URL to request on each poll.
  852. *
  853. * @param {LuCI.request.RequestOptions} [options]
  854. * Additional options to configure the request.
  855. *
  856. * @param {LuCI.request.poll~callbackFn} [callback]
  857. * {@link LuCI.request.poll~callbackFn Callback} function to
  858. * invoke for each HTTP reply.
  859. *
  860. * @throws {TypeError}
  861. * Throws `TypeError` when an invalid interval was passed.
  862. *
  863. * @returns {function}
  864. * Returns the internally created poll function.
  865. */
  866. add: function(interval, url, options, callback) {
  867. if (isNaN(interval) || interval <= 0)
  868. throw new TypeError('Invalid poll interval');
  869. var ival = interval >>> 0,
  870. opts = Object.assign({}, options, { timeout: ival * 1000 - 5 });
  871. var fn = function() {
  872. return Request.request(url, options).then(function(res) {
  873. if (!Poll.active())
  874. return;
  875. var res_json = null;
  876. try {
  877. res_json = res.json();
  878. }
  879. catch (err) {}
  880. callback(res, res_json, res.duration);
  881. });
  882. };
  883. return (Poll.add(fn, ival) ? fn : null);
  884. },
  885. /**
  886. * Remove a polling request that has been previously added using `add()`.
  887. * This function is essentially a wrapper around
  888. * {@link LuCI.poll.remove LuCI.poll.remove()}.
  889. *
  890. * @instance
  891. * @memberof LuCI.request.poll
  892. * @param {function} entry
  893. * The poll function returned by {@link LuCI.request.poll#add add()}.
  894. *
  895. * @returns {boolean}
  896. * Returns `true` if any function has been removed, else `false`.
  897. */
  898. remove: function(entry) { return Poll.remove(entry) },
  899. /**
  900. * Alias for {@link LuCI.poll.start LuCI.poll.start()}.
  901. *
  902. * @instance
  903. * @memberof LuCI.request.poll
  904. */
  905. start: function() { return Poll.start() },
  906. /**
  907. * Alias for {@link LuCI.poll.stop LuCI.poll.stop()}.
  908. *
  909. * @instance
  910. * @memberof LuCI.request.poll
  911. */
  912. stop: function() { return Poll.stop() },
  913. /**
  914. * Alias for {@link LuCI.poll.active LuCI.poll.active()}.
  915. *
  916. * @instance
  917. * @memberof LuCI.request.poll
  918. */
  919. active: function() { return Poll.active() }
  920. }
  921. });
  922. /**
  923. * @class poll
  924. * @memberof LuCI
  925. * @hideconstructor
  926. * @classdesc
  927. *
  928. * The `Poll` class allows registering and unregistering poll actions,
  929. * as well as starting, stopping and querying the state of the polling
  930. * loop.
  931. */
  932. var Poll = Class.singleton(/** @lends LuCI.poll.prototype */ {
  933. __name__: 'LuCI.poll',
  934. queue: [],
  935. /**
  936. * Add a new operation to the polling loop. If the polling loop is not
  937. * already started at this point, it will be implicitely started.
  938. *
  939. * @instance
  940. * @memberof LuCI.poll
  941. * @param {function} fn
  942. * The function to invoke on each poll interval.
  943. *
  944. * @param {number} interval
  945. * The poll interval in seconds.
  946. *
  947. * @throws {TypeError}
  948. * Throws `TypeError` when an invalid interval was passed.
  949. *
  950. * @returns {boolean}
  951. * Returns `true` if the function has been added or `false` if it
  952. * already is registered.
  953. */
  954. add: function(fn, interval) {
  955. if (interval == null || interval <= 0)
  956. interval = env.pollinterval || null;
  957. if (isNaN(interval) || typeof(fn) != 'function')
  958. throw new TypeError('Invalid argument to LuCI.poll.add()');
  959. for (var i = 0; i < this.queue.length; i++)
  960. if (this.queue[i].fn === fn)
  961. return false;
  962. var e = {
  963. r: true,
  964. i: interval >>> 0,
  965. fn: fn
  966. };
  967. this.queue.push(e);
  968. if (this.tick != null && !this.active())
  969. this.start();
  970. return true;
  971. },
  972. /**
  973. * Remove an operation from the polling loop. If no further operatons
  974. * are registered, the polling loop is implicitely stopped.
  975. *
  976. * @instance
  977. * @memberof LuCI.poll
  978. * @param {function} fn
  979. * The function to remove.
  980. *
  981. * @throws {TypeError}
  982. * Throws `TypeError` when the given argument isn't a function.
  983. *
  984. * @returns {boolean}
  985. * Returns `true` if the function has been removed or `false` if it
  986. * wasn't found.
  987. */
  988. remove: function(fn) {
  989. if (typeof(fn) != 'function')
  990. throw new TypeError('Invalid argument to LuCI.poll.remove()');
  991. var len = this.queue.length;
  992. for (var i = len; i > 0; i--)
  993. if (this.queue[i-1].fn === fn)
  994. this.queue.splice(i-1, 1);
  995. if (!this.queue.length && this.stop())
  996. this.tick = 0;
  997. return (this.queue.length != len);
  998. },
  999. /**
  1000. * (Re)start the polling loop. Dispatches a custom `poll-start` event
  1001. * to the `document` object upon successful start.
  1002. *
  1003. * @instance
  1004. * @memberof LuCI.poll
  1005. * @returns {boolean}
  1006. * Returns `true` if polling has been started (or if no functions
  1007. * where registered) or `false` when the polling loop already runs.
  1008. */
  1009. start: function() {
  1010. if (this.active())
  1011. return false;
  1012. this.tick = 0;
  1013. if (this.queue.length) {
  1014. this.timer = window.setInterval(this.step, 1000);
  1015. this.step();
  1016. document.dispatchEvent(new CustomEvent('poll-start'));
  1017. }
  1018. return true;
  1019. },
  1020. /**
  1021. * Stop the polling loop. Dispatches a custom `poll-stop` event
  1022. * to the `document` object upon successful stop.
  1023. *
  1024. * @instance
  1025. * @memberof LuCI.poll
  1026. * @returns {boolean}
  1027. * Returns `true` if polling has been stopped or `false` if it din't
  1028. * run to begin with.
  1029. */
  1030. stop: function() {
  1031. if (!this.active())
  1032. return false;
  1033. document.dispatchEvent(new CustomEvent('poll-stop'));
  1034. window.clearInterval(this.timer);
  1035. delete this.timer;
  1036. delete this.tick;
  1037. return true;
  1038. },
  1039. /* private */
  1040. step: function() {
  1041. for (var i = 0, e = null; (e = Poll.queue[i]) != null; i++) {
  1042. if ((Poll.tick % e.i) != 0)
  1043. continue;
  1044. if (!e.r)
  1045. continue;
  1046. e.r = false;
  1047. Promise.resolve(e.fn()).finally((function() { this.r = true }).bind(e));
  1048. }
  1049. Poll.tick = (Poll.tick + 1) % Math.pow(2, 32);
  1050. },
  1051. /**
  1052. * Test whether the polling loop is running.
  1053. *
  1054. * @instance
  1055. * @memberof LuCI.poll
  1056. * @returns {boolean} - Returns `true` if polling is active, else `false`.
  1057. */
  1058. active: function() {
  1059. return (this.timer != null);
  1060. }
  1061. });
  1062. /**
  1063. * @class dom
  1064. * @memberof LuCI
  1065. * @hideconstructor
  1066. * @classdesc
  1067. *
  1068. * The `dom` class provides convenience method for creating and
  1069. * manipulating DOM elements.
  1070. *
  1071. * To import the class in views, use `'require dom'`, to import it in
  1072. * external JavaScript, use `L.require("dom").then(...)`.
  1073. */
  1074. var DOM = Class.singleton(/** @lends LuCI.dom.prototype */ {
  1075. __name__: 'LuCI.dom',
  1076. /**
  1077. * Tests whether the given argument is a valid DOM `Node`.
  1078. *
  1079. * @instance
  1080. * @memberof LuCI.dom
  1081. * @param {*} e
  1082. * The value to test.
  1083. *
  1084. * @returns {boolean}
  1085. * Returns `true` if the value is a DOM `Node`, else `false`.
  1086. */
  1087. elem: function(e) {
  1088. return (e != null && typeof(e) == 'object' && 'nodeType' in e);
  1089. },
  1090. /**
  1091. * Parses a given string as HTML and returns the first child node.
  1092. *
  1093. * @instance
  1094. * @memberof LuCI.dom
  1095. * @param {string} s
  1096. * A string containing an HTML fragment to parse. Note that only
  1097. * the first result of the resulting structure is returned, so an
  1098. * input value of `<div>foo</div> <div>bar</div>` will only return
  1099. * the first `div` element node.
  1100. *
  1101. * @returns {Node}
  1102. * Returns the first DOM `Node` extracted from the HTML fragment or
  1103. * `null` on parsing failures or if no element could be found.
  1104. */
  1105. parse: function(s) {
  1106. var elem;
  1107. try {
  1108. domParser = domParser || new DOMParser();
  1109. elem = domParser.parseFromString(s, 'text/html').body.firstChild;
  1110. }
  1111. catch(e) {}
  1112. if (!elem) {
  1113. try {
  1114. dummyElem = dummyElem || document.createElement('div');
  1115. dummyElem.innerHTML = s;
  1116. elem = dummyElem.firstChild;
  1117. }
  1118. catch (e) {}
  1119. }
  1120. return elem || null;
  1121. },
  1122. /**
  1123. * Tests whether a given `Node` matches the given query selector.
  1124. *
  1125. * This function is a convenience wrapper around the standard
  1126. * `Node.matches("selector")` function with the added benefit that
  1127. * the `node` argument may be a non-`Node` value, in which case
  1128. * this function simply returns `false`.
  1129. *
  1130. * @instance
  1131. * @memberof LuCI.dom
  1132. * @param {*} node
  1133. * The `Node` argument to test the selector against.
  1134. *
  1135. * @param {string} [selector]
  1136. * The query selector expression to test against the given node.
  1137. *
  1138. * @returns {boolean}
  1139. * Returns `true` if the given node matches the specified selector
  1140. * or `false` when the node argument is no valid DOM `Node` or the
  1141. * selector didn't match.
  1142. */
  1143. matches: function(node, selector) {
  1144. var m = this.elem(node) ? node.matches || node.msMatchesSelector : null;
  1145. return m ? m.call(node, selector) : false;
  1146. },
  1147. /**
  1148. * Returns the closest parent node that matches the given query
  1149. * selector expression.
  1150. *
  1151. * This function is a convenience wrapper around the standard
  1152. * `Node.closest("selector")` function with the added benefit that
  1153. * the `node` argument may be a non-`Node` value, in which case
  1154. * this function simply returns `null`.
  1155. *
  1156. * @instance
  1157. * @memberof LuCI.dom
  1158. * @param {*} node
  1159. * The `Node` argument to find the closest parent for.
  1160. *
  1161. * @param {string} [selector]
  1162. * The query selector expression to test against each parent.
  1163. *
  1164. * @returns {Node|null}
  1165. * Returns the closest parent node matching the selector or
  1166. * `null` when the node argument is no valid DOM `Node` or the
  1167. * selector didn't match any parent.
  1168. */
  1169. parent: function(node, selector) {
  1170. if (this.elem(node) && node.closest)
  1171. return node.closest(selector);
  1172. while (this.elem(node))
  1173. if (this.matches(node, selector))
  1174. return node;
  1175. else
  1176. node = node.parentNode;
  1177. return null;
  1178. },
  1179. /**
  1180. * Appends the given children data to the given node.
  1181. *
  1182. * @instance
  1183. * @memberof LuCI.dom
  1184. * @param {*} node
  1185. * The `Node` argument to append the children to.
  1186. *
  1187. * @param {*} [children]
  1188. * The childrens to append to the given node.
  1189. *
  1190. * When `children` is an array, then each item of the array
  1191. * will be either appended as child element or text node,
  1192. * depending on whether the item is a DOM `Node` instance or
  1193. * some other non-`null` value. Non-`Node`, non-`null` values
  1194. * will be converted to strings first before being passed as
  1195. * argument to `createTextNode()`.
  1196. *
  1197. * When `children` is a function, it will be invoked with
  1198. * the passed `node` argument as sole parameter and the `append`
  1199. * function will be invoked again, with the given `node` argument
  1200. * as first and the return value of the `children` function as
  1201. * second parameter.
  1202. *
  1203. * When `children` is is a DOM `Node` instance, it will be
  1204. * appended to the given `node`.
  1205. *
  1206. * When `children` is any other non-`null` value, it will be
  1207. * converted to a string and appened to the `innerHTML` property
  1208. * of the given `node`.
  1209. *
  1210. * @returns {Node|null}
  1211. * Returns the last children `Node` appended to the node or `null`
  1212. * if either the `node` argument was no valid DOM `node` or if the
  1213. * `children` was `null` or didn't result in further DOM nodes.
  1214. */
  1215. append: function(node, children) {
  1216. if (!this.elem(node))
  1217. return null;
  1218. if (Array.isArray(children)) {
  1219. for (var i = 0; i < children.length; i++)
  1220. if (this.elem(children[i]))
  1221. node.appendChild(children[i]);
  1222. else if (children !== null && children !== undefined)
  1223. node.appendChild(document.createTextNode('' + children[i]));
  1224. return node.lastChild;
  1225. }
  1226. else if (typeof(children) === 'function') {
  1227. return this.append(node, children(node));
  1228. }
  1229. else if (this.elem(children)) {
  1230. return node.appendChild(children);
  1231. }
  1232. else if (children !== null && children !== undefined) {
  1233. node.innerHTML = '' + children;
  1234. return node.lastChild;
  1235. }
  1236. return null;
  1237. },
  1238. /**
  1239. * Replaces the content of the given node with the given children.
  1240. *
  1241. * This function first removes any children of the given DOM
  1242. * `Node` and then adds the given given children following the
  1243. * rules outlined below.
  1244. *
  1245. * @instance
  1246. * @memberof LuCI.dom
  1247. * @param {*} node
  1248. * The `Node` argument to replace the children of.
  1249. *
  1250. * @param {*} [children]
  1251. * The childrens to replace into the given node.
  1252. *
  1253. * When `children` is an array, then each item of the array
  1254. * will be either appended as child element or text node,
  1255. * depending on whether the item is a DOM `Node` instance or
  1256. * some other non-`null` value. Non-`Node`, non-`null` values
  1257. * will be converted to strings first before being passed as
  1258. * argument to `createTextNode()`.
  1259. *
  1260. * When `children` is a function, it will be invoked with
  1261. * the passed `node` argument as sole parameter and the `append`
  1262. * function will be invoked again, with the given `node` argument
  1263. * as first and the return value of the `children` function as
  1264. * second parameter.
  1265. *
  1266. * When `children` is is a DOM `Node` instance, it will be
  1267. * appended to the given `node`.
  1268. *
  1269. * When `children` is any other non-`null` value, it will be
  1270. * converted to a string and appened to the `innerHTML` property
  1271. * of the given `node`.
  1272. *
  1273. * @returns {Node|null}
  1274. * Returns the last children `Node` appended to the node or `null`
  1275. * if either the `node` argument was no valid DOM `node` or if the
  1276. * `children` was `null` or didn't result in further DOM nodes.
  1277. */
  1278. content: function(node, children) {
  1279. if (!this.elem(node))
  1280. return null;
  1281. var dataNodes = node.querySelectorAll('[data-idref]');
  1282. for (var i = 0; i < dataNodes.length; i++)
  1283. delete this.registry[dataNodes[i].getAttribute('data-idref')];
  1284. while (node.firstChild)
  1285. node.removeChild(node.firstChild);
  1286. return this.append(node, children);
  1287. },
  1288. /**
  1289. * Sets attributes or registers event listeners on element nodes.
  1290. *
  1291. * @instance
  1292. * @memberof LuCI.dom
  1293. * @param {*} node
  1294. * The `Node` argument to set the attributes or add the event
  1295. * listeners for. When the given `node` value is not a valid
  1296. * DOM `Node`, the function returns and does nothing.
  1297. *
  1298. * @param {string|Object<string, *>} key
  1299. * Specifies either the attribute or event handler name to use,
  1300. * or an object containing multiple key, value pairs which are
  1301. * each added to the node as either attribute or event handler,
  1302. * depending on the respective value.
  1303. *
  1304. * @param {*} [val]
  1305. * Specifies the attribute value or event handler function to add.
  1306. * If the `key` parameter is an `Object`, this parameter will be
  1307. * ignored.
  1308. *
  1309. * When `val` is of type function, it will be registered as event
  1310. * handler on the given `node` with the `key` parameter being the
  1311. * event name.
  1312. *
  1313. * When `val` is of type object, it will be serialized as JSON and
  1314. * added as attribute to the given `node`, using the given `key`
  1315. * as attribute name.
  1316. *
  1317. * When `val` is of any other type, it will be added as attribute
  1318. * to the given `node` as-is, with the underlying `setAttribute()`
  1319. * call implicitely turning it into a string.
  1320. */
  1321. attr: function(node, key, val) {
  1322. if (!this.elem(node))
  1323. return null;
  1324. var attr = null;
  1325. if (typeof(key) === 'object' && key !== null)
  1326. attr = key;
  1327. else if (typeof(key) === 'string')
  1328. attr = {}, attr[key] = val;
  1329. for (key in attr) {
  1330. if (!attr.hasOwnProperty(key) || attr[key] == null)
  1331. continue;
  1332. switch (typeof(attr[key])) {
  1333. case 'function':
  1334. node.addEventListener(key, attr[key]);
  1335. break;
  1336. case 'object':
  1337. node.setAttribute(key, JSON.stringify(attr[key]));
  1338. break;
  1339. default:
  1340. node.setAttribute(key, attr[key]);
  1341. }
  1342. }
  1343. },
  1344. /**
  1345. * Creates a new DOM `Node` from the given `html`, `attr` and
  1346. * `data` parameters.
  1347. *
  1348. * This function has multiple signatures, it can be either invoked
  1349. * in the form `create(html[, attr[, data]])` or in the form
  1350. * `create(html[, data])`. The used variant is determined from the
  1351. * type of the second argument.
  1352. *
  1353. * @instance
  1354. * @memberof LuCI.dom
  1355. * @param {*} html
  1356. * Describes the node to create.
  1357. *
  1358. * When the value of `html` is of type array, a `DocumentFragment`
  1359. * node is created and each item of the array is first converted
  1360. * to a DOM `Node` by passing it through `create()` and then added
  1361. * as child to the fragment.
  1362. *
  1363. * When the value of `html` is a DOM `Node` instance, no new
  1364. * element will be created but the node will be used as-is.
  1365. *
  1366. * When the value of `html` is a string starting with `<`, it will
  1367. * be passed to `dom.parse()` and the resulting value is used.
  1368. *
  1369. * When the value of `html` is any other string, it will be passed
  1370. * to `document.createElement()` for creating a new DOM `Node` of
  1371. * the given name.
  1372. *
  1373. * @param {Object<string, *>} [attr]
  1374. * Specifies an Object of key, value pairs to set as attributes
  1375. * or event handlers on the created node. Refer to
  1376. * {@link LuCI.dom#attr dom.attr()} for details.
  1377. *
  1378. * @param {*} [data]
  1379. * Specifies children to append to the newly created element.
  1380. * Refer to {@link LuCI.dom#append dom.append()} for details.
  1381. *
  1382. * @throws {InvalidCharacterError}
  1383. * Throws an `InvalidCharacterError` when the given `html`
  1384. * argument contained malformed markup (such as not escaped
  1385. * `&` characters in XHTML mode) or when the given node name
  1386. * in `html` contains characters which are not legal in DOM
  1387. * element names, such as spaces.
  1388. *
  1389. * @returns {Node}
  1390. * Returns the newly created `Node`.
  1391. */
  1392. create: function() {
  1393. var html = arguments[0],
  1394. attr = arguments[1],
  1395. data = arguments[2],
  1396. elem;
  1397. if (!(attr instanceof Object) || Array.isArray(attr))
  1398. data = attr, attr = null;
  1399. if (Array.isArray(html)) {
  1400. elem = document.createDocumentFragment();
  1401. for (var i = 0; i < html.length; i++)
  1402. elem.appendChild(this.create(html[i]));
  1403. }
  1404. else if (this.elem(html)) {
  1405. elem = html;
  1406. }
  1407. else if (html.charCodeAt(0) === 60) {
  1408. elem = this.parse(html);
  1409. }
  1410. else {
  1411. elem = document.createElement(html);
  1412. }
  1413. if (!elem)
  1414. return null;
  1415. this.attr(elem, attr);
  1416. this.append(elem, data);
  1417. return elem;
  1418. },
  1419. registry: {},
  1420. /**
  1421. * Attaches or detaches arbitrary data to and from a DOM `Node`.
  1422. *
  1423. * This function is useful to attach non-string values or runtime
  1424. * data that is not serializable to DOM nodes. To decouple data
  1425. * from the DOM, values are not added directly to nodes, but
  1426. * inserted into a registry instead which is then referenced by a
  1427. * string key stored as `data-idref` attribute in the node.
  1428. *
  1429. * This function has multiple signatures and is sensitive to the
  1430. * number of arguments passed to it.
  1431. *
  1432. * - `dom.data(node)` -
  1433. * Fetches all data associated with the given node.
  1434. * - `dom.data(node, key)` -
  1435. * Fetches a specific key associated with the given node.
  1436. * - `dom.data(node, key, val)` -
  1437. * Sets a specific key to the given value associated with the
  1438. * given node.
  1439. * - `dom.data(node, null)` -
  1440. * Clears any data associated with the node.
  1441. * - `dom.data(node, key, null)` -
  1442. * Clears the given key associated with the node.
  1443. *
  1444. * @instance
  1445. * @memberof LuCI.dom
  1446. * @param {Node} node
  1447. * The DOM `Node` instance to set or retrieve the data for.
  1448. *
  1449. * @param {string|null} [key]
  1450. * This is either a string specifying the key to retrieve, or
  1451. * `null` to unset the entire node data.
  1452. *
  1453. * @param {*|null} [val]
  1454. * This is either a non-`null` value to set for a given key or
  1455. * `null` to remove the given `key` from the specified node.
  1456. *
  1457. * @returns {*}
  1458. * Returns the get or set value, or `null` when no value could
  1459. * be found.
  1460. */
  1461. data: function(node, key, val) {
  1462. if (!node || !node.getAttribute)
  1463. return null;
  1464. var id = node.getAttribute('data-idref');
  1465. /* clear all data */
  1466. if (arguments.length > 1 && key == null) {
  1467. if (id != null) {
  1468. node.removeAttribute('data-idref');
  1469. val = this.registry[id]
  1470. delete this.registry[id];
  1471. return val;
  1472. }
  1473. return null;
  1474. }
  1475. /* clear a key */
  1476. else if (arguments.length > 2 && key != null && val == null) {
  1477. if (id != null) {
  1478. val = this.registry[id][key];
  1479. delete this.registry[id][key];
  1480. return val;
  1481. }
  1482. return null;
  1483. }
  1484. /* set a key */
  1485. else if (arguments.length > 2 && key != null && val != null) {
  1486. if (id == null) {
  1487. do { id = Math.floor(Math.random() * 0xffffffff).toString(16) }
  1488. while (this.registry.hasOwnProperty(id));
  1489. node.setAttribute('data-idref', id);
  1490. this.registry[id] = {};
  1491. }
  1492. return (this.registry[id][key] = val);
  1493. }
  1494. /* get all data */
  1495. else if (arguments.length == 1) {
  1496. if (id != null)
  1497. return this.registry[id];
  1498. return null;
  1499. }
  1500. /* get a key */
  1501. else if (arguments.length == 2) {
  1502. if (id != null)
  1503. return this.registry[id][key];
  1504. }
  1505. return null;
  1506. },
  1507. /**
  1508. * Binds the given class instance ot the specified DOM `Node`.
  1509. *
  1510. * This function uses the `dom.data()` facility to attach the
  1511. * passed instance of a Class to a node. This is needed for
  1512. * complex widget elements or similar where the corresponding
  1513. * class instance responsible for the element must be retrieved
  1514. * from DOM nodes obtained by `querySelector()` or similar means.
  1515. *
  1516. * @instance
  1517. * @memberof LuCI.dom
  1518. * @param {Node} node
  1519. * The DOM `Node` instance to bind the class to.
  1520. *
  1521. * @param {Class} inst
  1522. * The Class instance to bind to the node.
  1523. *
  1524. * @throws {TypeError}
  1525. * Throws a `TypeError` when the given instance argument isn't
  1526. * a valid Class instance.
  1527. *
  1528. * @returns {Class}
  1529. * Returns the bound class instance.
  1530. */
  1531. bindClassInstance: function(node, inst) {
  1532. if (!(inst instanceof Class))
  1533. LuCI.prototype.error('TypeError', 'Argument must be a class instance');
  1534. return this.data(node, '_class', inst);
  1535. },
  1536. /**
  1537. * Finds a bound class instance on the given node itself or the
  1538. * first bound instance on its closest parent node.
  1539. *
  1540. * @instance
  1541. * @memberof LuCI.dom
  1542. * @param {Node} node
  1543. * The DOM `Node` instance to start from.
  1544. *
  1545. * @returns {Class|null}
  1546. * Returns the founds class instance if any or `null` if no bound
  1547. * class could be found on the node itself or any of its parents.
  1548. */
  1549. findClassInstance: function(node) {
  1550. var inst = null;
  1551. do {
  1552. inst = this.data(node, '_class');
  1553. node = node.parentNode;
  1554. }
  1555. while (!(inst instanceof Class) && node != null);
  1556. return inst;
  1557. },
  1558. /**
  1559. * Finds a bound class instance on the given node itself or the
  1560. * first bound instance on its closest parent node and invokes
  1561. * the specified method name on the found class instance.
  1562. *
  1563. * @instance
  1564. * @memberof LuCI.dom
  1565. * @param {Node} node
  1566. * The DOM `Node` instance to start from.
  1567. *
  1568. * @param {string} method
  1569. * The name of the method to invoke on the found class instance.
  1570. *
  1571. * @param {...*} params
  1572. * Additional arguments to pass to the invoked method as-is.
  1573. *
  1574. * @returns {*|null}
  1575. * Returns the return value of the invoked method if a class
  1576. * instance and method has been found. Returns `null` if either
  1577. * no bound class instance could be found, or if the found
  1578. * instance didn't have the requested `method`.
  1579. */
  1580. callClassMethod: function(node, method /*, ... */) {
  1581. var inst = this.findClassInstance(node);
  1582. if (inst == null || typeof(inst[method]) != 'function')
  1583. return null;
  1584. return inst[method].apply(inst, inst.varargs(arguments, 2));
  1585. },
  1586. /**
  1587. * The ignore callback function is invoked by `isEmpty()` for each
  1588. * child node to decide whether to ignore a child node or not.
  1589. *
  1590. * When this function returns `false`, the node passed to it is
  1591. * ignored, else not.
  1592. *
  1593. * @callback LuCI.dom~ignoreCallbackFn
  1594. * @param {Node} node
  1595. * The child node to test.
  1596. *
  1597. * @returns {boolean}
  1598. * Boolean indicating whether to ignore the node or not.
  1599. */
  1600. /**
  1601. * Tests whether a given DOM `Node` instance is empty or appears
  1602. * empty.
  1603. *
  1604. * Any element child nodes which have the CSS class `hidden` set
  1605. * or for which the optionally passed `ignoreFn` callback function
  1606. * returns `false` are ignored.
  1607. *
  1608. * @instance
  1609. * @memberof LuCI.dom
  1610. * @param {Node} node
  1611. * The DOM `Node` instance to test.
  1612. *
  1613. * @param {LuCI.dom~ignoreCallbackFn} [ignoreFn]
  1614. * Specifies an optional function which is invoked for each child
  1615. * node to decide whether the child node should be ignored or not.
  1616. *
  1617. * @returns {boolean}
  1618. * Returns `true` if the node does not have any children or if
  1619. * any children node either has a `hidden` CSS class or a `false`
  1620. * result when testing it using the given `ignoreFn`.
  1621. */
  1622. isEmpty: function(node, ignoreFn) {
  1623. for (var child = node.firstElementChild; child != null; child = child.nextElementSibling)
  1624. if (!child.classList.contains('hidden') && (!ignoreFn || !ignoreFn(child)))
  1625. return false;
  1626. return true;
  1627. }
  1628. });
  1629. /**
  1630. * @class session
  1631. * @memberof LuCI
  1632. * @hideconstructor
  1633. * @classdesc
  1634. *
  1635. * The `session` class provides various session related functionality.
  1636. */
  1637. var Session = Class.singleton(/** @lends LuCI.session.prototype */ {
  1638. __name__: 'LuCI.session',
  1639. /**
  1640. * Retrieve the current session ID.
  1641. *
  1642. * @returns {string}
  1643. * Returns the current session ID.
  1644. */
  1645. getID: function() {
  1646. return env.sessionid || '00000000000000000000000000000000';
  1647. },
  1648. /**
  1649. * Retrieve the current session token.
  1650. *
  1651. * @returns {string|null}
  1652. * Returns the current session token or `null` if not logged in.
  1653. */
  1654. getToken: function() {
  1655. return env.token || null;
  1656. },
  1657. /**
  1658. * Retrieve data from the local session storage.
  1659. *
  1660. * @param {string} [key]
  1661. * The key to retrieve from the session data store. If omitted, all
  1662. * session data will be returned.
  1663. *
  1664. * @returns {*}
  1665. * Returns the stored session data or `null` if the given key wasn't
  1666. * found.
  1667. */
  1668. getLocalData: function(key) {
  1669. try {
  1670. var sid = this.getID(),
  1671. item = 'luci-session-store',
  1672. data = JSON.parse(window.sessionStorage.getItem(item));
  1673. if (!LuCI.prototype.isObject(data) || !data.hasOwnProperty(sid)) {
  1674. data = {};
  1675. data[sid] = {};
  1676. }
  1677. if (key != null)
  1678. return data[sid].hasOwnProperty(key) ? data[sid][key] : null;
  1679. return data[sid];
  1680. }
  1681. catch (e) {
  1682. return (key != null) ? null : {};
  1683. }
  1684. },
  1685. /**
  1686. * Set data in the local session storage.
  1687. *
  1688. * @param {string} key
  1689. * The key to set in the session data store.
  1690. *
  1691. * @param {*} value
  1692. * The value to store. It will be internally converted to JSON before
  1693. * being put in the session store.
  1694. *
  1695. * @returns {boolean}
  1696. * Returns `true` if the data could be stored or `false` on error.
  1697. */
  1698. setLocalData: function(key, value) {
  1699. if (key == null)
  1700. return false;
  1701. try {
  1702. var sid = this.getID(),
  1703. item = 'luci-session-store',
  1704. data = JSON.parse(window.sessionStorage.getItem(item));
  1705. if (!LuCI.prototype.isObject(data) || !data.hasOwnProperty(sid)) {
  1706. data = {};
  1707. data[sid] = {};
  1708. }
  1709. if (value != null)
  1710. data[sid][key] = value;
  1711. else
  1712. delete data[sid][key];
  1713. window.sessionStorage.setItem(item, JSON.stringify(data));
  1714. return true;
  1715. }
  1716. catch (e) {
  1717. return false;
  1718. }
  1719. }
  1720. });
  1721. /**
  1722. * @class view
  1723. * @memberof LuCI
  1724. * @hideconstructor
  1725. * @classdesc
  1726. *
  1727. * The `view` class forms the basis of views and provides a standard
  1728. * set of methods to inherit from.
  1729. */
  1730. var View = Class.extend(/** @lends LuCI.view.prototype */ {
  1731. __name__: 'LuCI.view',
  1732. __init__: function() {
  1733. var vp = document.getElementById('view');
  1734. DOM.content(vp, E('div', { 'class': 'spinning' }, _('Loading view…')));
  1735. return Promise.resolve(this.load())
  1736. .then(LuCI.prototype.bind(this.render, this))
  1737. .then(LuCI.prototype.bind(function(nodes) {
  1738. var vp = document.getElementById('view');
  1739. DOM.content(vp, nodes);
  1740. DOM.append(vp, this.addFooter());
  1741. }, this)).catch(LuCI.prototype.error);
  1742. },
  1743. /**
  1744. * The load function is invoked before the view is rendered.
  1745. *
  1746. * The invocation of this function is wrapped by
  1747. * `Promise.resolve()` so it may return Promises if needed.
  1748. *
  1749. * The return value of the function (or the resolved values
  1750. * of the promise returned by it) will be passed as first
  1751. * argument to `render()`.
  1752. *
  1753. * This function is supposed to be overwritten by subclasses,
  1754. * the default implementation does nothing.
  1755. *
  1756. * @instance
  1757. * @abstract
  1758. * @memberof LuCI.view
  1759. *
  1760. * @returns {*|Promise<*>}
  1761. * May return any value or a Promise resolving to any value.
  1762. */
  1763. load: function() {},
  1764. /**
  1765. * The render function is invoked after the
  1766. * {@link LuCI.view#load load()} function and responsible
  1767. * for setting up the view contents. It must return a DOM
  1768. * `Node` or `DocumentFragment` holding the contents to
  1769. * insert into the view area.
  1770. *
  1771. * The invocation of this function is wrapped by
  1772. * `Promise.resolve()` so it may return Promises if needed.
  1773. *
  1774. * The return value of the function (or the resolved values
  1775. * of the promise returned by it) will be inserted into the
  1776. * main content area using
  1777. * {@link LuCI.dom#append dom.append()}.
  1778. *
  1779. * This function is supposed to be overwritten by subclasses,
  1780. * the default implementation does nothing.
  1781. *
  1782. * @instance
  1783. * @abstract
  1784. * @memberof LuCI.view
  1785. * @param {*|null} load_results
  1786. * This function will receive the return value of the
  1787. * {@link LuCI.view#load view.load()} function as first
  1788. * argument.
  1789. *
  1790. * @returns {Node|Promise<Node>}
  1791. * Should return a DOM `Node` value or a `Promise` resolving
  1792. * to a `Node` value.
  1793. */
  1794. render: function() {},
  1795. /**
  1796. * The handleSave function is invoked when the user clicks
  1797. * the `Save` button in the page action footer.
  1798. *
  1799. * The default implementation should be sufficient for most
  1800. * views using {@link form#Map form.Map()} based forms - it
  1801. * will iterate all forms present in the view and invoke
  1802. * the {@link form#Map#save Map.save()} method on each form.
  1803. *
  1804. * Views not using `Map` instances or requiring other special
  1805. * logic should overwrite `handleSave()` with a custom
  1806. * implementation.
  1807. *
  1808. * To disable the `Save` page footer button, views extending
  1809. * this base class should overwrite the `handleSave` function
  1810. * with `null`.
  1811. *
  1812. * The invocation of this function is wrapped by
  1813. * `Promise.resolve()` so it may return Promises if needed.
  1814. *
  1815. * @instance
  1816. * @memberof LuCI.view
  1817. * @param {Event} ev
  1818. * The DOM event that triggered the function.
  1819. *
  1820. * @returns {*|Promise<*>}
  1821. * Any return values of this function are discarded, but
  1822. * passed through `Promise.resolve()` to ensure that any
  1823. * returned promise runs to completion before the button
  1824. * is reenabled.
  1825. */
  1826. handleSave: function(ev) {
  1827. var tasks = [];
  1828. document.getElementById('maincontent')
  1829. .querySelectorAll('.cbi-map').forEach(function(map) {
  1830. tasks.push(DOM.callClassMethod(map, 'save'));
  1831. });
  1832. return Promise.all(tasks);
  1833. },
  1834. /**
  1835. * The handleSaveApply function is invoked when the user clicks
  1836. * the `Save & Apply` button in the page action footer.
  1837. *
  1838. * The default implementation should be sufficient for most
  1839. * views using {@link form#Map form.Map()} based forms - it
  1840. * will first invoke
  1841. * {@link LuCI.view.handleSave view.handleSave()} and then
  1842. * call {@link ui#changes#apply ui.changes.apply()} to start the
  1843. * modal config apply and page reload flow.
  1844. *
  1845. * Views not using `Map` instances or requiring other special
  1846. * logic should overwrite `handleSaveApply()` with a custom
  1847. * implementation.
  1848. *
  1849. * To disable the `Save & Apply` page footer button, views
  1850. * extending this base class should overwrite the
  1851. * `handleSaveApply` function with `null`.
  1852. *
  1853. * The invocation of this function is wrapped by
  1854. * `Promise.resolve()` so it may return Promises if needed.
  1855. *
  1856. * @instance
  1857. * @memberof LuCI.view
  1858. * @param {Event} ev
  1859. * The DOM event that triggered the function.
  1860. *
  1861. * @returns {*|Promise<*>}
  1862. * Any return values of this function are discarded, but
  1863. * passed through `Promise.resolve()` to ensure that any
  1864. * returned promise runs to completion before the button
  1865. * is reenabled.
  1866. */
  1867. handleSaveApply: function(ev, mode) {
  1868. return this.handleSave(ev).then(function() {
  1869. classes.ui.changes.apply(mode == '0');
  1870. });
  1871. },
  1872. /**
  1873. * The handleReset function is invoked when the user clicks
  1874. * the `Reset` button in the page action footer.
  1875. *
  1876. * The default implementation should be sufficient for most
  1877. * views using {@link form#Map form.Map()} based forms - it
  1878. * will iterate all forms present in the view and invoke
  1879. * the {@link form#Map#save Map.reset()} method on each form.
  1880. *
  1881. * Views not using `Map` instances or requiring other special
  1882. * logic should overwrite `handleReset()` with a custom
  1883. * implementation.
  1884. *
  1885. * To disable the `Reset` page footer button, views extending
  1886. * this base class should overwrite the `handleReset` function
  1887. * with `null`.
  1888. *
  1889. * The invocation of this function is wrapped by
  1890. * `Promise.resolve()` so it may return Promises if needed.
  1891. *
  1892. * @instance
  1893. * @memberof LuCI.view
  1894. * @param {Event} ev
  1895. * The DOM event that triggered the function.
  1896. *
  1897. * @returns {*|Promise<*>}
  1898. * Any return values of this function are discarded, but
  1899. * passed through `Promise.resolve()` to ensure that any
  1900. * returned promise runs to completion before the button
  1901. * is reenabled.
  1902. */
  1903. handleReset: function(ev) {
  1904. var tasks = [];
  1905. document.getElementById('maincontent')
  1906. .querySelectorAll('.cbi-map').forEach(function(map) {
  1907. tasks.push(DOM.callClassMethod(map, 'reset'));
  1908. });
  1909. return Promise.all(tasks);
  1910. },
  1911. /**
  1912. * Renders a standard page action footer if any of the
  1913. * `handleSave()`, `handleSaveApply()` or `handleReset()`
  1914. * functions are defined.
  1915. *
  1916. * The default implementation should be sufficient for most
  1917. * views - it will render a standard page footer with action
  1918. * buttons labeled `Save`, `Save & Apply` and `Reset`
  1919. * triggering the `handleSave()`, `handleSaveApply()` and
  1920. * `handleReset()` functions respectively.
  1921. *
  1922. * When any of these `handle*()` functions is overwritten
  1923. * with `null` by a view extending this class, the
  1924. * corresponding button will not be rendered.
  1925. *
  1926. * @instance
  1927. * @memberof LuCI.view
  1928. * @returns {DocumentFragment}
  1929. * Returns a `DocumentFragment` containing the footer bar
  1930. * with buttons for each corresponding `handle*()` action
  1931. * or an empty `DocumentFragment` if all three `handle*()`
  1932. * methods are overwritten with `null`.
  1933. */
  1934. addFooter: function() {
  1935. var footer = E([]),
  1936. vp = document.getElementById('view'),
  1937. hasmap = false,
  1938. readonly = true;
  1939. vp.querySelectorAll('.cbi-map').forEach(function(map) {
  1940. var m = DOM.findClassInstance(map);
  1941. if (m) {
  1942. hasmap = true;
  1943. if (!m.readonly)
  1944. readonly = false;
  1945. }
  1946. });
  1947. if (!hasmap)
  1948. readonly = !LuCI.prototype.hasViewPermission();
  1949. var saveApplyBtn = this.handleSaveApply ? new classes.ui.ComboButton('0', {
  1950. 0: [ _('Save & Apply') ],
  1951. 1: [ _('Apply unchecked') ]
  1952. }, {
  1953. classes: {
  1954. 0: 'btn cbi-button cbi-button-apply important',
  1955. 1: 'btn cbi-button cbi-button-negative important'
  1956. },
  1957. click: classes.ui.createHandlerFn(this, 'handleSaveApply'),
  1958. disabled: readonly || null
  1959. }).render() : E([]);
  1960. if (this.handleSaveApply || this.handleSave || this.handleReset) {
  1961. footer.appendChild(E('div', { 'class': 'cbi-page-actions control-group' }, [
  1962. saveApplyBtn, ' ',
  1963. this.handleSave ? E('button', {
  1964. 'class': 'cbi-button cbi-button-save',
  1965. 'click': classes.ui.createHandlerFn(this, 'handleSave'),
  1966. 'disabled': readonly || null
  1967. }, [ _('Save') ]) : '', ' ',
  1968. this.handleReset ? E('button', {
  1969. 'class': 'cbi-button cbi-button-reset',
  1970. 'click': classes.ui.createHandlerFn(this, 'handleReset'),
  1971. 'disabled': readonly || null
  1972. }, [ _('Reset') ]) : ''
  1973. ]));
  1974. }
  1975. return footer;
  1976. }
  1977. });
  1978. var dummyElem = null,
  1979. domParser = null,
  1980. originalCBIInit = null,
  1981. rpcBaseURL = null,
  1982. sysFeatures = null,
  1983. preloadClasses = null;
  1984. /* "preload" builtin classes to make the available via require */
  1985. var classes = {
  1986. baseclass: Class,
  1987. dom: DOM,
  1988. poll: Poll,
  1989. request: Request,
  1990. session: Session,
  1991. view: View
  1992. };
  1993. var LuCI = Class.extend(/** @lends LuCI.prototype */ {
  1994. __name__: 'LuCI',
  1995. __init__: function(setenv) {
  1996. document.querySelectorAll('script[src*="/luci.js"]').forEach(function(s) {
  1997. if (setenv.base_url == null || setenv.base_url == '') {
  1998. var m = (s.getAttribute('src') || '').match(/^(.*)\/luci\.js(?:\?v=([^?]+))?$/);
  1999. if (m) {
  2000. setenv.base_url = m[1];
  2001. setenv.resource_version = m[2];
  2002. }
  2003. }
  2004. });
  2005. if (setenv.base_url == null)
  2006. this.error('InternalError', 'Cannot find url of luci.js');
  2007. setenv.cgi_base = setenv.scriptname.replace(/\/[^\/]+$/, '');
  2008. Object.assign(env, setenv);
  2009. var domReady = new Promise(function(resolveFn, rejectFn) {
  2010. document.addEventListener('DOMContentLoaded', resolveFn);
  2011. });
  2012. Promise.all([
  2013. domReady,
  2014. this.require('ui'),
  2015. this.require('rpc'),
  2016. this.require('form'),
  2017. this.probeRPCBaseURL()
  2018. ]).then(this.setupDOM.bind(this)).catch(this.error);
  2019. originalCBIInit = window.cbi_init;
  2020. window.cbi_init = function() {};
  2021. },
  2022. /**
  2023. * Captures the current stack trace and throws an error of the
  2024. * specified type as a new exception. Also logs the exception as
  2025. * error to the debug console if it is available.
  2026. *
  2027. * @instance
  2028. * @memberof LuCI
  2029. *
  2030. * @param {Error|string} [type=Error]
  2031. * Either a string specifying the type of the error to throw or an
  2032. * existing `Error` instance to copy.
  2033. *
  2034. * @param {string} [fmt=Unspecified error]
  2035. * A format string which is used to form the error message, together
  2036. * with all subsequent optional arguments.
  2037. *
  2038. * @param {...*} [args]
  2039. * Zero or more variable arguments to the supplied format string.
  2040. *
  2041. * @throws {Error}
  2042. * Throws the created error object with the captured stack trace
  2043. * appended to the message and the type set to the given type
  2044. * argument or copied from the given error instance.
  2045. */
  2046. raise: function(type, fmt /*, ...*/) {
  2047. var e = null,
  2048. msg = fmt ? String.prototype.format.apply(fmt, this.varargs(arguments, 2)) : null,
  2049. stack = null;
  2050. if (type instanceof Error) {
  2051. e = type;
  2052. if (msg)
  2053. e.message = msg + ': ' + e.message;
  2054. }
  2055. else {
  2056. try { throw new Error('stacktrace') }
  2057. catch (e2) { stack = (e2.stack || '').split(/\n/) }
  2058. e = new (window[type || 'Error'] || Error)(msg || 'Unspecified error');
  2059. e.name = type || 'Error';
  2060. }
  2061. stack = (stack || []).map(function(frame) {
  2062. frame = frame.replace(/(.*?)@(.+):(\d+):(\d+)/g, 'at $1 ($2:$3:$4)').trim();
  2063. return frame ? ' ' + frame : '';
  2064. });
  2065. if (!/^ at /.test(stack[0]))
  2066. stack.shift();
  2067. if (/\braise /.test(stack[0]))
  2068. stack.shift();
  2069. if (/\berror /.test(stack[0]))
  2070. stack.shift();
  2071. if (stack.length)
  2072. e.message += '\n' + stack.join('\n');
  2073. if (window.console && console.debug)
  2074. console.debug(e);
  2075. throw e;
  2076. },
  2077. /**
  2078. * A wrapper around {@link LuCI#raise raise()} which also renders
  2079. * the error either as modal overlay when `ui.js` is already loaed
  2080. * or directly into the view body.
  2081. *
  2082. * @instance
  2083. * @memberof LuCI
  2084. *
  2085. * @param {Error|string} [type=Error]
  2086. * Either a string specifying the type of the error to throw or an
  2087. * existing `Error` instance to copy.
  2088. *
  2089. * @param {string} [fmt=Unspecified error]
  2090. * A format string which is used to form the error message, together
  2091. * with all subsequent optional arguments.
  2092. *
  2093. * @param {...*} [args]
  2094. * Zero or more variable arguments to the supplied format string.
  2095. *
  2096. * @throws {Error}
  2097. * Throws the created error object with the captured stack trace
  2098. * appended to the message and the type set to the given type
  2099. * argument or copied from the given error instance.
  2100. */
  2101. error: function(type, fmt /*, ...*/) {
  2102. try {
  2103. LuCI.prototype.raise.apply(LuCI.prototype,
  2104. Array.prototype.slice.call(arguments));
  2105. }
  2106. catch (e) {
  2107. if (!e.reported) {
  2108. if (classes.ui)
  2109. classes.ui.addNotification(e.name || _('Runtime error'),
  2110. E('pre', {}, e.message), 'danger');
  2111. else
  2112. DOM.content(document.querySelector('#maincontent'),
  2113. E('pre', { 'class': 'alert-message error' }, e.message));
  2114. e.reported = true;
  2115. }
  2116. throw e;
  2117. }
  2118. },
  2119. /**
  2120. * Return a bound function using the given `self` as `this` context
  2121. * and any further arguments as parameters to the bound function.
  2122. *
  2123. * @instance
  2124. * @memberof LuCI
  2125. *
  2126. * @param {function} fn
  2127. * The function to bind.
  2128. *
  2129. * @param {*} self
  2130. * The value to bind as `this` context to the specified function.
  2131. *
  2132. * @param {...*} [args]
  2133. * Zero or more variable arguments which are bound to the function
  2134. * as parameters.
  2135. *
  2136. * @returns {function}
  2137. * Returns the bound function.
  2138. */
  2139. bind: function(fn, self /*, ... */) {
  2140. return Function.prototype.bind.apply(fn, this.varargs(arguments, 2, self));
  2141. },
  2142. /**
  2143. * Load an additional LuCI JavaScript class and its dependencies,
  2144. * instantiate it and return the resulting class instance. Each
  2145. * class is only loaded once. Subsequent attempts to load the same
  2146. * class will return the already instantiated class.
  2147. *
  2148. * @instance
  2149. * @memberof LuCI
  2150. *
  2151. * @param {string} name
  2152. * The name of the class to load in dotted notation. Dots will
  2153. * be replaced by spaces and joined with the runtime-determined
  2154. * base URL of LuCI.js to form an absolute URL to load the class
  2155. * file from.
  2156. *
  2157. * @throws {DependencyError}
  2158. * Throws a `DependencyError` when the class to load includes
  2159. * circular dependencies.
  2160. *
  2161. * @throws {NetworkError}
  2162. * Throws `NetworkError` when the underlying {@link LuCI.request}
  2163. * call failed.
  2164. *
  2165. * @throws {SyntaxError}
  2166. * Throws `SyntaxError` when the loaded class file code cannot
  2167. * be interpreted by `eval`.
  2168. *
  2169. * @throws {TypeError}
  2170. * Throws `TypeError` when the class file could be loaded and
  2171. * interpreted, but when invoking its code did not yield a valid
  2172. * class instance.
  2173. *
  2174. * @returns {Promise<LuCI.baseclass>}
  2175. * Returns the instantiated class.
  2176. */
  2177. require: function(name, from) {
  2178. var L = this, url = null, from = from || [];
  2179. /* Class already loaded */
  2180. if (classes[name] != null) {
  2181. /* Circular dependency */
  2182. if (from.indexOf(name) != -1)
  2183. LuCI.prototype.raise('DependencyError',
  2184. 'Circular dependency: class "%s" depends on "%s"',
  2185. name, from.join('" which depends on "'));
  2186. return Promise.resolve(classes[name]);
  2187. }
  2188. url = '%s/%s.js%s'.format(env.base_url, name.replace(/\./g, '/'), (env.resource_version ? '?v=' + env.resource_version : ''));
  2189. from = [ name ].concat(from);
  2190. var compileClass = function(res) {
  2191. if (!res.ok)
  2192. LuCI.prototype.raise('NetworkError',
  2193. 'HTTP error %d while loading class file "%s"', res.status, url);
  2194. var source = res.text(),
  2195. requirematch = /^require[ \t]+(\S+)(?:[ \t]+as[ \t]+([a-zA-Z_]\S*))?$/,
  2196. strictmatch = /^use[ \t]+strict$/,
  2197. depends = [],
  2198. args = '';
  2199. /* find require statements in source */
  2200. for (var i = 0, off = -1, prev = -1, quote = -1, comment = -1, esc = false; i < source.length; i++) {
  2201. var chr = source.charCodeAt(i);
  2202. if (esc) {
  2203. esc = false;
  2204. }
  2205. else if (comment != -1) {
  2206. if ((comment == 47 && chr == 10) || (comment == 42 && prev == 42 && chr == 47))
  2207. comment = -1;
  2208. }
  2209. else if ((chr == 42 || chr == 47) && prev == 47) {
  2210. comment = chr;
  2211. }
  2212. else if (chr == 92) {
  2213. esc = true;
  2214. }
  2215. else if (chr == quote) {
  2216. var s = source.substring(off, i),
  2217. m = requirematch.exec(s);
  2218. if (m) {
  2219. var dep = m[1], as = m[2] || dep.replace(/[^a-zA-Z0-9_]/g, '_');
  2220. depends.push(LuCI.prototype.require(dep, from));
  2221. args += ', ' + as;
  2222. }
  2223. else if (!strictmatch.exec(s)) {
  2224. break;
  2225. }
  2226. off = -1;
  2227. quote = -1;
  2228. }
  2229. else if (quote == -1 && (chr == 34 || chr == 39)) {
  2230. off = i + 1;
  2231. quote = chr;
  2232. }
  2233. prev = chr;
  2234. }
  2235. /* load dependencies and instantiate class */
  2236. return Promise.all(depends).then(function(instances) {
  2237. var _factory, _class;
  2238. try {
  2239. _factory = eval(
  2240. '(function(window, document, L%s) { %s })\n\n//# sourceURL=%s\n'
  2241. .format(args, source, res.url));
  2242. }
  2243. catch (error) {
  2244. LuCI.prototype.raise('SyntaxError', '%s\n in %s:%s',
  2245. error.message, res.url, error.lineNumber || '?');
  2246. }
  2247. _factory.displayName = toCamelCase(name + 'ClassFactory');
  2248. _class = _factory.apply(_factory, [window, document, L].concat(instances));
  2249. if (!Class.isSubclass(_class))
  2250. LuCI.prototype.error('TypeError', '"%s" factory yields invalid constructor', name);
  2251. if (_class.displayName == 'AnonymousClass')
  2252. _class.displayName = toCamelCase(name + 'Class');
  2253. var ptr = Object.getPrototypeOf(L),
  2254. parts = name.split(/\./),
  2255. instance = new _class();
  2256. for (var i = 0; ptr && i < parts.length - 1; i++)
  2257. ptr = ptr[parts[i]];
  2258. if (ptr)
  2259. ptr[parts[i]] = instance;
  2260. classes[name] = instance;
  2261. return instance;
  2262. });
  2263. };
  2264. /* Request class file */
  2265. classes[name] = Request.get(url, { cache: true }).then(compileClass);
  2266. return classes[name];
  2267. },
  2268. /* DOM setup */
  2269. probeRPCBaseURL: function() {
  2270. if (rpcBaseURL == null)
  2271. rpcBaseURL = Session.getLocalData('rpcBaseURL');
  2272. if (rpcBaseURL == null) {
  2273. var msg = {
  2274. jsonrpc: '2.0',
  2275. id: 'init',
  2276. method: 'list',
  2277. params: undefined
  2278. };
  2279. var rpcFallbackURL = this.url('admin/ubus');
  2280. rpcBaseURL = Request.post(env.ubuspath, msg, { nobatch: true }).then(function(res) {
  2281. return (rpcBaseURL = res.status == 200 ? env.ubuspath : rpcFallbackURL);
  2282. }, function() {
  2283. return (rpcBaseURL = rpcFallbackURL);
  2284. }).then(function(url) {
  2285. Session.setLocalData('rpcBaseURL', url);
  2286. return url;
  2287. });
  2288. }
  2289. return Promise.resolve(rpcBaseURL);
  2290. },
  2291. probeSystemFeatures: function() {
  2292. if (sysFeatures == null)
  2293. sysFeatures = Session.getLocalData('features');
  2294. if (!this.isObject(sysFeatures)) {
  2295. sysFeatures = classes.rpc.declare({
  2296. object: 'luci',
  2297. method: 'getFeatures',
  2298. expect: { '': {} }
  2299. })().then(function(features) {
  2300. Session.setLocalData('features', features);
  2301. sysFeatures = features;
  2302. return features;
  2303. });
  2304. }
  2305. return Promise.resolve(sysFeatures);
  2306. },
  2307. probePreloadClasses: function() {
  2308. if (preloadClasses == null)
  2309. preloadClasses = Session.getLocalData('preload');
  2310. if (!Array.isArray(preloadClasses)) {
  2311. preloadClasses = this.resolveDefault(classes.rpc.declare({
  2312. object: 'file',
  2313. method: 'list',
  2314. params: [ 'path' ],
  2315. expect: { 'entries': [] }
  2316. })(this.fspath(this.resource('preload'))), []).then(function(entries) {
  2317. var classes = [];
  2318. for (var i = 0; i < entries.length; i++) {
  2319. if (entries[i].type != 'file')
  2320. continue;
  2321. var m = entries[i].name.match(/(.+)\.js$/);
  2322. if (m)
  2323. classes.push('preload.%s'.format(m[1]));
  2324. }
  2325. Session.setLocalData('preload', classes);
  2326. preloadClasses = classes;
  2327. return classes;
  2328. });
  2329. }
  2330. return Promise.resolve(preloadClasses);
  2331. },
  2332. /**
  2333. * Test whether a particular system feature is available, such as
  2334. * hostapd SAE support or an installed firewall. The features are
  2335. * queried once at the beginning of the LuCI session and cached in
  2336. * `SessionStorage` throughout the lifetime of the associated tab or
  2337. * browser window.
  2338. *
  2339. * @instance
  2340. * @memberof LuCI
  2341. *
  2342. * @param {string} feature
  2343. * The feature to test. For detailed list of known feature flags,
  2344. * see `/modules/luci-base/root/usr/libexec/rpcd/luci`.
  2345. *
  2346. * @param {string} [subfeature]
  2347. * Some feature classes like `hostapd` provide sub-feature flags,
  2348. * such as `sae` or `11w` support. The `subfeature` argument can
  2349. * be used to query these.
  2350. *
  2351. * @return {boolean|null}
  2352. * Return `true` if the queried feature (and sub-feature) is available
  2353. * or `false` if the requested feature isn't present or known.
  2354. * Return `null` when a sub-feature was queried for a feature which
  2355. * has no sub-features.
  2356. */
  2357. hasSystemFeature: function() {
  2358. var ft = sysFeatures[arguments[0]];
  2359. if (arguments.length == 2)
  2360. return this.isObject(ft) ? ft[arguments[1]] : null;
  2361. return (ft != null && ft != false);
  2362. },
  2363. /* private */
  2364. notifySessionExpiry: function() {
  2365. Poll.stop();
  2366. classes.ui.showModal(_('Session expired'), [
  2367. E('div', { class: 'alert-message warning' },
  2368. _('A new login is required since the authentication session expired.')),
  2369. E('div', { class: 'right' },
  2370. E('div', {
  2371. class: 'btn primary',
  2372. click: function() {
  2373. var loc = window.location;
  2374. window.location = loc.protocol + '//' + loc.host + loc.pathname + loc.search;
  2375. }
  2376. }, _('To login…')))
  2377. ]);
  2378. LuCI.prototype.raise('SessionError', 'Login session is expired');
  2379. },
  2380. /* private */
  2381. setupDOM: function(res) {
  2382. var domEv = res[0],
  2383. uiClass = res[1],
  2384. rpcClass = res[2],
  2385. formClass = res[3],
  2386. rpcBaseURL = res[4];
  2387. rpcClass.setBaseURL(rpcBaseURL);
  2388. rpcClass.addInterceptor(function(msg, req) {
  2389. if (!LuCI.prototype.isObject(msg) ||
  2390. !LuCI.prototype.isObject(msg.error) ||
  2391. msg.error.code != -32002)
  2392. return;
  2393. if (!LuCI.prototype.isObject(req) ||
  2394. (req.object == 'session' && req.method == 'access'))
  2395. return;
  2396. return rpcClass.declare({
  2397. 'object': 'session',
  2398. 'method': 'access',
  2399. 'params': [ 'scope', 'object', 'function' ],
  2400. 'expect': { access: true }
  2401. })('uci', 'luci', 'read').catch(LuCI.prototype.notifySessionExpiry);
  2402. });
  2403. Request.addInterceptor(function(res) {
  2404. var isDenied = false;
  2405. if (res.status == 403 && res.headers.get('X-LuCI-Login-Required') == 'yes')
  2406. isDenied = true;
  2407. if (!isDenied)
  2408. return;
  2409. LuCI.prototype.notifySessionExpiry();
  2410. });
  2411. document.addEventListener('poll-start', function(ev) {
  2412. uiClass.showIndicator('poll-status', _('Refreshing'), function(ev) {
  2413. Request.poll.active() ? Request.poll.stop() : Request.poll.start();
  2414. });
  2415. });
  2416. document.addEventListener('poll-stop', function(ev) {
  2417. uiClass.showIndicator('poll-status', _('Paused'), null, 'inactive');
  2418. });
  2419. return Promise.all([
  2420. this.probeSystemFeatures(),
  2421. this.probePreloadClasses()
  2422. ]).finally(LuCI.prototype.bind(function() {
  2423. var tasks = [];
  2424. if (Array.isArray(preloadClasses))
  2425. for (var i = 0; i < preloadClasses.length; i++)
  2426. tasks.push(this.require(preloadClasses[i]));
  2427. return Promise.all(tasks);
  2428. }, this)).finally(this.initDOM);
  2429. },
  2430. /* private */
  2431. initDOM: function() {
  2432. originalCBIInit();
  2433. Poll.start();
  2434. document.dispatchEvent(new CustomEvent('luci-loaded'));
  2435. },
  2436. /**
  2437. * The `env` object holds environment settings used by LuCI, such
  2438. * as request timeouts, base URLs etc.
  2439. *
  2440. * @instance
  2441. * @memberof LuCI
  2442. */
  2443. env: env,
  2444. /**
  2445. * Construct an absolute filesystem path relative to the server
  2446. * document root.
  2447. *
  2448. * @instance
  2449. * @memberof LuCI
  2450. *
  2451. * @param {...string} [parts]
  2452. * An array of parts to join into a path.
  2453. *
  2454. * @return {string}
  2455. * Return the joined path.
  2456. */
  2457. fspath: function(/* ... */) {
  2458. var path = env.documentroot;
  2459. for (var i = 0; i < arguments.length; i++)
  2460. path += '/' + arguments[i];
  2461. var p = path.replace(/\/+$/, '').replace(/\/+/g, '/').split(/\//),
  2462. res = [];
  2463. for (var i = 0; i < p.length; i++)
  2464. if (p[i] == '..')
  2465. res.pop();
  2466. else if (p[i] != '.')
  2467. res.push(p[i]);
  2468. return res.join('/');
  2469. },
  2470. /**
  2471. * Construct a relative URL path from the given prefix and parts.
  2472. * The resulting URL is guaranteed to only contain the characters
  2473. * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
  2474. * as `/` for the path separator.
  2475. *
  2476. * @instance
  2477. * @memberof LuCI
  2478. *
  2479. * @param {string} [prefix]
  2480. * The prefix to join the given parts with. If the `prefix` is
  2481. * omitted, it defaults to an empty string.
  2482. *
  2483. * @param {string[]} [parts]
  2484. * An array of parts to join into an URL path. Parts may contain
  2485. * slashes and any of the other characters mentioned above.
  2486. *
  2487. * @return {string}
  2488. * Return the joined URL path.
  2489. */
  2490. path: function(prefix, parts) {
  2491. var url = [ prefix || '' ];
  2492. for (var i = 0; i < parts.length; i++)
  2493. if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i]))
  2494. url.push('/', parts[i]);
  2495. if (url.length === 1)
  2496. url.push('/');
  2497. return url.join('');
  2498. },
  2499. /**
  2500. * Construct an URL pathrelative to the script path of the server
  2501. * side LuCI application (usually `/cgi-bin/luci`).
  2502. *
  2503. * The resulting URL is guaranteed to only contain the characters
  2504. * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
  2505. * as `/` for the path separator.
  2506. *
  2507. * @instance
  2508. * @memberof LuCI
  2509. *
  2510. * @param {string[]} [parts]
  2511. * An array of parts to join into an URL path. Parts may contain
  2512. * slashes and any of the other characters mentioned above.
  2513. *
  2514. * @return {string}
  2515. * Returns the resulting URL path.
  2516. */
  2517. url: function() {
  2518. return this.path(env.scriptname, arguments);
  2519. },
  2520. /**
  2521. * Construct an URL path relative to the global static resource path
  2522. * of the LuCI ui (usually `/luci-static/resources`).
  2523. *
  2524. * The resulting URL is guaranteed to only contain the characters
  2525. * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
  2526. * as `/` for the path separator.
  2527. *
  2528. * @instance
  2529. * @memberof LuCI
  2530. *
  2531. * @param {string[]} [parts]
  2532. * An array of parts to join into an URL path. Parts may contain
  2533. * slashes and any of the other characters mentioned above.
  2534. *
  2535. * @return {string}
  2536. * Returns the resulting URL path.
  2537. */
  2538. resource: function() {
  2539. return this.path(env.resource, arguments);
  2540. },
  2541. /**
  2542. * Construct an URL path relative to the media resource path of the
  2543. * LuCI ui (usually `/luci-static/$theme_name`).
  2544. *
  2545. * The resulting URL is guaranteed to only contain the characters
  2546. * `a-z`, `A-Z`, `0-9`, `_`, `.`, `%`, `,`, `;`, and `-` as well
  2547. * as `/` for the path separator.
  2548. *
  2549. * @instance
  2550. * @memberof LuCI
  2551. *
  2552. * @param {string[]} [parts]
  2553. * An array of parts to join into an URL path. Parts may contain
  2554. * slashes and any of the other characters mentioned above.
  2555. *
  2556. * @return {string}
  2557. * Returns the resulting URL path.
  2558. */
  2559. media: function() {
  2560. return this.path(env.media, arguments);
  2561. },
  2562. /**
  2563. * Return the complete URL path to the current view.
  2564. *
  2565. * @instance
  2566. * @memberof LuCI
  2567. *
  2568. * @return {string}
  2569. * Returns the URL path to the current view.
  2570. */
  2571. location: function() {
  2572. return this.path(env.scriptname, env.requestpath);
  2573. },
  2574. /**
  2575. * Tests whether the passed argument is a JavaScript object.
  2576. * This function is meant to be an object counterpart to the
  2577. * standard `Array.isArray()` function.
  2578. *
  2579. * @instance
  2580. * @memberof LuCI
  2581. *
  2582. * @param {*} [val]
  2583. * The value to test
  2584. *
  2585. * @return {boolean}
  2586. * Returns `true` if the given value is of type object and
  2587. * not `null`, else returns `false`.
  2588. */
  2589. isObject: function(val) {
  2590. return (val != null && typeof(val) == 'object');
  2591. },
  2592. /**
  2593. * Return an array of sorted object keys, optionally sorted by
  2594. * a different key or a different sorting mode.
  2595. *
  2596. * @instance
  2597. * @memberof LuCI
  2598. *
  2599. * @param {object} obj
  2600. * The object to extract the keys from. If the given value is
  2601. * not an object, the function will return an empty array.
  2602. *
  2603. * @param {string} [key]
  2604. * Specifies the key to order by. This is mainly useful for
  2605. * nested objects of objects or objects of arrays when sorting
  2606. * shall not be performed by the primary object keys but by
  2607. * some other key pointing to a value within the nested values.
  2608. *
  2609. * @param {string} [sortmode]
  2610. * May be either `addr` or `num` to override the natural
  2611. * lexicographic sorting with a sorting suitable for IP/MAC style
  2612. * addresses or numeric values respectively.
  2613. *
  2614. * @return {string[]}
  2615. * Returns an array containing the sorted keys of the given object.
  2616. */
  2617. sortedKeys: function(obj, key, sortmode) {
  2618. if (obj == null || typeof(obj) != 'object')
  2619. return [];
  2620. return Object.keys(obj).map(function(e) {
  2621. var v = (key != null) ? obj[e][key] : e;
  2622. switch (sortmode) {
  2623. case 'addr':
  2624. v = (v != null) ? v.replace(/(?:^|[.:])([0-9a-fA-F]{1,4})/g,
  2625. function(m0, m1) { return ('000' + m1.toLowerCase()).substr(-4) }) : null;
  2626. break;
  2627. case 'num':
  2628. v = (v != null) ? +v : null;
  2629. break;
  2630. }
  2631. return [ e, v ];
  2632. }).filter(function(e) {
  2633. return (e[1] != null);
  2634. }).sort(function(a, b) {
  2635. if (a[1] < b[1])
  2636. return -1;
  2637. else if (a[1] > b[1])
  2638. return 1;
  2639. else
  2640. return 0;
  2641. }).map(function(e) {
  2642. return e[0];
  2643. });
  2644. },
  2645. /**
  2646. * Converts the given value to an array. If the given value is of
  2647. * type array, it is returned as-is, values of type object are
  2648. * returned as one-element array containing the object, empty
  2649. * strings and `null` values are returned as empty array, all other
  2650. * values are converted using `String()`, trimmed, split on white
  2651. * space and returned as array.
  2652. *
  2653. * @instance
  2654. * @memberof LuCI
  2655. *
  2656. * @param {*} val
  2657. * The value to convert into an array.
  2658. *
  2659. * @return {Array<*>}
  2660. * Returns the resulting array.
  2661. */
  2662. toArray: function(val) {
  2663. if (val == null)
  2664. return [];
  2665. else if (Array.isArray(val))
  2666. return val;
  2667. else if (typeof(val) == 'object')
  2668. return [ val ];
  2669. var s = String(val).trim();
  2670. if (s == '')
  2671. return [];
  2672. return s.split(/\s+/);
  2673. },
  2674. /**
  2675. * Returns a promise resolving with either the given value or or with
  2676. * the given default in case the input value is a rejecting promise.
  2677. *
  2678. * @instance
  2679. * @memberof LuCI
  2680. *
  2681. * @param {*} value
  2682. * The value to resolve the promise with.
  2683. *
  2684. * @param {*} defvalue
  2685. * The default value to resolve the promise with in case the given
  2686. * input value is a rejecting promise.
  2687. *
  2688. * @returns {Promise<*>}
  2689. * Returns a new promise resolving either to the given input value or
  2690. * to the given default value on error.
  2691. */
  2692. resolveDefault: function(value, defvalue) {
  2693. return Promise.resolve(value).catch(function() { return defvalue });
  2694. },
  2695. /**
  2696. * The request callback function is invoked whenever an HTTP
  2697. * reply to a request made using the `L.get()`, `L.post()` or
  2698. * `L.poll()` function is timed out or received successfully.
  2699. *
  2700. * @instance
  2701. * @memberof LuCI
  2702. *
  2703. * @callback LuCI.requestCallbackFn
  2704. * @param {XMLHTTPRequest} xhr
  2705. * The XMLHTTPRequest instance used to make the request.
  2706. *
  2707. * @param {*} data
  2708. * The response JSON if the response could be parsed as such,
  2709. * else `null`.
  2710. *
  2711. * @param {number} duration
  2712. * The total duration of the request in milliseconds.
  2713. */
  2714. /**
  2715. * Issues a GET request to the given url and invokes the specified
  2716. * callback function. The function is a wrapper around
  2717. * {@link LuCI.request#request Request.request()}.
  2718. *
  2719. * @deprecated
  2720. * @instance
  2721. * @memberof LuCI
  2722. *
  2723. * @param {string} url
  2724. * The URL to request.
  2725. *
  2726. * @param {Object<string, string>} [args]
  2727. * Additional query string arguments to append to the URL.
  2728. *
  2729. * @param {LuCI.requestCallbackFn} cb
  2730. * The callback function to invoke when the request finishes.
  2731. *
  2732. * @return {Promise<null>}
  2733. * Returns a promise resolving to `null` when concluded.
  2734. */
  2735. get: function(url, args, cb) {
  2736. return this.poll(null, url, args, cb, false);
  2737. },
  2738. /**
  2739. * Issues a POST request to the given url and invokes the specified
  2740. * callback function. The function is a wrapper around
  2741. * {@link LuCI.request#request Request.request()}. The request is
  2742. * sent using `application/x-www-form-urlencoded` encoding and will
  2743. * contain a field `token` with the current value of `LuCI.env.token`
  2744. * by default.
  2745. *
  2746. * @deprecated
  2747. * @instance
  2748. * @memberof LuCI
  2749. *
  2750. * @param {string} url
  2751. * The URL to request.
  2752. *
  2753. * @param {Object<string, string>} [args]
  2754. * Additional post arguments to append to the request body.
  2755. *
  2756. * @param {LuCI.requestCallbackFn} cb
  2757. * The callback function to invoke when the request finishes.
  2758. *
  2759. * @return {Promise<null>}
  2760. * Returns a promise resolving to `null` when concluded.
  2761. */
  2762. post: function(url, args, cb) {
  2763. return this.poll(null, url, args, cb, true);
  2764. },
  2765. /**
  2766. * Register a polling HTTP request that invokes the specified
  2767. * callback function. The function is a wrapper around
  2768. * {@link LuCI.request.poll#add Request.poll.add()}.
  2769. *
  2770. * @deprecated
  2771. * @instance
  2772. * @memberof LuCI
  2773. *
  2774. * @param {number} interval
  2775. * The poll interval to use. If set to a value less than or equal
  2776. * to `0`, it will default to the global poll interval configured
  2777. * in `LuCI.env.pollinterval`.
  2778. *
  2779. * @param {string} url
  2780. * The URL to request.
  2781. *
  2782. * @param {Object<string, string>} [args]
  2783. * Specifies additional arguments for the request. For GET requests,
  2784. * the arguments are appended to the URL as query string, for POST
  2785. * requests, they'll be added to the request body.
  2786. *
  2787. * @param {LuCI.requestCallbackFn} cb
  2788. * The callback function to invoke whenever a request finishes.
  2789. *
  2790. * @param {boolean} [post=false]
  2791. * When set to `false` or not specified, poll requests will be made
  2792. * using the GET method. When set to `true`, POST requests will be
  2793. * issued. In case of POST requests, the request body will contain
  2794. * an argument `token` with the current value of `LuCI.env.token` by
  2795. * default, regardless of the parameters specified with `args`.
  2796. *
  2797. * @return {function}
  2798. * Returns the internally created function that has been passed to
  2799. * {@link LuCI.request.poll#add Request.poll.add()}. This value can
  2800. * be passed to {@link LuCI.poll.remove Poll.remove()} to remove the
  2801. * polling request.
  2802. */
  2803. poll: function(interval, url, args, cb, post) {
  2804. if (interval !== null && interval <= 0)
  2805. interval = env.pollinterval;
  2806. var data = post ? { token: env.token } : null,
  2807. method = post ? 'POST' : 'GET';
  2808. if (!/^(?:\/|\S+:\/\/)/.test(url))
  2809. url = this.url(url);
  2810. if (args != null)
  2811. data = Object.assign(data || {}, args);
  2812. if (interval !== null)
  2813. return Request.poll.add(interval, url, { method: method, query: data }, cb);
  2814. else
  2815. return Request.request(url, { method: method, query: data })
  2816. .then(function(res) {
  2817. var json = null;
  2818. if (/^application\/json\b/.test(res.headers.get('Content-Type')))
  2819. try { json = res.json() } catch(e) {}
  2820. cb(res.xhr, json, res.duration);
  2821. });
  2822. },
  2823. /**
  2824. * Check whether a view has sufficient permissions.
  2825. *
  2826. * @return {boolean|null}
  2827. * Returns `null` if the current session has no permission at all to
  2828. * load resources required by the view. Returns `false` if readonly
  2829. * permissions are granted or `true` if at least one required ACL
  2830. * group is granted with write permissions.
  2831. */
  2832. hasViewPermission: function() {
  2833. if (!this.isObject(env.nodespec) || !env.nodespec.satisfied)
  2834. return null;
  2835. return !env.nodespec.readonly;
  2836. },
  2837. /**
  2838. * Deprecated wrapper around {@link LuCI.poll.remove Poll.remove()}.
  2839. *
  2840. * @deprecated
  2841. * @instance
  2842. * @memberof LuCI
  2843. *
  2844. * @param {function} entry
  2845. * The polling function to remove.
  2846. *
  2847. * @return {boolean}
  2848. * Returns `true` when the function has been removed or `false` if
  2849. * it could not be found.
  2850. */
  2851. stop: function(entry) { return Poll.remove(entry) },
  2852. /**
  2853. * Deprecated wrapper around {@link LuCI.poll.stop Poll.stop()}.
  2854. *
  2855. * @deprecated
  2856. * @instance
  2857. * @memberof LuCI
  2858. *
  2859. * @return {boolean}
  2860. * Returns `true` when the polling loop has been stopped or `false`
  2861. * when it didn't run to begin with.
  2862. */
  2863. halt: function() { return Poll.stop() },
  2864. /**
  2865. * Deprecated wrapper around {@link LuCI.poll.start Poll.start()}.
  2866. *
  2867. * @deprecated
  2868. * @instance
  2869. * @memberof LuCI
  2870. *
  2871. * @return {boolean}
  2872. * Returns `true` when the polling loop has been started or `false`
  2873. * when it was already running.
  2874. */
  2875. run: function() { return Poll.start() },
  2876. /**
  2877. * Legacy `L.dom` class alias. New view code should use `'require dom';`
  2878. * to request the `LuCI.dom` class.
  2879. *
  2880. * @instance
  2881. * @memberof LuCI
  2882. * @deprecated
  2883. */
  2884. dom: DOM,
  2885. /**
  2886. * Legacy `L.view` class alias. New view code should use `'require view';`
  2887. * to request the `LuCI.view` class.
  2888. *
  2889. * @instance
  2890. * @memberof LuCI
  2891. * @deprecated
  2892. */
  2893. view: View,
  2894. /**
  2895. * Legacy `L.Poll` class alias. New view code should use `'require poll';`
  2896. * to request the `LuCI.poll` class.
  2897. *
  2898. * @instance
  2899. * @memberof LuCI
  2900. * @deprecated
  2901. */
  2902. Poll: Poll,
  2903. /**
  2904. * Legacy `L.Request` class alias. New view code should use `'require request';`
  2905. * to request the `LuCI.request` class.
  2906. *
  2907. * @instance
  2908. * @memberof LuCI
  2909. * @deprecated
  2910. */
  2911. Request: Request,
  2912. /**
  2913. * Legacy `L.Class` class alias. New view code should use `'require baseclass';`
  2914. * to request the `LuCI.baseclass` class.
  2915. *
  2916. * @instance
  2917. * @memberof LuCI
  2918. * @deprecated
  2919. */
  2920. Class: Class
  2921. });
  2922. /**
  2923. * @class xhr
  2924. * @memberof LuCI
  2925. * @deprecated
  2926. * @classdesc
  2927. *
  2928. * The `LuCI.xhr` class is a legacy compatibility shim for the
  2929. * functionality formerly provided by `xhr.js`. It is registered as global
  2930. * `window.XHR` symbol for compatibility with legacy code.
  2931. *
  2932. * New code should use {@link LuCI.request} instead to implement HTTP
  2933. * request handling.
  2934. */
  2935. var XHR = Class.extend(/** @lends LuCI.xhr.prototype */ {
  2936. __name__: 'LuCI.xhr',
  2937. __init__: function() {
  2938. if (window.console && console.debug)
  2939. console.debug('Direct use XHR() is deprecated, please use L.Request instead');
  2940. },
  2941. _response: function(cb, res, json, duration) {
  2942. if (this.active)
  2943. cb(res, json, duration);
  2944. delete this.active;
  2945. },
  2946. /**
  2947. * This function is a legacy wrapper around
  2948. * {@link LuCI#get LuCI.get()}.
  2949. *
  2950. * @instance
  2951. * @deprecated
  2952. * @memberof LuCI.xhr
  2953. *
  2954. * @param {string} url
  2955. * The URL to request
  2956. *
  2957. * @param {Object} [data]
  2958. * Additional query string data
  2959. *
  2960. * @param {LuCI.requestCallbackFn} [callback]
  2961. * Callback function to invoke on completion
  2962. *
  2963. * @param {number} [timeout]
  2964. * Request timeout to use
  2965. *
  2966. * @return {Promise<null>}
  2967. */
  2968. get: function(url, data, callback, timeout) {
  2969. this.active = true;
  2970. LuCI.prototype.get(url, data, this._response.bind(this, callback), timeout);
  2971. },
  2972. /**
  2973. * This function is a legacy wrapper around
  2974. * {@link LuCI#post LuCI.post()}.
  2975. *
  2976. * @instance
  2977. * @deprecated
  2978. * @memberof LuCI.xhr
  2979. *
  2980. * @param {string} url
  2981. * The URL to request
  2982. *
  2983. * @param {Object} [data]
  2984. * Additional data to append to the request body.
  2985. *
  2986. * @param {LuCI.requestCallbackFn} [callback]
  2987. * Callback function to invoke on completion
  2988. *
  2989. * @param {number} [timeout]
  2990. * Request timeout to use
  2991. *
  2992. * @return {Promise<null>}
  2993. */
  2994. post: function(url, data, callback, timeout) {
  2995. this.active = true;
  2996. LuCI.prototype.post(url, data, this._response.bind(this, callback), timeout);
  2997. },
  2998. /**
  2999. * Cancels a running request.
  3000. *
  3001. * This function does not actually cancel the underlying
  3002. * `XMLHTTPRequest` request but it sets a flag which prevents the
  3003. * invocation of the callback function when the request eventually
  3004. * finishes or timed out.
  3005. *
  3006. * @instance
  3007. * @deprecated
  3008. * @memberof LuCI.xhr
  3009. */
  3010. cancel: function() { delete this.active },
  3011. /**
  3012. * Checks the running state of the request.
  3013. *
  3014. * @instance
  3015. * @deprecated
  3016. * @memberof LuCI.xhr
  3017. *
  3018. * @returns {boolean}
  3019. * Returns `true` if the request is still running or `false` if it
  3020. * already completed.
  3021. */
  3022. busy: function() { return (this.active === true) },
  3023. /**
  3024. * Ignored for backwards compatibility.
  3025. *
  3026. * This function does nothing.
  3027. *
  3028. * @instance
  3029. * @deprecated
  3030. * @memberof LuCI.xhr
  3031. */
  3032. abort: function() {},
  3033. /**
  3034. * Existing for backwards compatibility.
  3035. *
  3036. * This function simply throws an `InternalError` when invoked.
  3037. *
  3038. * @instance
  3039. * @deprecated
  3040. * @memberof LuCI.xhr
  3041. *
  3042. * @throws {InternalError}
  3043. * Throws an `InternalError` with the message `Not implemented`
  3044. * when invoked.
  3045. */
  3046. send_form: function() { LuCI.prototype.error('InternalError', 'Not implemented') },
  3047. });
  3048. XHR.get = function() { return LuCI.prototype.get.apply(LuCI.prototype, arguments) };
  3049. XHR.post = function() { return LuCI.prototype.post.apply(LuCI.prototype, arguments) };
  3050. XHR.poll = function() { return LuCI.prototype.poll.apply(LuCI.prototype, arguments) };
  3051. XHR.stop = Request.poll.remove.bind(Request.poll);
  3052. XHR.halt = Request.poll.stop.bind(Request.poll);
  3053. XHR.run = Request.poll.start.bind(Request.poll);
  3054. XHR.running = Request.poll.active.bind(Request.poll);
  3055. window.XHR = XHR;
  3056. window.LuCI = LuCI;
  3057. })(window, document);