Merge pull request #1524 from dibdot/travelmate
[project/luci.git] / modules / luci-base / luasrc / http.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Licensed to the public under the Apache License 2.0.
3
4 local ltn12 = require "luci.ltn12"
5 local protocol = require "luci.http.protocol"
6 local util  = require "luci.util"
7 local string = require "string"
8 local coroutine = require "coroutine"
9 local table = require "table"
10
11 local ipairs, pairs, next, type, tostring, error =
12         ipairs, pairs, next, type, tostring, error
13
14 module "luci.http"
15
16 context = util.threadlocal()
17
18 Request = util.class()
19 function Request.__init__(self, env, sourcein, sinkerr)
20         self.input = sourcein
21         self.error = sinkerr
22
23
24         -- File handler nil by default to let .content() work
25         self.filehandler = nil
26
27         -- HTTP-Message table
28         self.message = {
29                 env = env,
30                 headers = {},
31                 params = protocol.urldecode_params(env.QUERY_STRING or ""),
32         }
33
34         self.parsed_input = false
35 end
36
37 function Request.formvalue(self, name, noparse)
38         if not noparse and not self.parsed_input then
39                 self:_parse_input()
40         end
41
42         if name then
43                 return self.message.params[name]
44         else
45                 return self.message.params
46         end
47 end
48
49 function Request.formvaluetable(self, prefix)
50         local vals = {}
51         prefix = prefix and prefix .. "." or "."
52
53         if not self.parsed_input then
54                 self:_parse_input()
55         end
56
57         local void = self.message.params[nil]
58         for k, v in pairs(self.message.params) do
59                 if k:find(prefix, 1, true) == 1 then
60                         vals[k:sub(#prefix + 1)] = tostring(v)
61                 end
62         end
63
64         return vals
65 end
66
67 function Request.content(self)
68         if not self.parsed_input then
69                 self:_parse_input()
70         end
71
72         return self.message.content, self.message.content_length
73 end
74
75 function Request.getcookie(self, name)
76   local c = string.gsub(";" .. (self:getenv("HTTP_COOKIE") or "") .. ";", "%s*;%s*", ";")
77   local p = ";" .. name .. "=(.-);"
78   local i, j, value = c:find(p)
79   return value and urldecode(value)
80 end
81
82 function Request.getenv(self, name)
83         if name then
84                 return self.message.env[name]
85         else
86                 return self.message.env
87         end
88 end
89
90 function Request.setfilehandler(self, callback)
91         self.filehandler = callback
92
93         -- If input has already been parsed then any files are either in temporary files
94         -- or are in self.message.params[key]
95         if self.parsed_input then
96                 for param, value in pairs(self.message.params) do
97                 repeat
98                         -- We're only interested in files
99                         if (not value["file"]) then break end
100                         -- If we were able to write to temporary file
101                         if (value["fd"]) then 
102                                 fd = value["fd"]
103                                 local eof = false
104                                 repeat  
105                                         filedata = fd:read(1024)
106                                         if (filedata:len() < 1024) then
107                                                 eof = true
108                                         end
109                                         callback({ name=value["name"], file=value["file"] }, filedata, eof)
110                                 until (eof)
111                                 fd:close()
112                                 value["fd"] = nil
113                         -- We had to read into memory
114                         else
115                                 -- There should only be one numbered value in table - the data
116                                 for k, v in ipairs(value) do
117                                         callback({ name=value["name"], file=value["file"] }, v, true)
118                                 end
119                         end
120                 until true
121                 end
122         end
123 end
124
125 function Request._parse_input(self)
126         protocol.parse_message_body(
127                  self.input,
128                  self.message,
129                  self.filehandler
130         )
131         self.parsed_input = true
132 end
133
134 function close()
135         if not context.eoh then
136                 context.eoh = true
137                 coroutine.yield(3)
138         end
139
140         if not context.closed then
141                 context.closed = true
142                 coroutine.yield(5)
143         end
144 end
145
146 function content()
147         return context.request:content()
148 end
149
150 function formvalue(name, noparse)
151         return context.request:formvalue(name, noparse)
152 end
153
154 function formvaluetable(prefix)
155         return context.request:formvaluetable(prefix)
156 end
157
158 function getcookie(name)
159         return context.request:getcookie(name)
160 end
161
162 -- or the environment table itself.
163 function getenv(name)
164         return context.request:getenv(name)
165 end
166
167 function setfilehandler(callback)
168         return context.request:setfilehandler(callback)
169 end
170
171 function header(key, value)
172         if not context.headers then
173                 context.headers = {}
174         end
175         context.headers[key:lower()] = value
176         coroutine.yield(2, key, value)
177 end
178
179 function prepare_content(mime)
180         if not context.headers or not context.headers["content-type"] then
181                 if mime == "application/xhtml+xml" then
182                         if not getenv("HTTP_ACCEPT") or
183                           not getenv("HTTP_ACCEPT"):find("application/xhtml+xml", nil, true) then
184                                 mime = "text/html; charset=UTF-8"
185                         end
186                         header("Vary", "Accept")
187                 end
188                 header("Content-Type", mime)
189         end
190 end
191
192 function source()
193         return context.request.input
194 end
195
196 function status(code, message)
197         code = code or 200
198         message = message or "OK"
199         context.status = code
200         coroutine.yield(1, code, message)
201 end
202
203 -- This function is as a valid LTN12 sink.
204 -- If the content chunk is nil this function will automatically invoke close.
205 function write(content, src_err)
206         if not content then
207                 if src_err then
208                         error(src_err)
209                 else
210                         close()
211                 end
212                 return true
213         elseif #content == 0 then
214                 return true
215         else
216                 if not context.eoh then
217                         if not context.status then
218                                 status()
219                         end
220                         if not context.headers or not context.headers["content-type"] then
221                                 header("Content-Type", "text/html; charset=utf-8")
222                         end
223                         if not context.headers["cache-control"] then
224                                 header("Cache-Control", "no-cache")
225                                 header("Expires", "0")
226                         end
227                         if not context.headers["x-frame-options"] then
228                                 header("X-Frame-Options", "SAMEORIGIN")
229                         end
230                         if not context.headers["x-xss-protection"] then
231                                 header("X-XSS-Protection", "1; mode=block")
232                         end
233                         if not context.headers["x-content-type-options"] then
234                                 header("X-Content-Type-Options", "nosniff")
235                         end
236
237                         context.eoh = true
238                         coroutine.yield(3)
239                 end
240                 coroutine.yield(4, content)
241                 return true
242         end
243 end
244
245 function splice(fd, size)
246         coroutine.yield(6, fd, size)
247 end
248
249 function redirect(url)
250         if url == "" then url = "/" end
251         status(302, "Found")
252         header("Location", url)
253         close()
254 end
255
256 function build_querystring(q)
257         local s = { "?" }
258
259         for k, v in pairs(q) do
260                 if #s > 1 then s[#s+1] = "&" end
261
262                 s[#s+1] = urldecode(k)
263                 s[#s+1] = "="
264                 s[#s+1] = urldecode(v)
265         end
266
267         return table.concat(s, "")
268 end
269
270 urldecode = protocol.urldecode
271
272 urlencode = protocol.urlencode
273
274 function write_json(x)
275         util.serialize_json(x, write)
276 end