----------------------------------------------------------------------------- -- Xavante webDAV handler -- Author: Javier Guerra -- Copyright (c) 2005 Javier Guerra ----------------------------------------------------------------------------- require "lxp.lom" require "socket.url" local url = socket.url module (arg and arg[1]) -- returns a copy of the string without any -- leading or trailing whitespace local function trim (s) return (string.gsub (string.gsub (s, "^%s*", ""), "%s*$", "")) end -- reverts a LOM-style attribute array to XML form local function attrstostr (a) local out = {} for i,attr in ipairs (a) do table.insert (out, string.format ('%s = "%s"', attr, a[attr])) end return table.concat (out, " ") end -- outputs a LOM XML object back in XML text form -- params: -- x: LOM XML object -- of: output function, uses print() if not given local function lomtoxml (x, of) if type (x) == "string" then return of (x) end of = of or print local attrs = "" -- does it have any attributes? if x.attr [1] then attrs = " " .. attrstostr (x.attr) end -- is there any content? if x[1] then of ("<" .. x.tag .. attrs .. ">") for i in ipairs (x) do lomtoxml (x[i], of) end of ("") else of ("<" .. x.tag .. attrs .. "/>") end end -- gets request content, and parses it as XML -- returns LOM object local function req_xml (req) local sz = req.headers ["content-length"] local indata if sz then indata = req.socket:receive (sz) else indata = function () return req.socket:receive () end end return lxp.lom.parse (indata) end -- expands namespace tags in-situ -- returns the same LOM object after processing local function xml_ns (x, dict) if not x then return end if type (x) == "string" then return x end dict = dict or {} -- adds any new namespace to the dictionary for i,attr_name in ipairs (x.attr) do -- default namespace if attr_name == "xmlns" then dict [""] = x.attr [attr_name] end -- named namespaces local _,_, ns_var = string.find (attr_name, "xmlns:(.*)$") if ns_var then dict [ns_var] = x.attr [attr_name] end end -- modifies this node's tag local _,_, ns = string.find (x.tag, "^(.*):") if ns and dict [ns] then local pat = string.format ("^%s:", ns) x.tag = string.gsub (x.tag, pat, dict[ns]) end if not ns and dict [""] then x.tag = dict [""] .. x.tag end -- recurses to child nodes for _, sub in ipairs (x) do xml_ns (sub, dict) end return x end -- iterator for traversing only childs in a LOM object -- whith a given tagname. local function lomChildsByTagName (x, tagname) if not x then return function () return nil end end local function gen (x) for _,elem in ipairs (x) do if type(elem) == "table" then if elem.tag and elem.tag == tagname then coroutine.yield (elem) end end end end return coroutine.wrap (function () gen (x) end) end -- iterator for traversing all elements in a LOM object -- whith a given tagname (at any depth). local function lomElementsByTagName (x, tagname) if not x then return function () return nil end end local function gen (x) for _,elem in ipairs (x) do if type(elem) == "table" then if elem.tag and elem.tag == tagname then coroutine.yield (elem) end gen (elem) end end end return coroutine.wrap (function () gen (x) end) end -- iterates on the childs of a LOM node -- use as: -- for subnode, tagname in lomChilds (node) do ... end local function lomChilds (x) if not x then return function () return nil end end local function gen () for _, elem in ipairs (x) do if type (elem) == "table" and elem.tag then coroutine.yield (elem, elem.tag) end end end return coroutine.wrap (gen) end -- returns all the text content of a LOM node local function lomContent (x) if not x then return "" end local out = {} local function of (s) table.insert (out, s) end for _, child in ipairs (x) do lomtoxml (child, of) end return table.concat (out) end -- returns a table member of a table, creates it if needed -- params: -- t: table -- k: key local function maketabletable (t,k) t[k] = t[k] or {} return t[k] end -- if a tagname has inconvenient characters -- replaces part of if with a (possibly new) -- namespace reference -- params: -- ns: namespace dictionary -- name: tagname to reduce -- returns new tagname, ns is modified in-place if needed local function reducename (ns, name) if string.find (name, "[:/?#]") then local _,_,pfx,sfx = string.find (name, "(.*%W)(%w+)") local n = 0 for k,v in pairs (ns) do n = n+1 if v == pfx then return string.format ("%s:%s", k, sfx) end end local newns = "lm"..n ns [newns] = pfx return string.format ("%s:%s", newns, sfx) else return name end end -- returns a XML attr string encoding a namespace dictionary local function nsattr (ns) local attr = {} for k,v in pairs (ns) do table.insert (attr, string.format ([[xmlns:%s="%s"]], k, v)) end return table.concat (attr, " ") end local function addProp (outtable, propname, propval) if propval then local propentry = maketabletable (outtable, "HTTP/1.1 200 OK") if propval == "" then table.insert (propentry, string.format ([[<%s/>]], propname)) else table.insert (propentry, string.format ([[<%s>%s]], propname, propval, propname)) end else local propentry = maketabletable (outtable, "HTTP/1.1 404 NotFound") table.insert (propentry, string.format ([[<%s/>]], propname)) end end local function addPropstat (outtable, propstat) local codes = {} for stat in pairs (propstat) do table.insert (codes, stat) end table.sort (codes) for _, stat in ipairs (codes) do props = propstat [stat] if props then table.insert (outtable, [[]]) table.insert (outtable, [[]]) for _,prop in ipairs (props) do table.insert (outtable, prop) end table.insert (outtable, [[]]) table.insert (outtable, string.format ([[%s]], stat)) table.insert (outtable, [[]]) end end end local function dav_propfind (req, res, repos_b, props_b) res.statusline = "HTTP/1.1 207 Multi-Status\r\n" res.headers ["Content-Type"] = 'text/xml; charset="utf-8"' local depth = req.headers.depth local path = req.relpath local data = xml_ns (req_xml (req)) -- print ("path:", path, req.relpath) -- print ("depth:", depth) -- print ("como xml:") lomtoxml (data) print () local resource_q = repos_b:getResource (req.match, path) if not resource_q then return httpd.err_404 (req, res) end local content = {} table.insert (content, [[]]) table.insert (content, [[]]) local namespace = {} assert (data.tag == "DAV:propfind") for resource in resource_q:getItems (depth) do -- print ("resource:", resource.path) local propstat = {} namespace.D="DAV:" for findtype, findtype_tn in lomChilds (data) do if findtype_tn == "DAV:prop" then for _, propname in lomChilds (findtype) do local propval = resource:getProp (propname) if not propval and props_b then propval = props_b:getProp (req.relpath, propname) end local shortname = reducename (namespace, propname) addProp (propstat, shortname, propval) end elseif findtype_tn == "DAV:propname" then for propname in resource:getPropNames () do local shortname = reducename (namespace, propname) addProp (propstat, shortname, "") end if props_b then for propname in props_b:getPropNames (req.relpath) do local shortname = reducename (namespace, propname) addProp (propstat, shortname, "") end end elseif findtype_tn == "DAV:allprop" then for propname in resource:getPropNames () do local shortname = reducename (namespace, propname) local propval = resource:getProp (propname) addProp (propstat, shortname, propval) end if props_b then for propname in props_b:getPropNames (req.relpath) do local shortname = reducename (namespace, propname) local propval = props_b:getProp (req.relpath, propname) addProp (propstat, shortname, propval) end end end end namespace.D = nil table.insert (content, string.format ([[]], nsattr (namespace))) table.insert (content, string.format ([[%s]], resource:getHRef())) addPropstat (content, propstat) table.insert (content, [[]]) end table.insert (content, [[]]) -- for _,l in ipairs (content) do print (l) end res.content = content return res end local function dav_proppatch (req, res, repos_b, props_b) res.statusline = "HTTP/1.1 207 Multi-Status\r\n" res.headers ["Content-Type"] = 'text/xml; charset="utf-8"' local path = req.relpath local data = xml_ns (req_xml (req)) -- print ("como xml:") lomtoxml (data) print () local resource = repos_b:getResource (req.match, path) if not resource then return httpd.err_404 (req, res) end local content = {} table.insert (content, [[]]) local namespace = {D="DAV:"} local propstat = {} assert (data.tag == "DAV:propertyupdate") for cmd_node, cmd_name in lomChilds (data) do for nd in lomChildsByTagName (cmd_node, "DAV:prop") do local status = nil for prop_node, propname in lomChilds (nd) do if cmd_name == "DAV:set" then props_b:setProp (path, propname, lomContent (prop_node)) status = "HTTP/1.1 200 OK" elseif cmd_name == "DAV:remove" then props_b:setProp (path, propname, nil) status = "HTTP/1.1 200 OK" end if status then local propentry = maketabletable (content, status) table.insert (propentry, string.format ([[<%s/>]], reducename (namespace, propname))) end end end end table.insert (content, string.format ([[]], nsattr (namespace))) table.insert (content, [[]]) table.insert (content, string.format ([[%s]], resource:getHRef())) addPropstat (content, propstat) table.insert (content, [[]]) table.insert (content, [[]]) res.content = content return res end local function dav_options (req, res, repos_b, props_b) res.headers ["DAV"] = "1,2" res.content = "" return res end local function dav_get (req, res, repos_b, props_b) local resource = repos_b:getResource (req.match, req.relpath) if not resource then return httpd.err_404 (req, res) end res.headers ["Content-Type"] = resource:getContentType () res.headers ["Content-Length"] = resource:getContentSize () or 0 res:send_headers () for block in resource:getContentData () do res:send_data (block) end return res end local function dav_put (req, res, repos_b) local path = req.relpath local resource = repos_b:getResource (req.match, path) or repos_b:createResource (req.match, path) if req.headers["content-range"] then return httpd.err_405 (req, res) end local contentlength = assert (req.headers ["content-length"]) + 0 local bsz while contentlength > 0 do if contentlength > 4096 then bsz = 4096 else bsz = contentlength end resource:addContentData (req.socket:receive (bsz)) contentlength = contentlength - bsz end res:send_headers () return res end local function dav_mkcol (req, res, repos_b) local path = req.relpath local resource = repos_b:getResource (req.match, path) if resource then res.statusline = "HTTP/1.1 405 Method Not Allowed\r\n" res.content = "" return res end local done, err = repos_b:createCollection (req.match, path) if done then res.statusline = "HTTP/1.1 201 Created\r\n" else res.statusline = "HTTP/1.1 403 Forbidden\r\n" end res.content = "" return res end local function dav_delete (req, res, repos_b, props_b) local path = req.relpath local resource = repos_b:getResource (req.match, path) if not resource then return http.err_404 end -- NOTE: this should iterate depth-first for r in resource:getItems ("Infinity") do local ok, err = resource:delete () if not ok then res.statusline = "HTTP/1.1 207 Multi-Status\r\n" res.content = string.format ([[ %s %s ]], resource:getHRef(), err) return res end props_b:delete (resource:getPath ()) -- TODO end res.statusline = "HTTP/1.1 204 No Content\r\n" --[[ local done, err = resource:delete () if done then res.statusline = "HTTP/1.1 204 No Content\r\n" props_b:delete (path) else res.statusline = "HTTP/1.1 403 Forbidden\r\n" end --]] res.content = "" return res end local function dav_lock (req, res, repos_b) local data = xml_ns (req_xml (req)) print ("como xml:") lomtoxml (data) print () return res end local dav_cmd_dispatch = { PROPFIND = dav_propfind, PROPPATCH = dav_proppatch, OPTIONS = dav_options, GET = dav_get, PUT = dav_put, MKCOL = dav_mkcol, DELETE = dav_delete, LOCK = dav_lock, } function makeHandler (repos_b, props_b) return function (req, res) -- print (req.cmd_mth, req.parsed_url.path) -- for k,v in pairs (req.headers) do print (k,v) end local dav_handler = dav_cmd_dispatch [req.cmd_mth] if dav_handler then return dav_handler (req, res, repos_b, props_b) end end end