0876ce658580cf2848f419dd3411638b3f59aaa7
[project/luci.git] / modules / luci-base / luasrc / dispatcher.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
3 -- Licensed to the public under the Apache License 2.0.
4
5 local fs = require "nixio.fs"
6 local sys = require "luci.sys"
7 local util = require "luci.util"
8 local http = require "luci.http"
9 local nixio = require "nixio", require "nixio.util"
10
11 module("luci.dispatcher", package.seeall)
12 context = util.threadlocal()
13 uci = require "luci.model.uci"
14 i18n = require "luci.i18n"
15 _M.fs = fs
16
17 authenticator = {}
18
19 -- Index table
20 local index = nil
21
22 -- Fastindex
23 local fi
24
25
26 function build_url(...)
27         local path = {...}
28         local url = { http.getenv("SCRIPT_NAME") or "" }
29
30         local p
31         for _, p in ipairs(path) do
32                 if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then
33                         url[#url+1] = "/"
34                         url[#url+1] = p
35                 end
36         end
37
38         if #path == 0 then
39                 url[#url+1] = "/"
40         end
41
42         return table.concat(url, "")
43 end
44
45 function node_visible(node)
46    if node then
47           return not (
48                  (not node.title or #node.title == 0) or
49                  (not node.target or node.hidden == true) or
50                  (type(node.target) == "table" and node.target.type == "firstchild" and
51                   (type(node.nodes) ~= "table" or not next(node.nodes)))
52           )
53    end
54    return false
55 end
56
57 function node_childs(node)
58         local rv = { }
59         if node then
60                 local k, v
61                 for k, v in util.spairs(node.nodes,
62                         function(a, b)
63                                 return (node.nodes[a].order or 100)
64                                      < (node.nodes[b].order or 100)
65                         end)
66                 do
67                         if node_visible(v) then
68                                 rv[#rv+1] = k
69                         end
70                 end
71         end
72         return rv
73 end
74
75
76 function error404(message)
77         http.status(404, "Not Found")
78         message = message or "Not Found"
79
80         require("luci.template")
81         if not util.copcall(luci.template.render, "error404") then
82                 http.prepare_content("text/plain")
83                 http.write(message)
84         end
85         return false
86 end
87
88 function error500(message)
89         util.perror(message)
90         if not context.template_header_sent then
91                 http.status(500, "Internal Server Error")
92                 http.prepare_content("text/plain")
93                 http.write(message)
94         else
95                 require("luci.template")
96                 if not util.copcall(luci.template.render, "error500", {message=message}) then
97                         http.prepare_content("text/plain")
98                         http.write(message)
99                 end
100         end
101         return false
102 end
103
104 function authenticator.htmlauth(validator, accs, default)
105         local user = http.formvalue("luci_username")
106         local pass = http.formvalue("luci_password")
107
108         if user and validator(user, pass) then
109                 return user
110         end
111
112         require("luci.i18n")
113         require("luci.template")
114         context.path = {}
115         http.status(403, "Forbidden")
116         luci.template.render("sysauth", {duser=default, fuser=user})
117
118         return false
119
120 end
121
122 function httpdispatch(request, prefix)
123         http.context.request = request
124
125         local r = {}
126         context.request = r
127
128         local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true)
129
130         if prefix then
131                 for _, node in ipairs(prefix) do
132                         r[#r+1] = node
133                 end
134         end
135
136         for node in pathinfo:gmatch("[^/]+") do
137                 r[#r+1] = node
138         end
139
140         local stat, err = util.coxpcall(function()
141                 dispatch(context.request)
142         end, error500)
143
144         http.close()
145
146         --context._disable_memtrace()
147 end
148
149 local function require_post_security(target)
150         if type(target) == "table" then
151                 if type(target.post) == "table" then
152                         local param_name, required_val, request_val
153
154                         for param_name, required_val in pairs(target.post) do
155                                 request_val = http.formvalue(param_name)
156
157                                 if (type(required_val) == "string" and
158                                     request_val ~= required_val) or
159                                    (required_val == true and
160                                     (request_val == nil or request_val == ""))
161                                 then
162                                         return false
163                                 end
164                         end
165
166                         return true
167                 end
168
169                 return (target.post == true)
170         end
171
172         return false
173 end
174
175 function test_post_security()
176         if http.getenv("REQUEST_METHOD") ~= "POST" then
177                 http.status(405, "Method Not Allowed")
178                 http.header("Allow", "POST")
179                 return false
180         end
181
182         if http.formvalue("token") ~= context.authtoken then
183                 http.status(403, "Forbidden")
184                 luci.template.render("csrftoken")
185                 return false
186         end
187
188         return true
189 end
190
191 function dispatch(request)
192         --context._disable_memtrace = require "luci.debug".trap_memtrace("l")
193         local ctx = context
194         ctx.path = request
195
196         local conf = require "luci.config"
197         assert(conf.main,
198                 "/etc/config/luci seems to be corrupt, unable to find section 'main'")
199
200         local i18n = require "luci.i18n"
201         local lang = conf.main.lang or "auto"
202         if lang == "auto" then
203                 local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or ""
204                 for lpat in aclang:gmatch("[%w-]+") do
205                         lpat = lpat and lpat:gsub("-", "_")
206                         if conf.languages[lpat] then
207                                 lang = lpat
208                                 break
209                         end
210                 end
211         end
212         if lang == "auto" then
213                 lang = i18n.default
214         end
215         i18n.setlanguage(lang)
216
217         local c = ctx.tree
218         local stat
219         if not c then
220                 c = createtree()
221         end
222
223         local track = {}
224         local args = {}
225         ctx.args = args
226         ctx.requestargs = ctx.requestargs or args
227         local n
228         local preq = {}
229         local freq = {}
230
231         for i, s in ipairs(request) do
232                 preq[#preq+1] = s
233                 freq[#freq+1] = s
234                 c = c.nodes[s]
235                 n = i
236                 if not c then
237                         break
238                 end
239
240                 util.update(track, c)
241
242                 if c.leaf then
243                         break
244                 end
245         end
246
247         if c and c.leaf then
248                 for j=n+1, #request do
249                         args[#args+1] = request[j]
250                         freq[#freq+1] = request[j]
251                 end
252         end
253
254         ctx.requestpath = ctx.requestpath or freq
255         ctx.path = preq
256
257         if track.i18n then
258                 i18n.loadc(track.i18n)
259         end
260
261         -- Init template engine
262         if (c and c.index) or not track.notemplate then
263                 local tpl = require("luci.template")
264                 local media = track.mediaurlbase or luci.config.main.mediaurlbase
265                 if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then
266                         media = nil
267                         for name, theme in pairs(luci.config.themes) do
268                                 if name:sub(1,1) ~= "." and pcall(tpl.Template,
269                                  "themes/%s/header" % fs.basename(theme)) then
270                                         media = theme
271                                 end
272                         end
273                         assert(media, "No valid theme found")
274                 end
275
276                 local function _ifattr(cond, key, val)
277                         if cond then
278                                 local env = getfenv(3)
279                                 local scope = (type(env.self) == "table") and env.self
280                                 if type(val) == "table" then
281                                         if not next(val) then
282                                                 return ''
283                                         else
284                                                 val = util.serialize_json(val)
285                                         end
286                                 end
287                                 return string.format(
288                                         ' %s="%s"', tostring(key),
289                                         util.pcdata(tostring( val
290                                          or (type(env[key]) ~= "function" and env[key])
291                                          or (scope and type(scope[key]) ~= "function" and scope[key])
292                                          or "" ))
293                                 )
294                         else
295                                 return ''
296                         end
297                 end
298
299                 tpl.context.viewns = setmetatable({
300                    write       = http.write;
301                    include     = function(name) tpl.Template(name):render(getfenv(2)) end;
302                    translate   = i18n.translate;
303                    translatef  = i18n.translatef;
304                    export      = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end;
305                    striptags   = util.striptags;
306                    pcdata      = util.pcdata;
307                    media       = media;
308                    theme       = fs.basename(media);
309                    resource    = luci.config.main.resourcebase;
310                    ifattr      = function(...) return _ifattr(...) end;
311                    attr        = function(...) return _ifattr(true, ...) end;
312                    url         = build_url;
313                 }, {__index=function(table, key)
314                         if key == "controller" then
315                                 return build_url()
316                         elseif key == "REQUEST_URI" then
317                                 return build_url(unpack(ctx.requestpath))
318                         elseif key == "token" then
319                                 return ctx.authtoken
320                         else
321                                 return rawget(table, key) or _G[key]
322                         end
323                 end})
324         end
325
326         track.dependent = (track.dependent ~= false)
327         assert(not track.dependent or not track.auto,
328                 "Access Violation\nThe page at '" .. table.concat(request, "/") .. "/' " ..
329                 "has no parent node so the access to this location has been denied.\n" ..
330                 "This is a software bug, please report this message at " ..
331                 "https://github.com/openwrt/luci/issues"
332         )
333
334         if track.sysauth then
335                 local authen = type(track.sysauth_authenticator) == "function"
336                  and track.sysauth_authenticator
337                  or authenticator[track.sysauth_authenticator]
338
339                 local def  = (type(track.sysauth) == "string") and track.sysauth
340                 local accs = def and {track.sysauth} or track.sysauth
341                 local sess = ctx.authsession
342                 if not sess then
343                         sess = http.getcookie("sysauth")
344                         sess = sess and sess:match("^[a-f0-9]*$")
345                 end
346
347                 local sdat = (util.ubus("session", "get", { ubus_rpc_session = sess }) or { }).values
348                 local user, token
349
350                 if sdat then
351                         user = sdat.user
352                         token = sdat.token
353                 else
354                         local eu = http.getenv("HTTP_AUTH_USER")
355                         local ep = http.getenv("HTTP_AUTH_PASS")
356                         if eu and ep and sys.user.checkpasswd(eu, ep) then
357                                 authen = function() return eu end
358                         end
359                 end
360
361                 if not util.contains(accs, user) then
362                         if authen then
363                                 local user, sess = authen(sys.user.checkpasswd, accs, def)
364                                 local token
365                                 if not user or not util.contains(accs, user) then
366                                         return
367                                 else
368                                         if not sess then
369                                                 local sdat = util.ubus("session", "create", { timeout = tonumber(luci.config.sauth.sessiontime) })
370                                                 if sdat then
371                                                         token = sys.uniqueid(16)
372                                                         util.ubus("session", "set", {
373                                                                 ubus_rpc_session = sdat.ubus_rpc_session,
374                                                                 values = {
375                                                                         user = user,
376                                                                         token = token,
377                                                                         section = sys.uniqueid(16)
378                                                                 }
379                                                         })
380                                                         sess = sdat.ubus_rpc_session
381                                                 end
382                                         end
383
384                                         if sess and token then
385                                                 http.header("Set-Cookie", 'sysauth=%s; path=%s' %{ sess, build_url() })
386
387                                                 ctx.authsession = sess
388                                                 ctx.authtoken = token
389                                                 ctx.authuser = user
390
391                                                 http.redirect(build_url(unpack(ctx.requestpath)))
392                                         end
393                                 end
394                         else
395                                 http.status(403, "Forbidden")
396                                 return
397                         end
398                 else
399                         ctx.authsession = sess
400                         ctx.authtoken = token
401                         ctx.authuser = user
402                 end
403         end
404
405         if c and require_post_security(c.target) then
406                 if not test_post_security(c) then
407                         return
408                 end
409         end
410
411         if track.setgroup then
412                 sys.process.setgroup(track.setgroup)
413         end
414
415         if track.setuser then
416                 sys.process.setuser(track.setuser)
417         end
418
419         local target = nil
420         if c then
421                 if type(c.target) == "function" then
422                         target = c.target
423                 elseif type(c.target) == "table" then
424                         target = c.target.target
425                 end
426         end
427
428         if c and (c.index or type(target) == "function") then
429                 ctx.dispatched = c
430                 ctx.requested = ctx.requested or ctx.dispatched
431         end
432
433         if c and c.index then
434                 local tpl = require "luci.template"
435
436                 if util.copcall(tpl.render, "indexer", {}) then
437                         return true
438                 end
439         end
440
441         if type(target) == "function" then
442                 util.copcall(function()
443                         local oldenv = getfenv(target)
444                         local module = require(c.module)
445                         local env = setmetatable({}, {__index=
446
447                         function(tbl, key)
448                                 return rawget(tbl, key) or module[key] or oldenv[key]
449                         end})
450
451                         setfenv(target, env)
452                 end)
453
454                 local ok, err
455                 if type(c.target) == "table" then
456                         ok, err = util.copcall(target, c.target, unpack(args))
457                 else
458                         ok, err = util.copcall(target, unpack(args))
459                 end
460                 assert(ok,
461                        "Failed to execute " .. (type(c.target) == "function" and "function" or c.target.type or "unknown") ..
462                        " dispatcher target for entry '/" .. table.concat(request, "/") .. "'.\n" ..
463                        "The called action terminated with an exception:\n" .. tostring(err or "(unknown)"))
464         else
465                 local root = node()
466                 if not root or not root.target then
467                         error404("No root node was registered, this usually happens if no module was installed.\n" ..
468                                  "Install luci-mod-admin-full and retry. " ..
469                                  "If the module is already installed, try removing the /tmp/luci-indexcache file.")
470                 else
471                         error404("No page is registered at '/" .. table.concat(request, "/") .. "'.\n" ..
472                                  "If this url belongs to an extension, make sure it is properly installed.\n" ..
473                                  "If the extension was recently installed, try removing the /tmp/luci-indexcache file.")
474                 end
475         end
476 end
477
478 function createindex()
479         local controllers = { }
480         local base = "%s/controller/" % util.libpath()
481         local _, path
482
483         for path in (fs.glob("%s*.lua" % base) or function() end) do
484                 controllers[#controllers+1] = path
485         end
486
487         for path in (fs.glob("%s*/*.lua" % base) or function() end) do
488                 controllers[#controllers+1] = path
489         end
490
491         if indexcache then
492                 local cachedate = fs.stat(indexcache, "mtime")
493                 if cachedate then
494                         local realdate = 0
495                         for _, obj in ipairs(controllers) do
496                                 local omtime = fs.stat(obj, "mtime")
497                                 realdate = (omtime and omtime > realdate) and omtime or realdate
498                         end
499
500                         if cachedate > realdate and sys.process.info("uid") == 0 then
501                                 assert(
502                                         sys.process.info("uid") == fs.stat(indexcache, "uid")
503                                         and fs.stat(indexcache, "modestr") == "rw-------",
504                                         "Fatal: Indexcache is not sane!"
505                                 )
506
507                                 index = loadfile(indexcache)()
508                                 return index
509                         end
510                 end
511         end
512
513         index = {}
514
515         for _, path in ipairs(controllers) do
516                 local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".")
517                 local mod = require(modname)
518                 assert(mod ~= true,
519                        "Invalid controller file found\n" ..
520                        "The file '" .. path .. "' contains an invalid module line.\n" ..
521                        "Please verify whether the module name is set to '" .. modname ..
522                        "' - It must correspond to the file path!")
523
524                 local idx = mod.index
525                 assert(type(idx) == "function",
526                        "Invalid controller file found\n" ..
527                        "The file '" .. path .. "' contains no index() function.\n" ..
528                        "Please make sure that the controller contains a valid " ..
529                        "index function and verify the spelling!")
530
531                 index[modname] = idx
532         end
533
534         if indexcache then
535                 local f = nixio.open(indexcache, "w", 600)
536                 f:writeall(util.get_bytecode(index))
537                 f:close()
538         end
539 end
540
541 -- Build the index before if it does not exist yet.
542 function createtree()
543         if not index then
544                 createindex()
545         end
546
547         local ctx  = context
548         local tree = {nodes={}, inreq=true}
549         local modi = {}
550
551         ctx.treecache = setmetatable({}, {__mode="v"})
552         ctx.tree = tree
553         ctx.modifiers = modi
554
555         -- Load default translation
556         require "luci.i18n".loadc("base")
557
558         local scope = setmetatable({}, {__index = luci.dispatcher})
559
560         for k, v in pairs(index) do
561                 scope._NAME = k
562                 setfenv(v, scope)
563                 v()
564         end
565
566         local function modisort(a,b)
567                 return modi[a].order < modi[b].order
568         end
569
570         for _, v in util.spairs(modi, modisort) do
571                 scope._NAME = v.module
572                 setfenv(v.func, scope)
573                 v.func()
574         end
575
576         return tree
577 end
578
579 function modifier(func, order)
580         context.modifiers[#context.modifiers+1] = {
581                 func = func,
582                 order = order or 0,
583                 module
584                         = getfenv(2)._NAME
585         }
586 end
587
588 function assign(path, clone, title, order)
589         local obj  = node(unpack(path))
590         obj.nodes  = nil
591         obj.module = nil
592
593         obj.title = title
594         obj.order = order
595
596         setmetatable(obj, {__index = _create_node(clone)})
597
598         return obj
599 end
600
601 function entry(path, target, title, order)
602         local c = node(unpack(path))
603
604         c.target = target
605         c.title  = title
606         c.order  = order
607         c.module = getfenv(2)._NAME
608
609         return c
610 end
611
612 -- enabling the node.
613 function get(...)
614         return _create_node({...})
615 end
616
617 function node(...)
618         local c = _create_node({...})
619
620         c.module = getfenv(2)._NAME
621         c.auto = nil
622
623         return c
624 end
625
626 function _create_node(path)
627         if #path == 0 then
628                 return context.tree
629         end
630
631         local name = table.concat(path, ".")
632         local c = context.treecache[name]
633
634         if not c then
635                 local last = table.remove(path)
636                 local parent = _create_node(path)
637
638                 c = {nodes={}, auto=true}
639                 -- the node is "in request" if the request path matches
640                 -- at least up to the length of the node path
641                 if parent.inreq and context.path[#path+1] == last then
642                   c.inreq = true
643                 end
644                 parent.nodes[last] = c
645                 context.treecache[name] = c
646         end
647         return c
648 end
649
650 -- Subdispatchers --
651
652 function _firstchild()
653    local path = { unpack(context.path) }
654    local name = table.concat(path, ".")
655    local node = context.treecache[name]
656
657    local lowest
658    if node and node.nodes and next(node.nodes) then
659           local k, v
660           for k, v in pairs(node.nodes) do
661                  if not lowest or
662                         (v.order or 100) < (node.nodes[lowest].order or 100)
663                  then
664                         lowest = k
665                  end
666           end
667    end
668
669    assert(lowest ~= nil,
670                   "The requested node contains no childs, unable to redispatch")
671
672    path[#path+1] = lowest
673    dispatch(path)
674 end
675
676 function firstchild()
677    return { type = "firstchild", target = _firstchild }
678 end
679
680 function alias(...)
681         local req = {...}
682         return function(...)
683                 for _, r in ipairs({...}) do
684                         req[#req+1] = r
685                 end
686
687                 dispatch(req)
688         end
689 end
690
691 function rewrite(n, ...)
692         local req = {...}
693         return function(...)
694                 local dispatched = util.clone(context.dispatched)
695
696                 for i=1,n do
697                         table.remove(dispatched, 1)
698                 end
699
700                 for i, r in ipairs(req) do
701                         table.insert(dispatched, i, r)
702                 end
703
704                 for _, r in ipairs({...}) do
705                         dispatched[#dispatched+1] = r
706                 end
707
708                 dispatch(dispatched)
709         end
710 end
711
712
713 local function _call(self, ...)
714         local func = getfenv()[self.name]
715         assert(func ~= nil,
716                'Cannot resolve function "' .. self.name .. '". Is it misspelled or local?')
717
718         assert(type(func) == "function",
719                'The symbol "' .. self.name .. '" does not refer to a function but data ' ..
720                'of type "' .. type(func) .. '".')
721
722         if #self.argv > 0 then
723                 return func(unpack(self.argv), ...)
724         else
725                 return func(...)
726         end
727 end
728
729 function call(name, ...)
730         return {type = "call", argv = {...}, name = name, target = _call}
731 end
732
733 function post_on(params, name, ...)
734         return {
735                 type = "call",
736                 post = params,
737                 argv = { ... },
738                 name = name,
739                 target = _call
740         }
741 end
742
743 function post(...)
744         return post_on(true, ...)
745 end
746
747
748 local _template = function(self, ...)
749         require "luci.template".render(self.view)
750 end
751
752 function template(name)
753         return {type = "template", view = name, target = _template}
754 end
755
756
757 local function _cbi(self, ...)
758         local cbi = require "luci.cbi"
759         local tpl = require "luci.template"
760         local http = require "luci.http"
761
762         local config = self.config or {}
763         local maps = cbi.load(self.model, ...)
764
765         local state = nil
766
767         for i, res in ipairs(maps) do
768                 res.flow = config
769                 local cstate = res:parse()
770                 if cstate and (not state or cstate < state) then
771                         state = cstate
772                 end
773         end
774
775         local function _resolve_path(path)
776                 return type(path) == "table" and build_url(unpack(path)) or path
777         end
778
779         if config.on_valid_to and state and state > 0 and state < 2 then
780                 http.redirect(_resolve_path(config.on_valid_to))
781                 return
782         end
783
784         if config.on_changed_to and state and state > 1 then
785                 http.redirect(_resolve_path(config.on_changed_to))
786                 return
787         end
788
789         if config.on_success_to and state and state > 0 then
790                 http.redirect(_resolve_path(config.on_success_to))
791                 return
792         end
793
794         if config.state_handler then
795                 if not config.state_handler(state, maps) then
796                         return
797                 end
798         end
799
800         http.header("X-CBI-State", state or 0)
801
802         if not config.noheader then
803                 tpl.render("cbi/header", {state = state})
804         end
805
806         local redirect
807         local messages
808         local applymap   = false
809         local pageaction = true
810         local parsechain = { }
811
812         for i, res in ipairs(maps) do
813                 if res.apply_needed and res.parsechain then
814                         local c
815                         for _, c in ipairs(res.parsechain) do
816                                 parsechain[#parsechain+1] = c
817                         end
818                         applymap = true
819                 end
820
821                 if res.redirect then
822                         redirect = redirect or res.redirect
823                 end
824
825                 if res.pageaction == false then
826                         pageaction = false
827                 end
828
829                 if res.message then
830                         messages = messages or { }
831                         messages[#messages+1] = res.message
832                 end
833         end
834
835         for i, res in ipairs(maps) do
836                 res:render({
837                         firstmap   = (i == 1),
838                         applymap   = applymap,
839                         redirect   = redirect,
840                         messages   = messages,
841                         pageaction = pageaction,
842                         parsechain = parsechain
843                 })
844         end
845
846         if not config.nofooter then
847                 tpl.render("cbi/footer", {
848                         flow       = config,
849                         pageaction = pageaction,
850                         redirect   = redirect,
851                         state      = state,
852                         autoapply  = config.autoapply
853                 })
854         end
855 end
856
857 function cbi(model, config)
858         return {
859                 type = "cbi",
860                 post = { ["cbi.submit"] = "1" },
861                 config = config,
862                 model = model,
863                 target = _cbi
864         }
865 end
866
867
868 local function _arcombine(self, ...)
869         local argv = {...}
870         local target = #argv > 0 and self.targets[2] or self.targets[1]
871         setfenv(target.target, self.env)
872         target:target(unpack(argv))
873 end
874
875 function arcombine(trg1, trg2)
876         return {type = "arcombine", env = getfenv(), target = _arcombine, targets = {trg1, trg2}}
877 end
878
879
880 local function _form(self, ...)
881         local cbi = require "luci.cbi"
882         local tpl = require "luci.template"
883         local http = require "luci.http"
884
885         local maps = luci.cbi.load(self.model, ...)
886         local state = nil
887
888         for i, res in ipairs(maps) do
889                 local cstate = res:parse()
890                 if cstate and (not state or cstate < state) then
891                         state = cstate
892                 end
893         end
894
895         http.header("X-CBI-State", state or 0)
896         tpl.render("header")
897         for i, res in ipairs(maps) do
898                 res:render()
899         end
900         tpl.render("footer")
901 end
902
903 function form(model)
904         return {
905                 type = "cbi",
906                 post = { ["cbi.submit"] = "1" },
907                 model = model,
908                 target = _form
909         }
910 end
911
912 translate = i18n.translate
913
914 -- This function does not actually translate the given argument but
915 -- is used by build/i18n-scan.pl to find translatable entries.
916 function _(text)
917         return text
918 end