LiquidProxy Lua Edition
1-- by thenumbernine: github.com/thenumbernine/lua-url/blob/master/unfacebook.lua
2-- slightly, slightly modified
3
4-- because socket.url is another library
5-- meh
6local class = require 'ext.class'
7local table = require 'ext.table'
8--local assert = require 'ext.assert'
9local string = require 'ext.string'
10
11
12local defaultescapechars = "!#$&'()*+,/:;=?@[]%"
13local escapecharsets = {}
14local function escape(s, escapechars)
15 escapechars = escapechars or defaultescapechars
16 local escapecharset = escapecharsets[escapechars]
17 if not escapecharset then
18 escapecharset = string.split(escapechars):mapi(function(ch) return true, ch end):setmetatable(nil)
19 escapecharsets[escapechars] = escapecharset
20 end
21 return (s:gsub('.', function(ch)
22 if escapecharset[ch] then
23 return ('%%%02X'):format(ch:byte())
24 end
25 return ch -- necessary?
26 end))
27end
28
29local function unescape(s)
30 return (s:gsub('%%%x%x', function(s)
31 local n = tonumber(s:sub(2), 16)
32 if not n then return s end
33 local ch = string.char(n)
34 if not ch then return s end
35 return ch
36 end))
37end
38--[[
39't' handles *BOTH* pairs k=v and ipairs {k,v}
40first it processes ipairs
41then it processes all pairs that are non-integer keys
42this way you can pass it either pairs for quick Lua handling
43or ipairs if you care about the order
44--]]
45local escapekeychars = "!#$&'()*+,:;=?@%" -- key doesn't need /[] escaped ...
46local function argsToStr(t)
47 t = table(t):setmetatable(nil)
48 local s = table()
49 local sep
50 for i=1,#t do
51 local kv = t[i]
52 -- TODO still, what to do for key without value?
53 local k,v = tostring(kv[1]), tostring(kv[2])
54 if sep then s:insert(sep) end
55 sep = '&'
56 s:insert(escape(k, escapekeychars))
57 s:insert'='
58 s:insert(escape(v))
59 -- clear as you go
60 t[i] = nil
61 t[k] = nil
62 end
63 -- process whats left of ipairs
64 for k,v in pairs(t) do
65 if sep then s:insert(sep) end
66 sep = '&'
67 s:insert(escape(tostring(k), escapekeychars))
68 s:insert'='
69 s:insert(escape(tostring(v)))
70 end
71 return s:concat()
72end
73
74local URL = class()
75
76URL.escape = escape
77URL.unescape = unescape
78URL.argsToStr = argsToStr
79
80local function parseArgs(kvstr)
81 -- also return in-order list
82 local kvs = table()
83 for _,kv in ipairs(string.split(kvstr, '&')) do
84 local k,v = kv:match'^([^=]+)=(.*)$'
85
86 -- what if there's no "=" ? then what?
87 if not k then k,v = kv, '' end
88
89 k = unescape(k) or k
90 v = unescape(v) or v
91
92 kvs:insert{k, v}
93 kvs[k] = v
94 end
95 return kvs
96end
97
98URL.parseArgs = function(self,kvstr)
99 if not kvstr then return parseArgs(self) else return parseArgs(kvstr) end
100end
101
102
103--[[
104args as a string parses the fields.
105args as a table copies the fields.
106
107fields are:
108 scheme
109 host
110 user
111 pass
112 port
113 path
114 params
115 query
116 fragment
117
118 userinfo
119--]]
120function URL:init(args)
121 if type(args) == 'string' then
122 local url = args
123
124 -- <scheme>://<username>:<password>@<host>:<port>/<path>;<parameters>?<query>#<fragment>
125 -- [<scheme>://][<username>[:<password>]@]<host>[:<port>][/<path>][;<parameters>][?<query>][#<fragment>]
126
127 -- parse scheme
128 local scheme, rest = url:match'^([^:]+)://(.*)$'
129 rest = rest or url
130 self.scheme = scheme
131
132 -- parse authority vs path+query+params+fragment
133 local authority, pathqueryparamsfragment = rest:match'^([^/;?#]+)[/;?#](.*)$' -- expect host to end at /;?#
134 authority = authority or rest
135
136 local userinfo, hostandport = authority:match'^([^@]+)@(.*)$'
137 if userinfo then
138 local user, pass = userinfo:match'^([^:]*):(.*)$'
139 user = user or userinfo
140 self.user = user
141 self.pass = pass
142 else
143 hostandport = authority
144 end
145
146 local host, port = hostandport:match'^([^:]+):(.*)$'
147 host = host or hostandport
148 self.host = host
149 self.port = port
150
151 if pathqueryparamsfragment then
152 -- /path;params?query#fragment
153 -- what if you get ;abc/def? then it's still the params
154 local prevrest = pathqueryparamsfragment
155 rest, self.fragment = prevrest:match'^([^#]+)#(.*)$'
156 prevrest = rest or prevrest
157 rest, self.query = prevrest:match'^([^?]+)?(.*)$'
158 prevrest = rest or prevrest
159 rest, self.params = prevrest:match'^([^;]+);(.*)$'
160 prevrest = rest or prevrest
161 self.path = prevrest
162
163 if self.query then self.query = parseArgs(self.query) end
164 if self.params then self.params = parseArgs(self.params) end
165 elseif args:sub(1,1) == "?" then
166 self.query = parseArgs(args)
167 self.params = nil
168 end
169
170 elseif type(args) == 'table' then
171 for k,v in pairs(args) do self[k] = v end
172 elseif type(args) == 'nil' then
173 else
174 error("idk how to build a URL from this")
175 end
176end
177
178URL.__concat = string.concat
179
180-- tostring or another function?
181function URL:__tostring()
182 local s = table()
183 if self.scheme then
184 s:insert(self.scheme)
185 s:insert'://'
186 end
187 if self.user then
188 s:insert(self.user)
189 if self.pass then
190 s:insert':'
191 s:insert(self.pass)
192 end
193 s:insert'@'
194 end
195 s:insert(self.host)
196-- TODO escape path? except /'s ?
197 if self.path then
198 s:insert'/'
199 s:insert(self.path)
200 end
201 if self.params then
202 s:insert';'
203 s:insert(argsToStr(self.params))
204 end
205 if self.query then
206 s:insert'?'
207 s:insert(argsToStr(self.query))
208 end
209 if self.fragment then
210 s:insert'#'
211 s:insert(self.fragment)
212 end
213 return s:concat()
214end
215
216-- shorthand
217URL.tostring = URL.__tostring
218
219return URL