libs/web: transparently handle userdata in write_json()
[project/luci.git] / libs / web / luasrc / http.lua
index f2c36607398821eb50636cdc0eb1759a2e69ad21..18112507ce3565b6e7643ab8060fa61636282203 100644 (file)
@@ -7,17 +7,14 @@ HTTP-Header manipulator and form variable preprocessor
 FileId:
 $Id$
 
 FileId:
 $Id$
 
-ToDo:
-- Cookie handling
-
 License:
 Copyright 2008 Steven Barth <steven@midlink.org>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 License:
 Copyright 2008 Steven Barth <steven@midlink.org>
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
-You may obtain a copy of the License at 
+You may obtain a copy of the License at
 
 
-       http://www.apache.org/licenses/LICENSE-2.0 
+       http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
@@ -27,51 +24,47 @@ limitations under the License.
 
 ]]--
 
 
 ]]--
 
-module("luci.http", package.seeall)
-require("luci.http.protocol")
-require("luci.util")
+local ltn12 = require "luci.ltn12"
+local protocol = require "luci.http.protocol"
+local util  = require "luci.util"
+local string = require "string"
+local coroutine = require "coroutine"
+local table = require "table"
+
+local ipairs, pairs, next, type, tostring, error =
+       ipairs, pairs, next, type, tostring, error
+
+--- LuCI Web Framework high-level HTTP functions.
+module "luci.http"
 
 
-context = luci.util.threadlocal()
+context = util.threadlocal()
 
 
+Request = util.class()
+function Request.__init__(self, env, sourcein, sinkerr)
+       self.input = sourcein
+       self.error = sinkerr
 
 
-Request = luci.util.class()
-function Request.__init__(self, env, instream, errstream)
-       self.input = instream
-       self.error = errstream
-       
-       -- Provide readline function
-       self.inputreader = self.input.readline
-        or self.input.read and function() return self.input:read() end
-        or self.input.receive and function() return self.input:receive() end
-        or function() return nil end
 
        -- File handler
        self.filehandler = function() end
 
        -- File handler
        self.filehandler = function() end
-       
+
        -- HTTP-Message table
        self.message = {
                env = env,
                headers = {},
        -- HTTP-Message table
        self.message = {
                env = env,
                headers = {},
-               params = luci.http.protocol.urldecode_params("?"..(env.QUERY_STRING or "")),
+               params = protocol.urldecode_params(env.QUERY_STRING or ""),
        }
        }
-       
-       setmetatable(self.message.params, {__index =
-               function(tbl, key)
-                       luci.http.protocol.parse_message_body(
-                        self.inputreader,
-                        self.message,
-                        self.filehandler
-                       )
-                       
-                       setmetatable(tbl, nil)
-                       return rawget(tbl, key)
-               end
-       })
+
+       self.parsed_input = false
 end
 
 end
 
-function Request.formvalue(self, name, default)
+function Request.formvalue(self, name, noparse)
+       if not noparse and not self.parsed_input then
+               self:_parse_input()
+       end
+
        if name then
        if name then
-               return self.message.params[name] and tostring(self.message.params[name]) or default
+               return self.message.params[name]
        else
                return self.message.params
        end
        else
                return self.message.params
        end
@@ -80,66 +73,117 @@ end
 function Request.formvaluetable(self, prefix)
        local vals = {}
        prefix = prefix and prefix .. "." or "."
 function Request.formvaluetable(self, prefix)
        local vals = {}
        prefix = prefix and prefix .. "." or "."
