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