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