--[[

HTTP protocol implementation for LuCI
(c) 2008 Freifunk Leipzig / Jo-Philipp Wich <xm@leipzig.freifunk.net>

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

        http://www.apache.org/licenses/LICENSE-2.0

$Id: protocol.lua 3843 2008-11-30 13:19:45Z Cyrus $

Con toques hechos en inConcert para que se integre con Wsapi

]]--

local util = require"wsapi.util"
local ltn12 = require "ltn12"

local _M = {}


local function insert_field (tab, name, value)
	if not rawget(tab, name) then
		rawset(tab, name, value)
	else
		local t = type ( rawget(tab, name) )
		if t == "table" then
			table.insert (rawget(tab, name), value)
		else
			rawset(tab, name, { rawget(tab, name), value })
		end
	end
end

local function parse_qs(qs, tab)
	tab = tab or {}
	if type(qs) == "string" then
		local url_decode = util.url_decode
		for key, val in string.gmatch(qs, "([^&=]+)=([^&=]*)&?") do
			insert_field(tab, url_decode(key), url_decode(val))
		end
	elseif qs then
		error("WSAPI Request error: invalid query string")
	end
	return tab
end

--- Returns a function to store chunks in a table
--- When the last chunk is received, it stores it into tab[key]
local function make_accumulator( tab, key )
	local stuff = {}
	return function(hdr, buf, eof)
		stuff[#stuff + 1] = buf
		if eof then
			rawset(tab, key, table.concat(stuff))
			stuff = nil
		end
	end
end

--- Decode an urlencoded http message body with application/x-www-urlencoded
-- Content-Type. Stores all extracted data associated with its parameter name
-- in the params table withing the given message object. Multiple parameter
-- values are stored as tables, ordinary ones as strings.
-- @param source	Ltn12 source function
-- @param request	HTTP message object
-- @return			Value indicating successful operation (not nil means "ok")
-- @return			String containing the error if unsuccessful
-- @see				parse_message_header
function _M.urldecode_message_body( request, source )

	local tlen   = 0
	local lchunk = nil

	local function sink( chunk )

		tlen = tlen + ( chunk and #chunk or 0 )

		if request.env.CONTENT_LENGTH and tlen > tonumber(request.env.CONTENT_LENGTH) + 2 then
			return nil, "Message body size exceeds Content-Length"
		--elseif tlen > HTTP_MAX_CONTENT then
			--return nil, "Message body size exceeds maximum allowed length"
		end

		if not lchunk and chunk then
			lchunk = chunk

		elseif lchunk then
			local data = lchunk .. ( chunk or "&" )
			local spos, epos

			repeat
				spos, epos = data:find("^.-[;&]")

				if spos then
					local pair = data:sub( spos, epos - 1 )
					local key  = pair:match("^(.-)=")
					local val  = pair:match("=([^%s]*)%s*$")

					if key and #key > 0 then
						insert_field( request.params, key, util.url_decode(val) )
					end

					data = data:sub( epos + 1, #data )
				end
			until not spos

			lchunk = data
		end

		return true
	end

	return ltn12.pump.all( source, sink )
end

--- Decode a mime encoded http message body with multipart/form-data
-- Content-Type. Stores all extracted data associated with its parameter name
-- in the params table withing the given message object. Multiple parameter
-- values are stored as tables, ordinary ones as strings.
-- If an optional file callback function is given then it is feeded with the
-- file contents chunk by chunk and only the extracted file name is stored
-- within the params table. The callback function will be called subsequently
-- with three arguments:
--  o Table containing decoded (name, filename) and raw (headers) mime header data
--  o String value containing a chunk of the file data
--  o Boolean which indicates wheather the current chunk is the last one (eof)
-- @param source		Ltn12 source function
-- @param msg			HTTP message object
-- @param file_callback	File callback function (optional)
-- @return				Value indicating successful operation (not nil means "ok")
-- @return				String containing the error if unsuccessful
-- @see					parse_message_header
function _M.mimedecode_message_body( request, source, file_callback )

	if request and request.env.CONTENT_TYPE then
		request.mime_boundary = request.env.CONTENT_TYPE:match("^multipart/form%-data; boundary=(.+)$")
	end

	if not request.mime_boundary then
		return nil, "Invalid Content-Type found"
	end

	local tlen   = 0
	local inhdr  = false
	local field  = nil		-- mime field
	local store  = nil		-- callback function used to store chunks
	local lchunk = nil

	-- Parses the headers available in chunk. If headers were found, then it
	local function parse_headers( chunk, field )
		local stat
		repeat
			chunk, stat = chunk:gsub("^([A-Z][A-Za-z0-9%-_]+): +([^\r\n]+)\r\n",
				function(k,v)
					field.headers[k] = v
					return ""
				end
			)
		until stat == 0
		chunk, stat = chunk:gsub("^\r\n","")
		
		-- End of headers
		if stat > 0 then
			if field.headers["Content-Disposition"] and field.headers["Content-Disposition"]:match("^form%-data; ") then
				field.name = field.headers["Content-Disposition"]:match('name="(.-)"')
				field.filename = field.headers["Content-Disposition"]:match('filename="(.+)"$')
			end

			if not field.headers["Content-Type"] then
				field.headers["Content-Type"] = "application/octet-stream"
			end

			if field.name and field.filename and file_callback then
				insert_field( request.params, field.name, field.filename )
				store = file_callback
			elseif field.name then
				store = make_accumulator( request.params, field.name )
			else
				store = nil
			end

			return chunk, true
		end

		return chunk, false
	end

	local function sink( chunk )
		tlen = tlen + ( chunk and #chunk or 0 )

		if request.env.CONTENT_LENGTH and tlen > tonumber(request.env.CONTENT_LENGTH) + 2 then
			return nil, "Message body size exceeds Content-Length"
		end

		if chunk and not lchunk then
			lchunk = "\r\n" .. chunk

		elseif lchunk then
			local data = lchunk .. ( chunk or "" )
			local spos, epos, found

			repeat
				spos, epos = data:find( "\r\n--" .. request.mime_boundary .. "\r\n", 1, true )
				if not spos then
					spos, epos = data:find( "\r\n--" .. request.mime_boundary .. "--\r\n", 1, true )
				end

				if spos then
					local predata = data:sub( 1, spos - 1 )
					if inhdr then
						predata, eof = parse_headers( predata, field )

						if not eof then
							return nil, "Invalid MIME section header"
						elseif not field.name then
							return nil, "Invalid Content-Disposition header"
						end
					end

					if store then
						store( field, predata, true )
					end

					field = { headers = { } }
					found = found or true

					data, eof = parse_headers( data:sub( epos + 1, #data ), field )
					inhdr = not eof
				end
			until not spos

			-- 78 is the size of a mime header or something ?
			if found then
				if #data > 78 then
					lchunk = data:sub( #data - 78 + 1, #data )
					data   = data:sub( 1, #data - 78 )

					if store then
						store( field, data, false )
					else
						return nil, "Invalid MIME section header"
					end
				else
					lchunk, data = data, nil
				end
			else
				if inhdr then
					lchunk, eof = parse_headers( data, field )
					inhdr = not eof
				else
					store( field, lchunk, false )
					lchunk, chunk = chunk, nil
				end
			end
		end

		return true
	end

	return ltn12.pump.all( source, sink )
end

--- Try to extract and decode a http message body from the given ltn12 source.
-- This function will examine the Content-Type within the given message object
-- to select the appropriate content decoder.
-- Currently the application/x-www-urlencoded and application/form-data
-- mime types are supported. If the encountered content encoding can't be
-- handled then the whole message body will be stored unaltered as "content"
-- property within the given message object.
-- @param source			Ltn12 source function
-- @param msg				HTTP message object
-- @param file_callback		File data callback (optional, see mimedecode_message_body())
-- @return					Value indicating successful operation (not nil means "ok")
-- @return					String containing the error if unsuccessful
-- @see						parse_message_header
function _M.parse_message_body( request, source, file_callback )
	if request.method ~= "POST" then
		error("Only POST requests are handled")	--por ahora
	end
	local env = request.env
	local content_type = request.env.CONTENT_TYPE
	
	if content_type ~= "" then
		-- Is it multipart/mime ?
		if content_type:match("^multipart/form%-data") then
			return _M.mimedecode_message_body( request, source, file_callback )
		-- Is it application/x-www-form-urlencoded ?
		elseif content_type:match("^application/x%-www%-form%-urlencoded") then
			return _M.urldecode_message_body( request, source, file_callback )
		end
	end
	-- Unhandled encoding or no content-type specified
	-- If a file callback is given then feed it chunk by chunk, else
	-- store whole buffer in message.content
	local sink
	local data

	-- If we have a file callback then feed it
	if type(file_callback) == "function" then
		sink = file_callback
	else	-- ... else append to .content
		request.post_data = ""
		data = {}
		sink = ltn12.sink.table(data)
	end

	-- Pump data...
	--[=[
	while true do
		local ok, err = ltn12.pump.step( source, sink )

		if not ok and err then
			return nil, err
		elseif not err then
			if data then
				request.post_data = table.concat(data)
			end
			return true
		end
	end
	]=]
	local ret, err = ltn12.pump.all( source, sink )
	if not ret then
		return false, err
	end
	if data then
		request.post_data = table.concat(data)
	end
	return true
end


--- Make a ltn12 source that reads from the socket in chunks.
-- @param wsapi_env			Wsapi environment
-- @param options			Table with options used when building the source. chunk_size is the value in bytes of the chunk read each time from the socket.
function _M.make_source(wsapi_env, options)
	local chunk_size = options and options.chunk_size or 4096
	local length = tonumber(wsapi_env.CONTENT_LENGTH)
	local left = length
	local blocksize = math.min(chunk_size, left)
	
	return ltn12.source.simplify( function()
		while left > 0 do
			local chunk = wsapi_env.input:read(blocksize)
			if not chunk then break end
			left = left - #chunk
			blocksize = math.min(blocksize, left)
			return chunk, nil
		end
		return nil
	end )
end


function _M.new(wsapi_env, options)
	options = options or {}
	local req = {
		GET          = {},
		POST         = {},
		method       = wsapi_env.REQUEST_METHOD,
		path_info    = wsapi_env.PATH_INFO,
		query_string = wsapi_env.QUERY_STRING,
		script_name  = wsapi_env.SCRIPT_NAME,
		env          = wsapi_env,
		mk_app       = options.mk_app,
		doc_root     = wsapi_env.DOCUMENT_ROOT,
		app_path     = wsapi_env.APP_PATH
	}
	parse_qs(wsapi_env.QUERY_STRING, req.GET)
	
	--[=[ -- esto es como está wsapi hoy en CVS, pero no me sirve
	if options and options.delay_post then
		req.parse_post_data = function (self)
			parse_post_data(wsapi_env, self.POST)
			self.parse_post_data = function ()
				error("POST data already processed")
			end
		end
	else
		parse_post_data(wsapi_env, req.POST)
		req.parse_post_data = function ()
			error("POST data already processed")
		end
	end
	--]=]
	
	req.params = {}
	setmetatable(req.params, { __index = function (tab, name)
						local var = req.GET[name]
						rawset(tab, name, var)
						return var
					end })
	req.cookies = {}
	local cookies = string.gsub(";" .. (wsapi_env.HTTP_COOKIE or "") .. ";", "%s*;%s*", ";")
	setmetatable(req.cookies, { __index = function (tab, name)
						name = name
						local pattern = ";" .. name .. "=(.-);"
						local cookie = string.match(cookies, pattern)
						cookie = util.url_decode(cookie)
						rawset(tab, name, cookie)
						return cookie
					end } )
	
	req.parse_message_body = function(self, ...)
		return _M.parse_message_body( self, _M.make_source(wsapi_env), ... )
	end
	
	return req
end

return _M
