root/luci/trunk/libs/web/luasrc/dispatcher.lua @ 3684

Revision 3684, 12.9 KB (checked in by Cyrus, 5 years ago)

Merge pageactions

  • Property svn:keywords set to Id
Line 
1--[[
2LuCI - Dispatcher
3
4Description:
5The request dispatcher and module dispatcher generators
6
7FileId:
8$Id$
9
10License:
11Copyright 2008 Steven Barth <steven@midlink.org>
12
13Licensed under the Apache License, Version 2.0 (the "License");
14you may not use this file except in compliance with the License.
15You may obtain a copy of the License at
16
17    http://www.apache.org/licenses/LICENSE-2.0
18
19Unless required by applicable law or agreed to in writing, software
20distributed under the License is distributed on an "AS IS" BASIS,
21WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22See the License for the specific language governing permissions and
23limitations under the License.
24
25]]--
26
27--- LuCI web dispatcher.
28local fs = require "luci.fs"
29local sys = require "luci.sys"
30local init = require "luci.init"
31local util = require "luci.util"
32local http = require "luci.http"
33
34module("luci.dispatcher", package.seeall)
35context = luci.util.threadlocal()
36
37authenticator = {}
38
39-- Index table
40local index = nil
41
42-- Fastindex
43local fi
44
45
46--- Build the URL relative to the server webroot from given virtual path.
47-- @param ...   Virtual path
48-- @return      Relative URL
49function build_url(...)
50    return luci.http.getenv("SCRIPT_NAME") .. "/" .. table.concat(arg, "/")
51end
52
53--- Send a 404 error code and render the "error404" template if available.
54-- @param message   Custom error message (optional)
55-- @return          false
56function 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
66end
67
68--- Send a 500 error code and render the "error500" template if available.
69-- @param message   Custom error message (optional)#
70-- @return          false
71function 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
80end
81
82function 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
96end
97
98--- Dispatch an HTTP request.
99-- @param request   LuCI HTTP Request object
100function 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        luci.util.perror(err)
112        error500(err)
113    end
114
115    luci.http.close()
116
117    --context._disable_memtrace()
118end
119
120--- Dispatches a LuCI virtual path.
121-- @param request   Virtual path
122function dispatch(request)
123    --context._disable_memtrace = require "luci.debug".trap_memtrace()
124    local ctx = context
125    ctx.path = request
126
127    require "luci.i18n".setlanguage(require "luci.config".main.lang)
128
129    local c = ctx.tree
130    local stat
131    if not c then
132        c = createtree()
133    end
134
135    local track = {}
136    local args = {}
137    context.args = args
138    local n
139
140    for i, s in ipairs(request) do
141        c = c.nodes[s]
142        n = i
143        if not c then
144            break
145        end
146
147        util.update(track, c)
148
149        if c.leaf then
150            break
151        end
152    end
153
154    if c and c.leaf then
155        for j=n+1, #request do
156            table.insert(args, request[j])
157        end
158    end
159
160    if track.i18n then
161        require("luci.i18n").loadc(track.i18n)
162    end
163
164    -- Init template engine
165    if (c and c.index) or not track.notemplate then
166        local tpl = require("luci.template")
167        local media = track.mediaurlbase or luci.config.main.mediaurlbase
168        if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
169            media = nil
170            for name, theme in pairs(luci.config.themes) do
171                if name:sub(1,1) ~= "." and pcall(tpl.Template,
172                 "themes/%s/header" % fs.basename(theme)) then
173                    media = theme
174                end
175            end
176            assert(media, "No valid theme found")
177        end
178
179        local viewns = setmetatable({}, {__index=_G})
180        tpl.context.viewns = viewns
181        viewns.write       = luci.http.write
182        viewns.include     = function(name) tpl.Template(name):render(getfenv(2)) end
183        viewns.translate   = function(...) return require("luci.i18n").translate(...) end
184        viewns.striptags   = util.striptags
185        viewns.controller  = luci.http.getenv("SCRIPT_NAME")
186        viewns.media       = media
187        viewns.theme       = fs.basename(media)
188        viewns.resource    = luci.config.main.resourcebase
189        viewns.REQUEST_URI = (luci.http.getenv("SCRIPT_NAME") or "") .. (luci.http.getenv("PATH_INFO") or "")
190    end
191
192    track.dependent = (track.dependent ~= false)
193    assert(not track.dependent or not track.auto, "Access Violation")
194
195    if track.sysauth then
196        local sauth = require "luci.sauth"
197
198        local authen = type(track.sysauth_authenticator) == "function"
199         and track.sysauth_authenticator
200         or authenticator[track.sysauth_authenticator]
201
202        local def  = (type(track.sysauth) == "string") and track.sysauth
203        local accs = def and {track.sysauth} or track.sysauth
204        local sess = ctx.authsession or luci.http.getcookie("sysauth")
205        sess = sess and sess:match("^[A-F0-9]+$")
206        local user = sauth.read(sess)
207
208        if not util.contains(accs, user) then
209            if authen then
210                local user, sess = authen(luci.sys.user.checkpasswd, accs, def)
211                if not user or not util.contains(accs, user) then
212                    return
213                else
214                    local sid = sess or luci.sys.uniqueid(16)
215                    luci.http.header("Set-Cookie", "sysauth=" .. sid.."; path=/")
216                    if not sess then
217                        sauth.write(sid, user)
218                    end
219                    ctx.authsession = sid
220                end
221            else
222                luci.http.status(403, "Forbidden")
223                return
224            end
225        end
226    end
227
228    if track.setgroup then
229        luci.sys.process.setgroup(track.setgroup)
230    end
231
232    if track.setuser then
233        luci.sys.process.setuser(track.setuser)
234    end
235
236    if c and (c.index or type(c.target) == "function") then
237        ctx.dispatched = c
238        ctx.requested = ctx.requested or ctx.dispatched
239    end
240
241    if c and c.index then
242        local tpl = require "luci.template"
243
244        if util.copcall(tpl.render, "indexer", {}) then
245            return true
246        end
247    end
248
249    if c and type(c.target) == "function" then
250        util.copcall(function()
251            local oldenv = getfenv(c.target)
252            local module = require(c.module)
253            local env = setmetatable({}, {__index=
254
255            function(tbl, key)
256                return rawget(tbl, key) or module[key] or oldenv[key]
257            end})
258
259            setfenv(c.target, env)
260        end)
261
262        c.target(unpack(args))
263    else
264        error404()
265    end
266end
267
268--- Generate the dispatching index using the best possible strategy.
269function createindex()
270    local path = luci.util.libpath() .. "/controller/"
271    local suff = ".lua"
272
273    if luci.util.copcall(require, "luci.fastindex") then
274        createindex_fastindex(path, suff)
275    else
276        createindex_plain(path, suff)
277    end
278end
279
280--- Generate the dispatching index using the fastindex C-indexer.
281-- @param path      Controller base directory
282-- @param suffix    Controller file suffix
283function createindex_fastindex(path, suffix)
284    index = {}
285
286    if not fi then
287        fi = luci.fastindex.new("index")
288        fi.add(path .. "*" .. suffix)
289        fi.add(path .. "*/*" .. suffix)
290    end
291    fi.scan()
292
293    for k, v in pairs(fi.indexes) do
294        index[v[2]] = v[1]
295    end
296end
297
298--- Generate the dispatching index using the native file-cache based strategy.
299-- @param path      Controller base directory
300-- @param suffix    Controller file suffix
301function createindex_plain(path, suffix)
302    if indexcache then
303        local cachedate = fs.mtime(indexcache)
304        if cachedate and cachedate > fs.mtime(path) then
305
306            assert(
307                sys.process.info("uid") == fs.stat(indexcache, "uid")
308                and fs.stat(indexcache, "mode") == "rw-------",
309                "Fatal: Indexcache is not sane!"
310            )
311
312            index = loadfile(indexcache)()
313            return index
314        end
315    end
316
317    index = {}
318
319    local controllers = util.combine(
320        luci.fs.glob(path .. "*" .. suffix) or {},
321        luci.fs.glob(path .. "*/*" .. suffix) or {}
322    )
323
324    for i,c in ipairs(controllers) do
325        local module = "luci.controller." .. c:sub(#path+1, #c-#suffix):gsub("/", ".")
326        local mod = require(module)
327        local idx = mod.index
328
329        if type(idx) == "function" then
330            index[module] = idx
331        end
332    end
333
334    if indexcache then
335        fs.writefile(indexcache, util.get_bytecode(index))
336        fs.chmod(indexcache, "a-rwx,u+rw")
337    end
338end
339
340--- Create the dispatching tree from the index.
341-- Build the index before if it does not exist yet.
342function createtree()
343    if not index then
344        createindex()
345    end
346
347    local ctx  = context
348    local tree = {nodes={}}
349
350    ctx.treecache = setmetatable({}, {__mode="v"})
351    ctx.tree = tree
352
353    -- Load default translation
354    require "luci.i18n".loadc("default")
355
356    local scope = setmetatable({}, {__index = luci.dispatcher})
357
358    for k, v in pairs(index) do
359        scope._NAME = k
360        setfenv(v, scope)
361        v()
362    end
363
364    return tree
365end
366
367--- Clone a node of the dispatching tree to another position.
368-- @param   path    Virtual path destination
369-- @param   clone   Virtual path source
370-- @param   title   Destination node title (optional)
371-- @param   order   Destination node order value (optional)
372-- @return          Dispatching tree node
373function assign(path, clone, title, order)
374    local obj  = node(unpack(path))
375    obj.nodes  = nil
376    obj.module = nil
377
378    obj.title = title
379    obj.order = order
380
381    setmetatable(obj, {__index = _create_node(clone)})
382
383    return obj
384end
385
386--- Create a new dispatching node and define common parameters.
387-- @param   path    Virtual path
388-- @param   target  Target function to call when dispatched.
389-- @param   title   Destination node title
390-- @param   order   Destination node order value (optional)
391-- @return          Dispatching tree node
392function entry(path, target, title, order)
393    local c = node(unpack(path))
394
395    c.target = target
396    c.title  = title
397    c.order  = order
398    c.module = getfenv(2)._NAME
399
400    return c
401end
402
403--- Fetch or create a new dispatching node.
404-- @param   ...     Virtual path
405-- @return          Dispatching tree node
406function node(...)
407    local c = _create_node({...})
408
409    c.module = getfenv(2)._NAME
410    c.path = arg
411    c.auto = nil
412
413    return c
414end
415
416function _create_node(path, cache)
417    if #path == 0 then
418        return context.tree
419    end
420
421    cache = cache or context.treecache
422    local name = table.concat(path, ".")
423    local c = cache[name]
424
425    if not c then
426        local last = table.remove(path)
427        c = _create_node(path, cache)
428
429        local new = {nodes={}, auto=true}
430        c.nodes[last] = new
431        cache[name] = new
432
433        return new
434    else
435        return c
436    end
437end
438
439-- Subdispatchers --
440
441--- Create a redirect to another dispatching node.
442-- @param   ...     Virtual path destination
443function alias(...)
444    local req = {...}
445    return function(...)
446        for _, r in ipairs({...}) do
447            req[#req+1] = r
448        end
449
450        dispatch(req)
451    end
452end
453
454--- Rewrite the first x path values of the request.
455-- @param   n       Number of path values to replace
456-- @param   ...     Virtual path to replace removed path values with
457function rewrite(n, ...)
458    local req = {...}
459    return function(...)
460        local dispatched = util.clone(context.dispatched)
461
462        for i=1,n do
463            table.remove(dispatched, 1)
464        end
465
466        for i, r in ipairs(req) do
467            table.insert(dispatched, i, r)
468        end
469
470        for _, r in ipairs({...}) do
471            dispatched[#dispatched+1] = r
472        end
473
474        dispatch(dispatched)
475    end
476end
477
478--- Create a function-call dispatching target.
479-- @param   name    Target function of local controller
480-- @param   ...     Additional parameters passed to the function
481function call(name, ...)
482    local argv = {...}
483    return function(...)
484        if #argv > 0 then 
485            return getfenv()[name](unpack(argv), ...)
486        else
487            return getfenv()[name](...)
488        end
489    end
490end
491
492--- Create a template render dispatching target.
493-- @param   name    Template to be rendered
494function template(name)
495    return function()
496        require("luci.template")
497        luci.template.render(name)
498    end
499end
500
501--- Create a CBI model dispatching target.
502-- @param   model   CBI model to be rendered
503function cbi(model, config)
504    config = config or {}
505    return function(...)
506        require("luci.cbi")
507        require("luci.template")
508        local http = require "luci.http"
509
510        maps = luci.cbi.load(model, ...)
511
512        local state = nil
513
514        for i, res in ipairs(maps) do
515            if config.autoapply then
516                res.autoapply = config.autoapply
517            end
518            local cstate = res:parse()
519            if not state or cstate < state then
520                state = cstate
521            end
522        end
523
524        local pageaction = true
525        http.header("X-CBI-State", state or 0)
526        luci.template.render("cbi/header", {state = state})
527        for i, res in ipairs(maps) do
528            res:render()
529            if res.pageaction == false then
530                pageaction = false
531            end
532        end
533        luci.template.render("cbi/footer", {pageaction=pageaction, state = state, autoapply = config.autoapply})
534    end
535end
536
537--- Create a CBI form model dispatching target.
538-- @param   model   CBI form model tpo be rendered
539function form(model)
540    return function(...)
541        require("luci.cbi")
542        require("luci.template")
543        local http = require "luci.http"
544
545        maps = luci.cbi.load(model, ...)
546
547        local state = nil
548
549        for i, res in ipairs(maps) do
550            local cstate = res:parse()
551            if not state or cstate < state then
552                state = cstate
553            end
554        end
555
556        http.header("X-CBI-State", state or 0)
557        luci.template.render("header")
558        for i, res in ipairs(maps) do
559            res:render()
560        end
561        luci.template.render("footer")
562    end
563end
Note: See TracBrowser for help on using the browser.