2 -- Copyright (C) 2008-2010 Matthew Wild
3 -- Copyright (C) 2008-2010 Waqas Hussain
5 -- This project is MIT/X11 licensed. Please see the
6 -- COPYING file in the source package for more information.
11 local t_insert
= table.insert
;
12 local t_remove
= table.remove;
13 local t_concat
= table.concat
;
14 local s_format
= string.format;
15 local s_match
= string.match
;
16 local tostring = tostring;
17 local setmetatable
= setmetatable
;
18 local getmetatable
= getmetatable
;
20 local ipairs
= ipairs
;
22 local s_gsub
= string.gsub;
23 local s_sub
= string.sub
;
24 local s_find
= string.find
;
27 local valid_utf8
= require
"util.encodings".utf8
.valid
;
29 local do_pretty_printing
= not os
.getenv("WINDIR");
30 local getstyle
, getstring
;
31 if do_pretty_printing
then
32 local ok
, termcolours
= pcall(require
, "util.termcolours");
34 getstyle
, getstring
= termcolours
.getstyle
, termcolours
.getstring
;
36 do_pretty_printing
= nil;
40 local xmlns_stanzas
= "urn:ietf:params:xml:ns:xmpp-stanzas";
45 local stanza_mt
= { __name
= "stanza" };
46 stanza_mt
.__index
= stanza_mt
;
48 local function check_name(name
, name_type
)
49 if type(name
) ~= "string" then
50 error("invalid "..name_type
.." name: expected string, got "..type(name
));
51 elseif #name
== 0 then
52 error("invalid "..name_type
.." name: empty string");
53 elseif s_find(name
, "[<>& '\"]") then
54 error("invalid "..name_type
.." name: contains invalid characters");
55 elseif not valid_utf8(name
) then
56 error("invalid "..name_type
.." name: contains invalid utf8");
60 local function check_text(text
, text_type
)
61 if type(text
) ~= "string" then
62 error("invalid "..text_type
.." value: expected string, got "..type(text
));
63 elseif not valid_utf8(text
) then
64 error("invalid "..text_type
.." value: contains invalid utf8");
68 local function check_attr(attr
)
70 if type(attr
) ~= "table" then
71 error("invalid attributes, expected table got "..type(attr
));
73 for k
, v
in pairs(attr
) do
74 check_name(k
, "attribute");
75 check_text(v
, "attribute");
76 if type(v
) ~= "string" then
77 error("invalid attribute value for '"..k
.."': expected string, got "..type(v
));
78 elseif not valid_utf8(v
) then
79 error("invalid attribute value for '"..k
.."': contains invalid utf8");
85 local function new_stanza(name
, attr
, namespaces
)
86 check_name(name
, "tag");
88 local stanza
= { name
= name
, attr
= attr
or {}, namespaces
= namespaces
, tags
= {} };
89 return setmetatable(stanza
, stanza_mt
);
92 local function is_stanza(s
)
93 return getmetatable(s
) == stanza_mt
;
96 function stanza_mt
:query(xmlns
)
97 return self
:tag("query", { xmlns
= xmlns
});
100 function stanza_mt
:body(text
, attr
)
101 return self
:text_tag("body", text
, attr
);
104 function stanza_mt
:text_tag(name
, text
, attr
, namespaces
)
105 return self
:tag(name
, attr
, namespaces
):text(text
):up();
108 function stanza_mt
:tag(name
, attr
, namespaces
)
109 local s
= new_stanza(name
, attr
, namespaces
);
110 local last_add
= self
.last_add
;
111 if not last_add
then last_add
= {}; self
.last_add
= last_add
; end
112 (last_add
[#last_add
] or self
):add_direct_child(s
);
113 t_insert(last_add
, s
);
117 function stanza_mt
:text(text
)
118 if text
~= nil and text
~= "" then
119 local last_add
= self
.last_add
;
120 (last_add
and last_add
[#last_add
] or self
):add_direct_child(text
);
125 function stanza_mt
:up()
126 local last_add
= self
.last_add
;
127 if last_add
then t_remove(last_add
); end
131 function stanza_mt
:reset()
136 function stanza_mt
:add_direct_child(child
)
137 if is_stanza(child
) then
138 t_insert(self
.tags
, child
);
139 t_insert(self
, child
);
141 check_text(child
, "text");
142 t_insert(self
, child
);
146 function stanza_mt
:add_child(child
)
147 local last_add
= self
.last_add
;
148 (last_add
and last_add
[#last_add
] or self
):add_direct_child(child
);
152 function stanza_mt
:remove_children(name
, xmlns
)
153 xmlns
= xmlns
or self
.attr
.xmlns
;
154 return self
:maptags(function (tag)
155 if (not name
or tag.name
== name
) and tag.attr
.xmlns
== xmlns
then
162 function stanza_mt
:get_child(name
, xmlns
)
163 for _
, child
in ipairs(self
.tags
) do
164 if (not name
or child
.name
== name
)
165 and ((not xmlns
and self
.attr
.xmlns
== child
.attr
.xmlns
)
166 or child
.attr
.xmlns
== xmlns
) then
173 function stanza_mt
:get_child_text(name
, xmlns
)
174 local tag = self
:get_child(name
, xmlns
);
176 return tag:get_text();
181 function stanza_mt
:child_with_name(name
)
182 for _
, child
in ipairs(self
.tags
) do
183 if child
.name
== name
then return child
; end
187 function stanza_mt
:child_with_ns(ns
)
188 for _
, child
in ipairs(self
.tags
) do
189 if child
.attr
.xmlns
== ns
then return child
; end
193 function stanza_mt
:children()
201 function stanza_mt
:childtags(name
, xmlns
)
202 local tags
= self
.tags
;
203 local start_i
, max_i
= 1, #tags
;
205 for i
= start_i
, max_i
do
207 if (not name
or v
.name
== name
)
208 and ((not xmlns
and self
.attr
.xmlns
== v
.attr
.xmlns
)
209 or v
.attr
.xmlns
== xmlns
) then
217 function stanza_mt
:maptags(callback
)
218 local tags
, curr_tag
= self
.tags
, 1;
219 local n_children
, n_tags
= #self
, #tags
;
220 local max_iterations
= n_children
+ 1;
223 while curr_tag
<= n_tags
and n_tags
> 0 do
224 if self
[i
] == tags
[curr_tag
] then
225 local ret
= callback(self
[i
]);
228 t_remove(tags
, curr_tag
);
229 n_children
= n_children
- 1;
232 curr_tag
= curr_tag
- 1;
235 tags
[curr_tag
] = ret
;
237 curr_tag
= curr_tag
+ 1;
240 if i
> max_iterations
then
241 -- COMPAT: Hopefully temporary guard against #981 while we
242 -- figure out the root cause
243 error("Invalid stanza state! Please report this error.");
249 function stanza_mt
:find(path
)
251 local len
= #path
+ 1;
254 local xmlns
, name
, text
;
255 local char
= s_sub(path
, pos
, pos
);
257 return self
.attr
[s_sub(path
, pos
+ 1)];
258 elseif char
== "{" then
259 xmlns
, pos
= s_match(path
, "^([^}]+)}()", pos
+ 1);
261 name
, text
, pos
= s_match(path
, "^([^@/#]*)([/#]?)()", pos
);
262 name
= name
~= "" and name
or nil;
265 return self
:get_child_text(name
, xmlns
);
267 return self
:get_child(name
, xmlns
);
269 self
= self
:get_child(name
, xmlns
);
273 local function _clone(stanza
, only_top
)
274 local attr
, tags
= {}, {};
275 for k
,v
in pairs(stanza
.attr
) do attr
[k
] = v
; end
276 local old_namespaces
, namespaces
= stanza
.namespaces
;
277 if old_namespaces
then
279 for k
,v
in pairs(old_namespaces
) do namespaces
[k
] = v
; end
281 local new
= { name
= stanza
.name
, attr
= attr
, namespaces
= namespaces
, tags
= tags
};
284 local child
= stanza
[i
];
286 child
= _clone(child
);
287 t_insert(tags
, child
);
289 t_insert(new
, child
);
292 return setmetatable(new
, stanza_mt
);
295 local function clone(stanza
, only_top
)
296 if not is_stanza(stanza
) then
297 error("bad argument to clone: expected stanza, got "..type(stanza
));
299 return _clone(stanza
, only_top
);
302 local escape_table
= { ["'"] = "'", ["\""] = """, ["<"] = "<", [">"] = ">", ["&"] = "&" };
303 local function xml_escape(str
) return (s_gsub(str
, "['&<>\"]", escape_table
)); end
305 local function _dostring(t
, buf
, self
, _xml_escape
, parentns
)
308 t_insert(buf
, "<"..name
);
309 for k
, v
in pairs(t
.attr
) do
310 if s_find(k
, "\1", 1, true) then
311 local ns
, attrk
= s_match(k
, "^([^\1]*)\1?(.*)$");
313 t_insert(buf
, " xmlns:ns"..nsid
.."='".._xml_escape(ns
).."' ".."ns"..nsid
..":"..attrk
.."='".._xml_escape(v
).."'");
314 elseif not(k
== "xmlns" and v
== parentns
) then
315 t_insert(buf
, " "..k
.."='".._xml_escape(v
).."'");
326 self(child
, buf
, self
, _xml_escape
, t
.attr
.xmlns
);
328 t_insert(buf
, _xml_escape(child
));
331 t_insert(buf
, "</"..name
..">");
334 function stanza_mt
.__tostring(t
)
336 _dostring(t
, buf
, _dostring
, xml_escape
, nil);
337 return t_concat(buf
);
340 function stanza_mt
.top_tag(t
)
341 local top_tag_clone
= clone(t
, true);
342 return tostring(top_tag_clone
):sub(1,-3)..">";
345 function stanza_mt
.get_text(t
)
351 function stanza_mt
.get_error(stanza
)
352 local error_type
, condition
, text
;
354 local error_tag
= stanza
:get_child("error");
355 if not error_tag
then
356 return nil, nil, nil;
358 error_type
= error_tag
.attr
.type;
360 for _
, child
in ipairs(error_tag
.tags
) do
361 if child
.attr
.xmlns
== xmlns_stanzas
then
362 if not text
and child
.name
== "text" then
363 text
= child
:get_text();
364 elseif not condition
then
365 condition
= child
.name
;
367 if condition
and text
then
372 return error_type
, condition
or "undefined-condition", text
;
375 local function preserialize(stanza
)
376 local s
= { name
= stanza
.name
, attr
= stanza
.attr
};
377 for _
, child
in ipairs(stanza
) do
378 if type(child
) == "table" then
379 t_insert(s
, preserialize(child
));
387 stanza_mt
.__freeze
= preserialize
;
389 local function deserialize(serialized
)
392 local attr
= serialized
.attr
;
394 for att
, val
in pairs(attr
) do
395 if type(att
) == "string" then
396 if s_find(att
, "|", 1, true) and not s_find(att
, "\1", 1, true) then
397 local ns
,na
= s_match(att
, "^([^|]+)|(.+)$");
398 attrx
[ns
.."\1"..na
] = val
;
404 local stanza
= new_stanza(serialized
.name
, attrx
);
405 for _
, child
in ipairs(serialized
) do
406 if type(child
) == "table" then
407 stanza
:add_direct_child(deserialize(child
));
408 elseif type(child
) == "string" then
409 stanza
:add_direct_child(child
);
416 local function message(attr
, body
)
418 return new_stanza("message", attr
);
420 return new_stanza("message", attr
):text_tag("body", body
);
423 local function iq(attr
)
425 error("iq stanzas require id and type attributes");
428 error("iq stanzas require an id attribute");
430 if not attr
.type then
431 error("iq stanzas require a type attribute");
433 return new_stanza("iq", attr
);
436 local function reply(orig
)
437 return new_stanza(orig
.name
,
442 type = ((orig
.name
== "iq" and "result") or orig
.attr
.type)
446 local xmpp_stanzas_attr
= { xmlns
= xmlns_stanzas
};
447 local function error_reply(orig
, error_type
, condition
, error_message
)
448 local t
= reply(orig
);
449 t
.attr
.type = "error";
450 t
:tag("error", {type = error_type
}) --COMPAT: Some day xmlns:stanzas goes here
451 :tag(condition
, xmpp_stanzas_attr
):up();
452 if error_message
then t
:text_tag("text", error_message
, xmpp_stanzas_attr
); end
453 return t
; -- stanza ready for adding app-specific errors
456 local function presence(attr
)
457 return new_stanza("presence", attr
);
460 if do_pretty_printing
then
461 local style_attrk
= getstyle("yellow");
462 local style_attrv
= getstyle("red");
463 local style_tagname
= getstyle("red");
464 local style_punc
= getstyle("magenta");
466 local attr_format
= " "..getstring(style_attrk
, "%s")..getstring(style_punc
, "=")..getstring(style_attrv
, "'%s'");
467 local top_tag_format
= getstring(style_punc
, "<")..getstring(style_tagname
, "%s").."%s"..getstring(style_punc
, ">");
468 --local tag_format = getstring(style_punc, "<")..getstring(style_tagname, "%s").."%s"..getstring(style_punc, ">").."%s"..getstring(style_punc, "</")..getstring(style_tagname, "%s")..getstring(style_punc, ">");
469 local tag_format
= top_tag_format
.."%s"..getstring(style_punc
, "</")..getstring(style_tagname
, "%s")..getstring(style_punc
, ">");
470 function stanza_mt
.pretty_print(t
)
471 local children_text
= "";
472 for _
, child
in ipairs(t
) do
473 if type(child
) == "string" then
474 children_text
= children_text
.. xml_escape(child
);
476 children_text
= children_text
.. child
:pretty_print();
480 local attr_string
= "";
482 for k
, v
in pairs(t
.attr
) do if type(k
) == "string" then attr_string
= attr_string
.. s_format(attr_format
, k
, tostring(v
)); end end
484 return s_format(tag_format
, t
.name
, attr_string
, children_text
, t
.name
);
487 function stanza_mt
.pretty_top_tag(t
)
488 local attr_string
= "";
490 for k
, v
in pairs(t
.attr
) do if type(k
) == "string" then attr_string
= attr_string
.. s_format(attr_format
, k
, tostring(v
)); end end
492 return s_format(top_tag_format
, t
.name
, attr_string
);
495 -- Sorry, fresh out of colours for you guys ;)
496 stanza_mt
.pretty_print
= stanza_mt
.__tostring
;
497 stanza_mt
.pretty_top_tag
= stanza_mt
.top_tag
;
501 stanza_mt
= stanza_mt
;
503 is_stanza
= is_stanza
;
504 preserialize
= preserialize
;
505 deserialize
= deserialize
;
510 error_reply
= error_reply
;
512 xml_escape
= xml_escape
;