Drop support for luaposix and bitlib (obsoleted by nixio)
[project/luci.git] / libs / web / luasrc / dispatcher.lua
1 --[[
2 LuCI - Dispatcher
3
4 Description:
5 The request dispatcher and module dispatcher generators
6
7 FileId:
8 $Id$
9
10 License:
11 Copyright 2008 Steven Barth <steven@midlink.org>
12
13 Licensed under the Apache License, Version 2.0 (the "License");
14 you may not use this file except in compliance with the License.
15 You may obtain a copy of the License at
16
17 http://www.apache.org/licenses/LICENSE-2.0
18
19 Unless required by applicable law or agreed to in writing, software
20 distributed under the License is distributed on an "AS IS" BASIS,
21 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 See the License for the specific language governing permissions and
23 limitations under the License.
24
25 ]]--
26
27 --- LuCI web dispatcher.
28 local fs = require "luci.fs"
29 local sys = require "luci.sys"
30 local init = require "luci.init"
31 local util = require "luci.util"
32 local http = require "luci.http"
33 local nixio = require "nixio", require "nixio.util"
34
35 module("luci.dispatcher", package.seeall)
36 context = util.threadlocal()
37
38 authenticator = {}
39
40 -- Index table
41 local index = nil
42
43 -- Fastindex
44 local fi
45
46
47 --- Build the URL relative to the server webroot from given virtual path.
48 -- @param ... Virtual path
49 -- @return Relative URL
50 function build_url(...)
51 local path = {...}
52 local sn = http.getenv("SCRIPT_NAME") or ""
53 for k, v in pairs(context.urltoken) do
54 sn = sn .. "/;" .. k .. "=" .. http.urlencode(v)
55 end
56 return sn .. ((#path > 0) and "/" .. table.concat(path, "/") or "")
57 end
58
59 --- Send a 404 error code and render the "error404" template if available.
60 -- @param message Custom error message (optional)
61 -- @return false
62 function error404(message)
63 luci.http.status(404, "Not Found")
64 message = message or "Not Found"
65
66 require("luci.template")
67 if not luci.util.copcall(luci.template.render, "error404") then
68 luci.http.prepare_content("text/plain")
69 luci.http.write(message)
70 end
71 return false
72 end
73
74 --- Send a 500 error code and render the "error500" template if available.
75 -- @param message Custom error message (optional)#
76 -- @return false
77 function error500(message)
78 luci.util.perror(message)
79 if not context.template_header_sent then
80 luci.http.status(500, "Internal Server Error")
81 luci.http.prepare_content("text/plain")
82 luci.http.write(message)
83 else
84 require("luci.template")
85 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
86 luci.http.prepare_content("text/plain")
87 luci.http.write(message)
88 end
89 end
90 return false
91 end
92
93 function authenticator.htmlauth(validator, accs, default)
94 local user = luci.http.formvalue("username")
95 local pass = luci.http.formvalue("password")
96
97 if user and validator(user, pass) then
98 return user
99 end
100
101 require("luci.i18n")
102 require("luci.template")
103 context.path = {}
104 luci.template.render("sysauth", {duser=default, fuser=user})
105 return false
106
107 end
108
109 --- Dispatch an HTTP request.
110 -- @param request LuCI HTTP Request object
111 function httpdispatch(request)
112 luci.http.context.request = request
113 context.request = {}
114 local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
115
116 for node in pathinfo:gmatch("[^/]+") do
117 table.insert(context.request, node)
118 end
119
120 local stat, err = util.coxpcall(function()
121 dispatch(context.request)
122 end, error500)
123
124 luci.http.close()
125
126 --context._disable_memtrace()
127 end
128
129 --- Dispatches a LuCI virtual path.
130 -- @param request Virtual path
131 function dispatch(request)
132 --context._disable_memtrace = require "luci.debug".trap_memtrace()
133 local ctx = context
134 ctx.path = request
135 ctx.urltoken = ctx.urltoken or {}
136
137 local conf = require "luci.config"
138 assert(conf.main,
139 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
140
141 local lang = conf.main.lang
142 if lang == "auto" then
143 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
144 for lpat in aclang:gmatch("[%w-]+") do
145 lpat = lpat and lpat:gsub("-", "_")
146 if conf.languages[lpat] then
147 lang = lpat
148 break
149 end
150 end
151 end
152 require "luci.i18n".setlanguage(lang)
153
154 local c = ctx.tree
155 local stat
156 if not c then
157 c = createtree()
158 end
159
160 local track = {}
161 local args = {}
162 ctx.args = args
163 ctx.requestargs = ctx.requestargs or args
164 local n
165 local t = true
166 local token = ctx.urltoken
167 local preq = {}
168 local freq = {}
169
170 for i, s in ipairs(request) do
171 local tkey, tval
172 if t then
173 tkey, tval = s:match(";(%w+)=(.*)")
174 end
175
176 if tkey then
177 token[tkey] = tval
178 else
179 t = false
180 preq[#preq+1] = s
181 freq[#freq+1] = s
182 c = c.nodes[s]
183 n = i
184 if not c then
185 break
186 end
187
188 util.update(track, c)
189
190 if c.leaf then
191 break
192 end
193 end
194 end
195
196 if c and c.leaf then
197 for j=n+1, #request do
198 args[#args+1] = request[j]
199 freq[#freq+1] = request[j]
200 end
201 end
202
203 ctx.requestpath = freq
204 ctx.path = preq
205
206 if track.i18n then
207 require("luci.i18n").loadc(track.i18n)
208 end
209
210 -- Init template engine
211 if (c and c.index) or not track.notemplate then
212 local tpl = require("luci.template")
213 local media = track.mediaurlbase or luci.config.main.mediaurlbase
214 if not tpl.Template("themes/%s/header" % fs.basename(media)) then
215 --if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
216 media = nil
217 for name, theme in pairs(luci.config.themes) do
218 if name:sub(1,1) ~= "." and pcall(tpl.Template,
219 "themes/%s/header" % fs.basename(theme)) then
220 media = theme
221 end
222 end
223 assert(media, "No valid theme found")
224 end
225
226 local viewns = setmetatable({}, {__index=function(table, key)
227 if key == "controller" then
228 return build_url()
229 elseif key == "REQUEST_URI" then
230 return build_url(unpack(ctx.requestpath))
231 else
232 return rawget(table, key) or _G[key]
233 end
234 end})
235 tpl.context.viewns = viewns
236 viewns.write = luci.http.write
237 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
238 viewns.translate = function(...) return require("luci.i18n").translate(...) end
239 viewns.striptags = util.striptags
240 viewns.media = media
241 viewns.theme = fs.basename(media)
242 viewns.resource = luci.config.main.resourcebase
243 end
244
245 track.dependent = (track.dependent ~= false)
246 assert(not track.dependent or not track.auto, "Access Violation")
247
248 if track.sysauth then
249 local sauth = require "luci.sauth"
250
251 local authen = type(track.sysauth_authenticator) == "function"
252 and track.sysauth_authenticator
253 or authenticator[track.sysauth_authenticator]
254
255 local def = (type(track.sysauth) == "string") and track.sysauth
256 local accs = def and {track.sysauth} or track.sysauth
257 local sess = ctx.authsession
258 local verifytoken = false
259 if not sess then
260 sess = luci.http.getcookie("sysauth")
261 sess = sess and sess:match("^[A-F0-9]+$")
262 verifytoken = true
263 end
264
265 local sdat = sauth.read(sess)
266 local user
267
268 if sdat then
269 sdat = loadstring(sdat)
270 setfenv(sdat, {})
271 sdat = sdat()
272 if not verifytoken or ctx.urltoken.stok == sdat.token then
273 user = sdat.user
274 end
275 end
276
277 if not util.contains(accs, user) then
278 if authen then
279 ctx.urltoken.stok = nil
280 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
281 if not user or not util.contains(accs, user) then
282 return
283 else
284 local sid = sess or luci.sys.uniqueid(16)
285 if not sess then
286 local token = luci.sys.uniqueid(16)
287 sauth.write(sid, util.get_bytecode({
288 user=user,
289 token=token,
290 secret=luci.sys.uniqueid(16)
291 }))
292 ctx.urltoken.stok = token
293 end
294 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path="..build_url())
295 ctx.authsession = sid
296 end
297 else
298 luci.http.status(403, "Forbidden")
299 return
300 end
301 else
302 ctx.authsession = sess
303 end
304 end
305
306 if track.setgroup then
307 luci.sys.process.setgroup(track.setgroup)
308 end
309
310 if track.setuser then
311 luci.sys.process.setuser(track.setuser)
312 end
313
314 local target = nil
315 if c then
316 if type(c.target) == "function" then
317 target = c.target
318 elseif type(c.target) == "table" then
319 target = c.target.target
320 end
321 end
322
323 if c and (c.index or type(target) == "function") then
324 ctx.dispatched = c
325 ctx.requested = ctx.requested or ctx.dispatched
326 end
327
328 if c and c.index then
329 local tpl = require "luci.template"
330
331 if util.copcall(tpl.render, "indexer", {}) then
332 return true
333 end
334 end
335
336 if type(target) == "function" then
337 util.copcall(function()
338 local oldenv = getfenv(target)
339 local module = require(c.module)
340 local env = setmetatable({}, {__index=
341
342 function(tbl, key)
343 return rawget(tbl, key) or module[key] or oldenv[key]
344 end})
345
346 setfenv(target, env)
347 end)
348
349 if type(c.target) == "table" then
350 target(c.target, unpack(args))
351 else
352 target(unpack(args))
353 end
354 else
355 error404()
356 end
357 end
358
359 --- Generate the dispatching index using the best possible strategy.
360 function createindex()
361 local path = luci.util.libpath() .. "/controller/"
362 local suff = { ".lua", ".lua.gz" }
363
364 if luci.util.copcall(require, "luci.fastindex") then
365 createindex_fastindex(path, suff)
366 else
367 createindex_plain(path, suff)
368 end
369 end
370
371 --- Generate the dispatching index using the fastindex C-indexer.
372 -- @param path Controller base directory
373 -- @param suffixes Controller file suffixes
374 function createindex_fastindex(path, suffixes)
375 index = {}
376
377 if not fi then
378 fi = luci.fastindex.new("index")
379 for _, suffix in ipairs(suffixes) do
380 fi.add(path .. "*" .. suffix)
381 fi.add(path .. "*/*" .. suffix)
382 end
383 end
384 fi.scan()
385
386 for k, v in pairs(fi.indexes) do
387 index[v[2]] = v[1]
388 end
389 end
390
391 --- Generate the dispatching index using the native file-cache based strategy.
392 -- @param path Controller base directory
393 -- @param suffixes Controller file suffixes
394 function createindex_plain(path, suffixes)
395 local controllers = { }
396 for _, suffix in ipairs(suffixes) do
397 controllers = util.combine(
398 controllers,
399 luci.fs.glob(path .. "*" .. suffix) or {},
400 luci.fs.glob(path .. "*/*" .. suffix) or {}
401 )
402 end
403
404 if indexcache then
405 local cachedate = fs.mtime(indexcache)
406 if cachedate then
407 local realdate = 0
408 for _, obj in ipairs(controllers) do
409 local omtime = fs.mtime(path .. "/" .. obj)
410 realdate = (omtime and omtime > realdate) and omtime or realdate
411 end
412
413 if cachedate > realdate then
414 assert(
415 sys.process.info("uid") == fs.stat(indexcache, "uid")
416 and fs.stat(indexcache, "modestr") == "rw-------",
417 "Fatal: Indexcache is not sane!"
418 )
419
420 index = loadfile(indexcache)()
421 return index
422 end
423 end
424 end
425
426 index = {}
427
428 for i,c in ipairs(controllers) do
429 local module = "luci.controller." .. c:sub(#path+1, #c):gsub("/", ".")
430 for _, suffix in ipairs(suffixes) do
431 module = module:gsub(suffix.."$", "")
432 end
433
434 local mod = require(module)
435 local idx = mod.index
436
437 if type(idx) == "function" then
438 index[module] = idx
439 end
440 end
441
442 if indexcache then
443 local f = nixio.open(indexcache, "w", 600)
444 f:writeall(util.get_bytecode(index))
445 f:close()
446 end
447 end
448
449 --- Create the dispatching tree from the index.
450 -- Build the index before if it does not exist yet.
451 function createtree()
452 if not index then
453 createindex()
454 end
455
456 local ctx = context
457 local tree = {nodes={}}
458 local modi = {}
459
460 ctx.treecache = setmetatable({}, {__mode="v"})
461 ctx.tree = tree
462 ctx.modifiers = modi
463
464 -- Load default translation
465 require "luci.i18n".loadc("default")
466
467 local scope = setmetatable({}, {__index = luci.dispatcher})
468
469 for k, v in pairs(index) do
470 scope._NAME = k
471 setfenv(v, scope)
472 v()
473 end
474
475 local function modisort(a,b)
476 return modi[a].order < modi[b].order
477 end
478
479 for _, v in util.spairs(modi, modisort) do
480 scope._NAME = v.module
481 setfenv(v.func, scope)
482 v.func()
483 end
484
485 return tree
486 end
487
488 --- Register a tree modifier.
489 -- @param func Modifier function
490 -- @param order Modifier order value (optional)
491 function modifier(func, order)
492 context.modifiers[#context.modifiers+1] = {
493 func = func,
494 order = order or 0,
495 module
496 = getfenv(2)._NAME
497 }
498 end
499
500 --- Clone a node of the dispatching tree to another position.
501 -- @param path Virtual path destination
502 -- @param clone Virtual path source
503 -- @param title Destination node title (optional)
504 -- @param order Destination node order value (optional)
505 -- @return Dispatching tree node
506 function assign(path, clone, title, order)
507 local obj = node(unpack(path))
508 obj.nodes = nil
509 obj.module = nil
510
511 obj.title = title
512 obj.order = order
513
514 setmetatable(obj, {__index = _create_node(clone)})
515
516 return obj
517 end
518
519 --- Create a new dispatching node and define common parameters.
520 -- @param path Virtual path
521 -- @param target Target function to call when dispatched.
522 -- @param title Destination node title
523 -- @param order Destination node order value (optional)
524 -- @return Dispatching tree node
525 function entry(path, target, title, order)
526 local c = node(unpack(path))
527
528 c.target = target
529 c.title = title
530 c.order = order
531 c.module = getfenv(2)._NAME
532
533 return c
534 end
535
536 --- Fetch or create a dispatching node without setting the target module or
537 -- enabling the node.
538 -- @param ... Virtual path
539 -- @return Dispatching tree node
540 function get(...)
541 return _create_node({...})
542 end
543
544 --- Fetch or create a new dispatching node.
545 -- @param ... Virtual path
546 -- @return Dispatching tree node
547 function node(...)
548 local c = _create_node({...})
549
550 c.module = getfenv(2)._NAME
551 c.auto = nil
552
553 return c
554 end
555
556 function _create_node(path, cache)
557 if #path == 0 then
558 return context.tree
559 end
560
561 cache = cache or context.treecache
562 local name = table.concat(path, ".")
563 local c = cache[name]
564
565 if not c then
566 local new = {nodes={}, auto=true, path=util.clone(path)}
567 local last = table.remove(path)
568
569 c = _create_node(path, cache)
570
571 c.nodes[last] = new
572 cache[name] = new
573
574 return new
575 else
576 return c
577 end
578 end
579
580 -- Subdispatchers --
581
582 --- Create a redirect to another dispatching node.
583 -- @param ... Virtual path destination
584 function alias(...)
585 local req = {...}
586 return function(...)
587 for _, r in ipairs({...}) do
588 req[#req+1] = r
589 end
590
591 dispatch(req)
592 end
593 end
594
595 --- Rewrite the first x path values of the request.
596 -- @param n Number of path values to replace
597 -- @param ... Virtual path to replace removed path values with
598 function rewrite(n, ...)
599 local req = {...}
600 return function(...)
601 local dispatched = util.clone(context.dispatched)
602
603 for i=1,n do
604 table.remove(dispatched, 1)
605 end
606
607 for i, r in ipairs(req) do
608 table.insert(dispatched, i, r)
609 end
610
611 for _, r in ipairs({...}) do
612 dispatched[#dispatched+1] = r
613 end
614
615 dispatch(dispatched)
616 end
617 end
618
619
620 local function _call(self, ...)
621 if #self.argv > 0 then
622 return getfenv()[self.name](unpack(self.argv), ...)
623 else
624 return getfenv()[self.name](...)
625 end
626 end
627
628 --- Create a function-call dispatching target.
629 -- @param name Target function of local controller
630 -- @param ... Additional parameters passed to the function
631 function call(name, ...)
632 return {type = "call", argv = {...}, name = name, target = _call}
633 end
634
635
636 local _template = function(self, ...)
637 require "luci.template".render(self.view)
638 end
639
640 --- Create a template render dispatching target.
641 -- @param name Template to be rendered
642 function template(name)
643 return {type = "template", view = name, target = _template}
644 end
645
646
647 local function _cbi(self, ...)
648 local cbi = require "luci.cbi"
649 local tpl = require "luci.template"
650 local http = require "luci.http"
651
652 local config = self.config or {}
653 local maps = cbi.load(self.model, ...)
654
655 local state = nil
656
657 for i, res in ipairs(maps) do
658 res.flow = config
659 local cstate = res:parse()
660 if cstate and (not state or cstate < state) then
661 state = cstate
662 end
663 end
664
665 local function _resolve_path(path)
666 return type(path) == "table" and build_url(unpack(path)) or path
667 end
668
669 if config.on_valid_to and state and state > 0 and state < 2 then
670 http.redirect(_resolve_path(config.on_valid_to))
671 return
672 end
673
674 if config.on_changed_to and state and state > 1 then
675 http.redirect(_resolve_path(config.on_changed_to))
676 return
677 end
678
679 if config.on_success_to and state and state > 0 then
680 http.redirect(_resolve_path(config.on_success_to))
681 return
682 end
683
684 if config.state_handler then
685 if not config.state_handler(state, maps) then
686 return
687 end
688 end
689
690 local pageaction = true
691 http.header("X-CBI-State", state or 0)
692 if not config.noheader then
693 tpl.render("cbi/header", {state = state})
694 end
695 for i, res in ipairs(maps) do
696 res:render()
697 if res.pageaction == false then
698 pageaction = false
699 end
700 end
701 if not config.nofooter then
702 tpl.render("cbi/footer", {flow = config, pageaction=pageaction, state = state, autoapply = config.autoapply})
703 end
704 end
705
706 --- Create a CBI model dispatching target.
707 -- @param model CBI model to be rendered
708 function cbi(model, config)
709 return {type = "cbi", config = config, model = model, target = _cbi}
710 end
711
712
713 local function _arcombine(self, ...)
714 local argv = {...}
715 local target = #argv > 0 and self.targets[2] or self.targets[1]
716 setfenv(target.target, self.env)
717 target:target(unpack(argv))
718 end
719
720 --- Create a combined dispatching target for non argv and argv requests.
721 -- @param trg1 Overview Target
722 -- @param trg2 Detail Target
723 function arcombine(trg1, trg2)
724 return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
725 end
726
727
728 local function _form(self, ...)
729 local cbi = require "luci.cbi"
730 local tpl = require "luci.template"
731 local http = require "luci.http"
732
733 local maps = luci.cbi.load(self.model, ...)
734 local state = nil
735
736 for i, res in ipairs(maps) do
737 local cstate = res:parse()
738 if cstate and (not state or cstate < state) then
739 state = cstate
740 end
741 end
742
743 http.header("X-CBI-State", state or 0)
744 tpl.render("header")
745 for i, res in ipairs(maps) do
746 res:render()
747 end
748 tpl.render("footer")
749 end
750
751 --- Create a CBI form model dispatching target.
752 -- @param model CBI form model tpo be rendered
753 function form(model)
754 return {type = "cbi", model = model, target = _form}
755 end