-       
+
+       if not self.parsed_input then
+               self:_parse_input()
+       end
+
        local void = self.message.params[nil]
        for k, v in pairs(self.message.params) do
                if k:find(prefix, 1, true) == 1 then
                        vals[k:sub(#prefix + 1)] = tostring(v)
                end
        end
        local void = self.message.params[nil]
        for k, v in pairs(self.message.params) do
                if k:find(prefix, 1, true) == 1 then
                        vals[k:sub(#prefix + 1)] = tostring(v)
                end
        end
-       
+
        return vals
 end
 
        return vals
 end
 
+function Request.content(self)
+       if not self.parsed_input then
+               self:_parse_input()
+       end
+
+       return self.message.content, self.message.content_length
+end
+
+function Request.getcookie(self, name)
+  local c = string.gsub(";" .. (self:getenv("HTTP_COOKIE") or "") .. ";", "%s*;%s*", ";")
+  local p = ";" .. name .. "=(.-);"
+  local i, j, value = c:find(p)
+  return value and urldecode(value)
+end
+
 function Request.getenv(self, name)
 function Request.getenv(self, name)
-       return name and self.message.env[name] or self.message.env
+       if name then
+               return self.message.env[name]
+       else
+               return self.message.env
+       end
 end
 
 function Request.setfilehandler(self, callback)
        self.filehandler = callback
 end
 
 end
 
 function Request.setfilehandler(self, callback)
        self.filehandler = callback
 end
 
+function Request._parse_input(self)
+       protocol.parse_message_body(
+                self.input,
+                self.message,
+                self.filehandler
+       )
+       self.parsed_input = true
+end
 
 
+--- Close the HTTP-Connection.
 function close()
        if not context.eoh then
                context.eoh = true
                coroutine.yield(3)
        end
 function close()
        if not context.eoh then
                context.eoh = true
                coroutine.yield(3)
        end
-       
+
        if not context.closed then
                context.closed = true
                coroutine.yield(5)
        end
 end
 
        if not context.closed then
                context.closed = true
                coroutine.yield(5)
        end
 end
 
-function formvalue(...)
-       return context.request:formvalue(...)
+--- Return the request content if the request was of unknown type.
+-- @return     HTTP request body
+-- @return     HTTP request body length
+function content()
+       return context.request:content()
 end
 
 end
 
-function formvaluetable(...)
-       return context.request:formvaluetable(...)
+--- Get a certain HTTP input value or a table of all input values.
+-- @param name         Name of the GET or POST variable to fetch
+-- @param noparse      Don't parse POST data before getting the value
+-- @return                     HTTP input value or table of all input value
+function formvalue(name, noparse)
+       return context.request:formvalue(name, noparse)
 end
 
 end
 
-function getvalue(...)
-       return context.request:getvalue(...)
+--- Get a table of all HTTP input values with a certain prefix.
+-- @param prefix       Prefix
+-- @return                     Table of all HTTP input values with given prefix
+function formvaluetable(prefix)
+       return context.request:formvaluetable(prefix)
 end
 
 end
 
-function postvalue(...)
-       return context.request:postvalue(...)
+--- Get the value of a certain HTTP-Cookie.
+-- @param name         Cookie Name
+-- @return                     String containing cookie data
+function getcookie(name)
+       return context.request:getcookie(name)
 end
 
 end
 
-function getenv(...)
-       return context.request:getenv(...)
+--- Get the value of a certain HTTP environment variable
+-- or the environment table itself.
+-- @param name         Environment variable
+-- @return                     HTTP environment value or environment table
+function getenv(name)
+       return context.request:getenv(name)
 end
 
 end
 
-function setfilehandler(...)
-       return context.request:setfilehandler(...)
+--- Set a handler function for incoming user file uploads.
+-- @param callback     Handler function
+function setfilehandler(callback)
+       return context.request:setfilehandler(callback)
 end
 
 end
 
+--- Send a HTTP-Header.
+-- @param key  Header key
+-- @param value Header value
 function header(key, value)
 function header(key, value)
-       if not context.status then
-               status()
-       end
        if not context.headers then
                context.headers = {}
        end
        if not context.headers then
                context.headers = {}
        end
@@ -147,10 +191,30 @@ function header(key, value)
        coroutine.yield(2, key, value)
 end
 
        coroutine.yield(2, key, value)
 end
 
+--- Set the mime type of following content data.
+-- @param mime Mimetype of following content
 function prepare_content(mime)
 function prepare_content(mime)
-       header("Content-Type", mime)
+       if not context.headers or not context.headers["content-type"] then
+               if mime == "application/xhtml+xml" then
+                       if not getenv("HTTP_ACCEPT") or
+                         not getenv("HTTP_ACCEPT"):find("application/xhtml+xml", nil, true) then
+                               mime = "text/html; charset=UTF-8"
+                       end
+                       header("Vary", "Accept")
+               end
+               header("Content-Type", mime)
+       end
+end
+
+--- Get the RAW HTTP input source
+-- @return     HTTP LTN12 source
+function source()
+       return context.request.input
 end
 
 end
 
+--- Set the HTTP status code and status message.
+-- @param code         Status code
+-- @param message      Status message
 function status(code, message)
        code = code or 200
        message = message or "OK"
 function status(code, message)
        code = code or 200
        message = message or "OK"
@@ -158,51 +222,124 @@ function status(code, message)
        coroutine.yield(1, code, message)
 end
 
        coroutine.yield(1, code, message)
 end
 
-function write(content)
-       if not content or #content == 0 then
-               return
-       end
-       if not context.eoh then
-               if not context.status then
-                       status()
+--- Send a chunk of content data to the client.
+-- This function is as a valid LTN12 sink.
+-- If the content chunk is nil this function will automatically invoke close.
+-- @param content      Content chunk
+-- @param src_err      Error object from source (optional)
+-- @see close
+function write(content, src_err)
+       if not content then
+               if src_err then
+                       error(src_err)
+               else
+                       close()
                end
                end
-               if not context.headers or not context.headers["content-type"] then
-                       header("Content-Type", "text/html; charset=utf-8")
+               return true
+       elseif #content == 0 then
+               return true
+       else
+               if not context.eoh then
+                       if not context.status then
+                               status()
+                       end
+                       if not context.headers or not context.headers["content-type"] then
+                               header("Content-Type", "text/html; charset=utf-8")
+                       end
+                       if not context.headers["cache-control"] then
+                               header("Cache-Control", "no-cache")
+                               header("Expires", "0")
+                       end
+
+
+                       context.eoh = true
+                       coroutine.yield(3)
                end
                end
-               
-               context.eoh = true
-               coroutine.yield(3)
+               coroutine.yield(4, content)
+               return true
        end
        end
-       coroutine.yield(4, content)
 end
 
 end
 
-
-function basic_auth(realm, errorpage)
-       header("Status", "401 Unauthorized")
-       header("WWW-Authenticate", string.format('Basic realm="%s"', realm or ""))
-       
-       if errorpage then
-               errorpage()
-       end
-       
-       close()
+--- Splice data from a filedescriptor to the client.
+-- @param fp   File descriptor
+-- @param size Bytes to splice (optional)
+function splice(fd, size)
+       coroutine.yield(6, fd, size)
 end
 
 end
 
+--- Redirects the client to a new URL and closes the connection.
+-- @param url  Target URL
 function redirect(url)
 function redirect(url)
-       header("Status", "302 Found")
+       status(302, "Found")
        header("Location", url)
        close()
 end
 
        header("Location", url)
        close()
 end
 
-function build_querystring(table)
-       local s="?"
-       
-       for k, v in pairs(table) do
-               s = s .. urlencode(k) .. "=" .. urlencode(v) .. "&"
+--- Create a querystring out of a table of key - value pairs.
+-- @param table                Query string source table
+-- @return                     Encoded HTTP query string
+function build_querystring(q)
+       local s = { "?" }
+
+       for k, v in pairs(q) do
+               if #s > 1 then s[#s+1] = "&" end
+
+               s[#s+1] = urldecode(k)
+               s[#s+1] = "="
+               s[#s+1] = urldecode(v)
        end
        end
-       
-       return s
+
+       return table.concat(s, "")
 end
 
 end
 
-urldecode = luci.http.protocol.urldecode
-urlencode = luci.http.protocol.urlencode
+--- Return the URL-decoded equivalent of a string.
+-- @param str          URL-encoded string
+-- @param no_plus      Don't decode + to " "
+-- @return                     URL-decoded string
+-- @see urlencode
+urldecode = protocol.urldecode
+
+--- Return the URL-encoded equivalent of a string.
+-- @param str          Source string
+-- @return                     URL-encoded string
+-- @see urldecode
+urlencode = protocol.urlencode
+
+--- Send the given data as JSON encoded string.
+-- @param data         Data to send
+function write_json(x)
+       if x == nil then
+               write("null")
+       elseif type(x) == "table" then
+               local k, v
+               if type(next(x)) == "number" then
+                       write("[ ")
+                       for k, v in ipairs(x) do
+                               write_json(v)
+                               if next(x, k) then
+                                       write(", ")
+                               end
+                       end
+                       write(" ]")
+               else
+                       write("{ ")
+                       for k, v in pairs(x) do
+                       write("%q: " % k)
+                               write_json(v)
+                               if next(x, k) then
+                                       write(", ")
+                               end
+                       end
+                       write(" }")
+               end
+       elseif type(x) == "number" or type(x) == "boolean" then
+               if (x ~= x) then
+                       -- NaN is the only value that doesn't equal to itself.   
+                       write("Number.NaN")
+               else
+                       write(tostring(x))
+               end
+       else
+               write("%q" % tostring(x))
+       end
+end