Removed UCI Save/Apply cache from Essentials
[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 = luci.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 return luci.http.getenv("SCRIPT_NAME") .. "/" .. table.concat(arg, "/")
51 end
52
53 --- Send a 404 error code and render the "error404" template if available.
54 -- @param message Custom error message (optional)
55 -- @return false
56 function error404(message)
57 luci.http.status(404, "Not Found")
58 message = message or "Not Found"
59
60 require("luci.template")
61 if not luci.util.copcall(luci.template.render, "error404") then
62 luci.http.prepare_content("text/plain")
63 luci.http.write(message)
64 end
65 return false
66 end
67
68 --- Send a 500 error code and render the "error500" template if available.
69 -- @param message Custom error message (optional)#
70 -- @return false
71 function error500(message)
72 luci.http.status(500, "Internal Server Error")
73
74 require("luci.template")
75 if not luci.util.copcall(luci.template.render, "error500", {message=message}) then
76 luci.http.prepare_content("text/plain")
77 luci.http.write(message)
78 end
79 return false
80 end
81
82 function authenticator.htmlauth(validator, accs, default)
83 local user = luci.http.formvalue("username")
84 local pass = luci.http.formvalue("password")
85
86 if user and validator(user, pass) then
87 return user
88 end
89
90 require("luci.i18n")
91 require("luci.template")
92 context.path = {}
93 luci.template.render("sysauth", {duser=default, fuser=user})
94 return false
95
96 end
97
98 --- Dispatch an HTTP request.
99 -- @param request LuCI HTTP Request object
100 function httpdispatch(request)
101 luci.http.context.request = request
102 context.request = {}
103 local pathinfo = request:getenv("PATH_INFO") or ""
104
105 for node in pathinfo:gmatch("[^/]+") do
106 table.insert(context.request, node)
107 end
108
109 local stat, err = util.copcall(dispatch, context.request)
110 if not stat then
111 error500(err)
112 end
113
114 luci.http.close()
115
116 --context._disable_memtrace()
117 end
118
119 --- Dispatches a LuCI virtual path.
120 -- @param request Virtual path
121 function dispatch(request)
122 --context._disable_memtrace = require "luci.debug".trap_memtrace()
123 local ctx = context
124 ctx.path = request
125
126 require "luci.i18n".setlanguage(require "luci.config".main.lang)
127
128 local c = ctx.tree
129 local stat
130 if not c then
131 c = createtree()
132 end
133
134 local track = {}
135 local args = {}
136 context.args = args
137 local n
138
139 for i, s in ipairs(request) do
140 c = c.nodes[s]
141 n = i
142 if not c then
143 break
144 end
145
146 util.update(track, c)
147
148 if c.leaf then
149 break
150 end
151 end
152
153 if c and c.leaf then
154 for j=n+1, #request do
155 table.insert(args, request[j])
156 end
157 end
158
159 if track.i18n then
160 require("luci.i18n").loadc(track.i18n)
161 end
162
163 -- Init template engine
164 if (c and c.index) or not track.notemplate then
165 local tpl = require("luci.template")
166 local media = track.mediaurlbase or luci.config.main.mediaurlbase
167 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
168 media = nil
169 for name, theme in pairs(luci.config.themes) do
170 if name:sub(1,1) ~= "." and pcall(tpl.Template,
171 "themes/%s/header" % fs.basename(theme)) then
172 media = theme
173 end
174 end
175 assert(media, "No valid theme found")
176 end
177
178 local viewns = setmetatable({}, {__index=_G})
179 tpl.context.viewns = viewns
180 viewns.write = luci.http.write
181 viewns.include = function(name) tpl.Template(name):render(getfenv(2)) end
182 viewns.translate = function(...) return require("luci.i18n").translate(...) end
183 viewns.striptags = util.striptags
184 viewns.controller = luci.http.getenv("SCRIPT_NAME")
185 viewns.media = media
186 viewns.theme = fs.basename(media)
187 viewns.resource = luci.config.main.resourcebase
188 viewns.REQUEST_URI = (luci.http.getenv("SCRIPT_NAME") or "") .. (luci.http.getenv("PATH_INFO") or "")
189 end
190
191 track.dependent = (track.dependent ~= false)
192 assert(not track.dependent or not track.auto, "Access Violation")
193
194 if track.sysauth then
195 local sauth = require "luci.sauth"
196
197 local authen = type(track.sysauth_authenticator) == "function"
198 and track.sysauth_authenticator
199 or authenticator[track.sysauth_authenticator]
200
201 local def = (type(track.sysauth) == "string") and track.sysauth
202 local accs = def and {track.sysauth} or track.sysauth
203 local sess = ctx.authsession or luci.http.getcookie("sysauth")
204 sess = sess and sess:match("^[A-F0-9]+$")
205 local user = sauth.read(sess)
206
207 if not util.contains(accs, user) then
208 if authen then
209 local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
210 if not user or not util.contains(accs, user) then
211 return
212 else
213 local sid = sess or luci.sys.uniqueid(16)
214 luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
215 if not sess then
216 sauth.write(sid, user)
217 end
218 ctx.authsession = sid
219 end
220 else
221 luci.http.status(403, "Forbidden")
222 return
223 end
224 end
225 end
226
227 if track.setgroup then
228 luci.sys.process.setgroup(track.setgroup)
229 end
230
231 if track.setuser then
232 luci.sys.process.setuser(track.setuser)
233 end
234
235 if c and (c.index or type(c.target) == "function") then
236 ctx.dispatched = c
237 ctx.requested = ctx.requested or ctx.dispatched
238 end
239
240 if c and c.index then
241 local tpl = require "luci.template"
242
243 if util.copcall(tpl.render, "indexer", {}) then
244 return true
245 end
246 end
247
248 if c and type(c.target) == "function" then
249 util.copcall(function()
250 local oldenv = getfenv(c.target)
251 local module = require(c.module)
252 local env = setmetatable({}, {__index=
253
254 function(tbl, key)
255 return rawget(tbl, key) or module[key] or oldenv[key]
256 end})
257
258 setfenv(c.target, env)
259 end)
260
261 c.target(unpack(args))
262 else
263 error404()
264 end
265 end
266
267 --- Generate the dispatching index using the best possible strategy.
268 function createindex()
269 local path = luci.util.libpath() .. "/controller/"
270 local suff = ".lua"
271
272 if luci.util.copcall(require, "luci.fastindex") then
273 createindex_fastindex(path, suff)
274 else
275 createindex_plain(path, suff)
276 end
277 end
278
279 --- Generate the dispatching index using the fastindex C-indexer.
280 -- @param path Controller base directory
281 -- @param suffix Controller file suffix
282 function createindex_fastindex(path, suffix)
283 index = {}
284
285 if not fi then
286 fi = luci.fastindex.new("index")
287 fi.add(path .. "*" .. suffix)
288 fi.add(path .. "*/*" .. suffix)
289 end
290 fi.scan()
291
292 for k, v in pairs(fi.indexes) do
293 index[v[2]] = v[1]
294 end
295 end
296
297 --- Generate the dispatching index using the native file-cache based strategy.
298 -- @param path Controller base directory
299 -- @param suffix Controller file suffix
300 function createindex_plain(path, suffix)
301 if indexcache then
302 local cachedate = fs.mtime(indexcache)
303 if cachedate and cachedate > fs.mtime(path) then
304
305 assert(
306 sys.process.info("uid") == fs.stat(indexcache, "uid")
307 and fs.stat(indexcache, "mode") == "rw-------",
308 "Fatal: Indexcache is not sane!"
309 )
310
311 index = loadfile(indexcache)()
312 return index
313 end
314 end
315
316 index = {}
317
318 local controllers = util.combine(
319 luci.fs.glob(path .. "*" .. suffix) or {},
320 luci.fs.glob(path .. "*/*" .. suffix) or {}
321 )
322
323 for i,c in ipairs(controllers) do
324 local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
325 local mod = require(module)
326 local idx = mod.index
327
328 if type(idx) == "function" then
329 index[module] = idx
330 end
331 end
332
333 if indexcache then
334 fs.writefile(indexcache, util.get_bytecode(index))
335 fs.chmod(indexcache, "a-rwx,u+rw")
336 end
337 end
338
339 --- Create the dispatching tree from the index.
340 -- Build the index before if it does not exist yet.
341 function createtree()
342 if not index then
343 createindex()
344 end
345
346 local ctx = context
347 local tree = {nodes={}}
348
349 ctx.treecache = setmetatable({}, {__mode="v"})
350 ctx.tree = tree
351
352 -- Load default translation
353 require "luci.i18n".loadc("default")
354
355 local scope = setmetatable({}, {__index = luci.dispatcher})
356
357 for k, v in pairs(index) do
358 scope._NAME = k
359 setfenv(v, scope)
360 v()
361 end
362
363 return tree
364 end
365
366 --- Clone a node of the dispatching tree to another position.
367 -- @param path Virtual path destination
368 -- @param clone Virtual path source
369 -- @param title Destination node title (optional)
370 -- @param order Destination node order value (optional)
371 -- @return Dispatching tree node
372 function assign(path, clone, title, order)
373 local obj = node(unpack(path))
374 obj.nodes = nil
375 obj.module = nil
376
377 obj.title = title
378 obj.order = order
379
380 setmetatable(obj, {__index = _create_node(clone)})
381
382 return obj
383 end
384
385 --- Create a new dispatching node and define common parameters.
386 -- @param path Virtual path
387 -- @param target Target function to call when dispatched.
388 -- @param title Destination node title
389 -- @param order Destination node order value (optional)
390 -- @return Dispatching tree node
391 function entry(path, target, title, order)
392 local c = node(unpack(path))
393
394 c.target = target
395 c.title = title
396 c.order = order
397 c.module = getfenv(2)._NAME
398
399 return c
400 end
401
402 --- Fetch or create a new dispatching node.
403 -- @param ... Virtual path
404 -- @return Dispatching tree node
405 function node(...)
406 local c = _create_node({...})
407
408 c.module = getfenv(2)._NAME
409 c.path = arg
410 c.auto = nil
411
412 return c
413 end
414
415 function _create_node(path, cache)
416 if #path == 0 then
417 return context.tree
418 end
419
420 cache = cache or context.treecache
421 local name = table.concat(path, ".")
422 local c = cache[name]
423
424 if not c then
425 local last = table.remove(path)
426 c = _create_node(path, cache)
427
428 local new = {nodes={}, auto=true}
429 c.nodes[last] = new
430 cache[name] = new
431
432 return new
433 else
434 return c
435 end
436 end
437
438 -- Subdispatchers --
439
440 --- Create a redirect to another dispatching node.
441 -- @param ... Virtual path destination
442 function alias(...)
443 local req = {...}
444 return function(...)
445 for _, r in ipairs({...}) do
446 req[#req+1] = r
447 end
448
449 dispatch(req)
450 end
451 end
452
453 --- Rewrite the first x path values of the request.
454 -- @param n Number of path values to replace
455 -- @param ... Virtual path to replace removed path values with
456 function rewrite(n, ...)
457 local req = {...}
458 return function(...)
459 local dispatched = util.clone(context.dispatched)
460
461 for i=1,n do
462 table.remove(dispatched, 1)
463 end
464
465 for i, r in ipairs(req) do
466 table.insert(dispatched, i, r)
467 end
468
469 for _, r in ipairs({...}) do
470 dispatched[#dispatched+1] = r
471 end
472
473 dispatch(dispatched)
474 end
475 end
476
477 --- Create a function-call dispatching target.
478 -- @param name Target function of local controller
479 -- @param ... Additional parameters passed to the function
480 function call(name, ...)
481 local argv = {...}
482 return function(...)
483 if #argv > 0 then
484 return getfenv()[name](unpack(argv), ...)
485 else
486 return getfenv()[name](...)
487 end
488 end
489 end
490
491 --- Create a template render dispatching target.
492 -- @param name Template to be rendered
493 function template(name)
494 return function()
495 require("luci.template")
496 luci.template.render(name)
497 end
498 end
499
500 --- Create a CBI model dispatching target.
501 -- @param model CBI model tpo be rendered
502 function cbi(model, config)
503 config = config or {}
504 return function(...)
505 require("luci.cbi")
506 require("luci.template")
507 local http = require "luci.http"
508
509 maps = luci.cbi.load(model, ...)
510
511 local state = nil
512
513 for i, res in ipairs(maps) do
514 if config.autoapply then
515 res.autoapply = config.autoapply
516 end
517 local cstate = res:parse()
518 if not state or cstate < state then
519 state = cstate
520 end
521 end
522
523 http.header("X-CBI-State", state or 0)
524 luci.template.render("cbi/header", {state = state})
525 for i, res in ipairs(maps) do
526 res:render()
527 end
528 luci.template.render("cbi/footer", {state = state, autoapply = config.autoapply})
529 end
530 end
531
532 --- Create a CBI form model dispatching target.
533 -- @param model CBI form model tpo be rendered
534 function form(model)
535 return function(...)
536 require("luci.cbi")
537 require("luci.template")
538 local http = require "luci.http"
539
540 maps = luci.cbi.load(model, ...)
541
542 local state = nil
543
544 for i, res in ipairs(maps) do
545 local cstate = res:parse()
546 if not state or cstate < state then
547 state = cstate
548 end
549 end
550
551 http.header("X-CBI-State", state or 0)
552 luci.template.render("header")
553 for i, res in ipairs(maps) do
554 res:render()
555 end
556 luci.template.render("footer")
557 end
558 end