Migrate certificates, icons, logs to XDG dirs
[pidgin-git.git] / libpurple / protocols / jabber / message.c
blobef38fa93371f52eee5f5c557072b5e74328a5286
1 /*
2 * purple - Jabber Protocol Plugin
4 * Purple is the legal property of its developers, whose names are too numerous
5 * to list here. Please refer to the COPYRIGHT file distributed with this
6 * source distribution.
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, write to the Free Software
20 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
23 #include "internal.h"
25 #include "debug.h"
26 #include "notify.h"
27 #include "smiley-custom.h"
28 #include "smiley-parser.h"
29 #include "server.h"
30 #include "util.h"
31 #include "adhoccommands.h"
32 #include "buddy.h"
33 #include "chat.h"
34 #include "data.h"
35 #include "google/google.h"
36 #include "message.h"
37 #include "xmlnode.h"
38 #include "pep.h"
39 #include "smiley.h"
40 #include "iq.h"
42 #include <string.h>
44 void jabber_message_free(JabberMessage *jm)
46 g_free(jm->from);
47 g_free(jm->to);
48 g_free(jm->id);
49 g_free(jm->subject);
50 g_free(jm->body);
51 g_free(jm->xhtml);
52 g_free(jm->password);
53 g_free(jm->error);
54 g_free(jm->thread_id);
55 g_list_free(jm->etc);
56 g_list_free(jm->eventitems);
58 g_free(jm);
61 static void handle_chat(JabberMessage *jm)
63 JabberID *jid = jabber_id_new(jm->from);
65 PurpleConnection *gc;
66 PurpleAccount *account;
67 JabberBuddy *jb;
68 JabberBuddyResource *jbr;
70 if(!jid)
71 return;
73 gc = jm->js->gc;
74 account = purple_connection_get_account(gc);
76 jb = jabber_buddy_find(jm->js, jm->from, TRUE);
77 jbr = jabber_buddy_find_resource(jb, jid->resource);
79 if(!jm->xhtml && !jm->body) {
80 if (jbr && jm->chat_state != JM_STATE_NONE)
81 jbr->chat_states = JABBER_CHAT_STATES_SUPPORTED;
83 if(JM_STATE_COMPOSING == jm->chat_state) {
84 purple_serv_got_typing(gc, jm->from, 0, PURPLE_IM_TYPING);
85 } else if(JM_STATE_PAUSED == jm->chat_state) {
86 purple_serv_got_typing(gc, jm->from, 0, PURPLE_IM_TYPED);
87 } else if(JM_STATE_GONE == jm->chat_state) {
88 PurpleIMConversation *im = purple_conversations_find_im_with_account(
89 jm->from, account);
90 if (im && jid->node && jid->domain) {
91 char buf[256];
92 PurpleBuddy *buddy;
94 g_snprintf(buf, sizeof(buf), "%s@%s", jid->node, jid->domain);
96 if ((buddy = purple_blist_find_buddy(account, buf))) {
97 const char *who;
98 char *escaped;
100 who = purple_buddy_get_alias(buddy);
101 escaped = g_markup_escape_text(who, -1);
103 g_snprintf(buf, sizeof(buf),
104 _("%s has left the conversation."), escaped);
105 g_free(escaped);
107 /* At some point when we restructure PurpleConversation,
108 * this should be able to be implemented by removing the
109 * user from the conversation like we do with chats now. */
110 purple_conversation_write_system_message(
111 PURPLE_CONVERSATION(im), buf, 0);
114 purple_serv_got_typing_stopped(gc, jm->from);
116 } else {
117 purple_serv_got_typing_stopped(gc, jm->from);
119 } else {
120 if (jid->resource) {
122 * We received a message from a specific resource, so
123 * we probably want a reply to go to this specific
124 * resource (i.e. bind/lock the conversation to this
125 * resource).
127 * This works because purple_im_conversation_send gets the name
128 * from purple_conversation_get_name()
130 PurpleIMConversation *im;
132 im = purple_conversations_find_im_with_account(jm->from, account);
133 if (im && !g_str_equal(jm->from,
134 purple_conversation_get_name(PURPLE_CONVERSATION(im)))) {
135 purple_debug_info("jabber", "Binding conversation to %s\n",
136 jm->from);
137 purple_conversation_set_name(PURPLE_CONVERSATION(im), jm->from);
141 if(jbr) {
142 /* Treat SUPPORTED as a terminal with no escape :) */
143 if (jbr->chat_states != JABBER_CHAT_STATES_SUPPORTED) {
144 if (jm->chat_state != JM_STATE_NONE)
145 jbr->chat_states = JABBER_CHAT_STATES_SUPPORTED;
146 else
147 jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED;
150 g_free(jbr->thread_id);
151 jbr->thread_id = g_strdup(jbr->thread_id);
154 if (jm->js->googletalk && jm->xhtml == NULL) {
155 char *tmp = jm->body;
156 jm->body = jabber_google_format_to_html(jm->body);
157 g_free(tmp);
159 purple_serv_got_im(gc, jm->from, jm->xhtml ? jm->xhtml : jm->body, 0, jm->sent);
162 jabber_id_free(jid);
165 static void handle_headline(JabberMessage *jm)
167 char *title;
168 GString *body;
169 GList *etc;
171 if(!jm->xhtml && !jm->body)
172 return; /* ignore headlines without any content */
174 body = g_string_new("");
175 title = g_strdup_printf(_("Message from %s"), jm->from);
177 if(jm->xhtml)
178 g_string_append(body, jm->xhtml);
179 else if(jm->body)
180 g_string_append(body, jm->body);
182 for(etc = jm->etc; etc; etc = etc->next) {
183 PurpleXmlNode *x = etc->data;
184 const char *xmlns = purple_xmlnode_get_namespace(x);
185 if(xmlns && !strcmp(xmlns, NS_OOB_X_DATA)) {
186 PurpleXmlNode *url, *desc;
187 char *urltxt, *desctxt;
189 url = purple_xmlnode_get_child(x, "url");
190 desc = purple_xmlnode_get_child(x, "desc");
192 if(!url || !desc)
193 continue;
195 urltxt = purple_xmlnode_get_data(url);
196 desctxt = purple_xmlnode_get_data(desc);
198 /* I'm all about ugly hacks */
199 if(body->len && jm->body && !strcmp(body->str, jm->body))
200 g_string_printf(body, "<a href='%s'>%s</a>",
201 urltxt, desctxt);
202 else
203 g_string_append_printf(body, "<br/><a href='%s'>%s</a>",
204 urltxt, desctxt);
206 g_free(urltxt);
207 g_free(desctxt);
211 purple_notify_formatted(jm->js->gc, title, jm->subject ? jm->subject : title,
212 NULL, body->str, NULL, NULL);
214 g_free(title);
215 g_string_free(body, TRUE);
218 static void handle_groupchat(JabberMessage *jm)
220 JabberID *jid = jabber_id_new(jm->from);
221 JabberChat *chat;
222 PurpleMessageFlags messageFlags = 0;
224 if(!jid)
225 return;
227 chat = jabber_chat_find(jm->js, jid->node, jid->domain);
229 if(!chat)
230 return;
232 if(jm->subject) {
233 purple_chat_conversation_set_topic(chat->conv, jid->resource,
234 jm->subject);
235 messageFlags |= PURPLE_MESSAGE_NO_LOG;
236 if(!jm->xhtml && !jm->body) {
237 char *msg, *tmp, *tmp2;
238 tmp = g_markup_escape_text(jm->subject, -1);
239 tmp2 = purple_markup_linkify(tmp);
240 if(jid->resource)
241 msg = g_strdup_printf(_("%s has set the topic to: %s"), jid->resource, tmp2);
242 else
243 msg = g_strdup_printf(_("The topic is: %s"), tmp2);
244 purple_conversation_write_system_message(PURPLE_CONVERSATION(chat->conv),
245 msg, messageFlags);
246 g_free(tmp);
247 g_free(tmp2);
248 g_free(msg);
252 if(jm->xhtml || jm->body) {
253 if(jid->resource)
254 purple_serv_got_chat_in(jm->js->gc, chat->id, jid->resource,
255 messageFlags | (jm->delayed ? PURPLE_MESSAGE_DELAYED : 0),
256 jm->xhtml ? jm->xhtml : jm->body, jm->sent);
257 else if(chat->muc)
258 purple_conversation_write_system_message(
259 PURPLE_CONVERSATION(chat->conv),
260 jm->xhtml ? jm->xhtml : jm->body, messageFlags);
263 jabber_id_free(jid);
266 static void handle_groupchat_invite(JabberMessage *jm)
268 GHashTable *components;
269 JabberID *jid = jabber_id_new(jm->to);
271 if(!jid)
272 return;
274 components = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, g_free);
276 g_hash_table_replace(components, "room", g_strdup(jid->node));
277 g_hash_table_replace(components, "server", g_strdup(jid->domain));
278 g_hash_table_replace(components, "handle", g_strdup(jm->js->user->node));
279 g_hash_table_replace(components, "password", g_strdup(jm->password));
281 jabber_id_free(jid);
282 purple_serv_got_chat_invite(jm->js->gc, jm->to, jm->from, jm->body, components);
285 static void handle_error(JabberMessage *jm)
287 char *buf;
289 if(!jm->body)
290 return;
292 buf = g_strdup_printf(_("Message delivery to %s failed: %s"),
293 jm->from, jm->error ? jm->error : "");
295 purple_notify_formatted(jm->js->gc, _("XMPP Message Error"), _("XMPP Message Error"), buf,
296 jm->xhtml ? jm->xhtml : jm->body, NULL, NULL);
298 g_free(buf);
301 static void handle_buzz(JabberMessage *jm) {
302 PurpleAccount *account;
304 /* Delayed buzz MUST NOT be accepted */
305 if(jm->delayed)
306 return;
308 /* Reject buzz when it's not enabled */
309 if(!jm->js->allowBuzz)
310 return;
312 account = purple_connection_get_account(jm->js->gc);
314 if (purple_blist_find_buddy(account, jm->from) == NULL)
315 return; /* Do not accept buzzes from unknown people */
317 /* xmpp only has 1 attention type, so index is 0 */
318 purple_protocol_got_attention(jm->js->gc, jm->from, 0);
321 /* used internally by the functions below */
322 typedef struct {
323 gchar *cid;
324 gchar *alt;
325 } JabberSmileyRef;
328 static void
329 jabber_message_get_refs_from_xmlnode_internal(const PurpleXmlNode *message,
330 GHashTable *table)
332 PurpleXmlNode *child;
334 for (child = purple_xmlnode_get_child(message, "img") ; child ;
335 child = purple_xmlnode_get_next_twin(child)) {
336 const gchar *src = purple_xmlnode_get_attrib(child, "src");
338 if (g_str_has_prefix(src, "cid:")) {
339 const gchar *cid = src + 4;
341 /* if we haven't "fetched" this yet... */
342 if (!g_hash_table_lookup(table, cid)) {
343 /* take a copy of the cid and let the SmileyRef own it... */
344 gchar *temp_cid = g_strdup(cid);
345 JabberSmileyRef *ref = g_new0(JabberSmileyRef, 1);
346 const gchar *alt = purple_xmlnode_get_attrib(child, "alt");
347 ref->cid = temp_cid;
348 /* if there is no "alt" string, use the cid...
349 include the entire src, eg. "cid:.." to avoid linkification */
350 if (alt && alt[0] != '\0') {
351 /* workaround for when "alt" is set to the value of the
352 CID (which Jabbim seems to do), to avoid it showing up
353 as an mailto: link */
354 if (purple_email_is_valid(alt)) {
355 ref->alt = g_strdup_printf("smiley:%s", alt);
356 } else {
357 ref->alt = g_strdup(alt);
359 } else {
360 ref->alt = g_strdup(src);
362 g_hash_table_insert(table, temp_cid, ref);
367 for (child = message->child ; child ; child = child->next) {
368 jabber_message_get_refs_from_xmlnode_internal(child, table);
372 static gboolean
373 jabber_message_get_refs_steal(gpointer key, gpointer value, gpointer user_data)
375 GList **refs = (GList **) user_data;
376 JabberSmileyRef *ref = (JabberSmileyRef *) value;
378 *refs = g_list_append(*refs, ref);
380 return TRUE;
383 static GList *
384 jabber_message_get_refs_from_xmlnode(const PurpleXmlNode *message)
386 GList *refs = NULL;
387 GHashTable *unique_refs = g_hash_table_new(g_str_hash, g_str_equal);
389 jabber_message_get_refs_from_xmlnode_internal(message, unique_refs);
390 (void) g_hash_table_foreach_steal(unique_refs,
391 jabber_message_get_refs_steal, (gpointer) &refs);
392 g_hash_table_destroy(unique_refs);
393 return refs;
396 static gchar *
397 jabber_message_xml_to_string_strip_img_smileys(PurpleXmlNode *xhtml)
399 gchar *markup = purple_xmlnode_to_str(xhtml, NULL);
400 int len = strlen(markup);
401 int pos = 0;
402 GString *out = g_string_new(NULL);
404 while (pos < len) {
405 /* this is a bit cludgy, maybe there is a better way to do this...
406 we need to find all <img> tags within the XHTML and replace those
407 tags with the value of their "alt" attributes */
408 if (g_str_has_prefix(&(markup[pos]), "<img")) {
409 PurpleXmlNode *img = NULL;
410 int pos2 = pos;
411 const gchar *src;
413 for (; pos2 < len ; pos2++) {
414 if (g_str_has_prefix(&(markup[pos2]), "/>")) {
415 pos2 += 2;
416 break;
417 } else if (g_str_has_prefix(&(markup[pos2]), "</img>")) {
418 pos2 += 5;
419 break;
423 /* note, if the above loop didn't find the end of the <img> tag,
424 it the parsed string will be until the end of the input string,
425 in which case purple_xmlnode_from_str will bail out and return NULL,
426 in this case the "if" statement below doesn't trigger and the
427 text is copied unchanged */
428 img = purple_xmlnode_from_str(&(markup[pos]), pos2 - pos);
429 src = purple_xmlnode_get_attrib(img, "src");
431 if (g_str_has_prefix(src, "cid:")) {
432 const gchar *alt = purple_xmlnode_get_attrib(img, "alt");
433 /* if the "alt" attribute is empty, put the cid as smiley string */
434 if (alt && alt[0] != '\0') {
435 /* if the "alt" is the same as the CID, as Jabbim does,
436 this prevents linkification... */
437 if (purple_email_is_valid(alt)) {
438 gchar *safe_alt = g_strdup_printf("smiley:%s", alt);
439 out = g_string_append(out, safe_alt);
440 g_free(safe_alt);
441 } else {
442 gchar *alt_escaped = g_markup_escape_text(alt, -1);
443 out = g_string_append(out, alt_escaped);
444 g_free(alt_escaped);
446 } else {
447 out = g_string_append(out, src);
449 pos += pos2 - pos;
450 } else {
451 out = g_string_append_c(out, markup[pos]);
452 pos++;
455 purple_xmlnode_free(img);
457 } else {
458 out = g_string_append_c(out, markup[pos]);
459 pos++;
463 g_free(markup);
464 return g_string_free(out, FALSE);
467 static void
468 jabber_message_add_remote_smileys(JabberStream *js, const gchar *who,
469 const PurpleXmlNode *message)
471 PurpleXmlNode *data_tag;
472 for (data_tag = purple_xmlnode_get_child_with_namespace(message, "data", NS_BOB) ;
473 data_tag ;
474 data_tag = purple_xmlnode_get_next_twin(data_tag)) {
475 const gchar *cid = purple_xmlnode_get_attrib(data_tag, "cid");
476 const JabberData *data = jabber_data_find_remote_by_cid(js, who, cid);
478 if (!data && cid != NULL) {
479 /* we haven't cached this already, let's add it */
480 JabberData *new_data = jabber_data_create_from_xml(data_tag);
482 if (new_data) {
483 jabber_data_associate_remote(js, who, new_data);
489 static void
490 jabber_message_remote_smiley_got(JabberData *data, gchar *alt, gpointer _smiley)
492 PurpleSmiley *smiley = _smiley;
493 PurpleImage *image = purple_smiley_get_image(smiley);
495 g_free(alt); /* we really don't need it */
497 if (data) {
498 purple_debug_info("jabber",
499 "smiley data retrieved successfully");
500 purple_image_transfer_write(image, jabber_data_get_data(data),
501 jabber_data_get_size(data));
502 purple_image_transfer_close(image);
503 } else {
504 purple_debug_error("jabber", "failed retrieving smiley data");
505 purple_image_transfer_failed(image);
508 g_object_unref(smiley);
511 static void
512 jabber_message_remote_smiley_add(JabberStream *js, PurpleConversation *conv,
513 const gchar *from, const gchar *shortcut, const gchar *cid)
515 PurpleSmiley *smiley;
516 const JabberData *jdata;
518 purple_debug_misc("jabber", "about to add remote smiley %s to the conv",
519 shortcut);
521 smiley = purple_conversation_add_remote_smiley(conv, shortcut);
522 if (!smiley) {
523 purple_debug_misc("jabber", "smiley was already present");
524 return;
527 /* TODO: cache lookup by "cid" */
529 jdata = jabber_data_find_remote_by_cid(js, from, cid);
530 if (jdata) {
531 PurpleImage *image = purple_smiley_get_image(smiley);
533 purple_debug_info("jabber", "smiley data is already known");
535 purple_image_transfer_write(image, jabber_data_get_data(jdata),
536 jabber_data_get_size(jdata));
537 purple_image_transfer_close(image);
538 } else {
539 gchar *alt = g_strdup(shortcut); /* it it really necessary? */
541 purple_debug_info("jabber", "smiley data is unknown, "
542 "need to request it");
544 g_object_ref(smiley);
545 jabber_data_request(js, cid, from, alt, FALSE,
546 jabber_message_remote_smiley_got, smiley);
550 void jabber_message_parse(JabberStream *js, PurpleXmlNode *packet)
552 JabberMessage *jm;
553 const char *id, *from, *to, *type;
554 PurpleXmlNode *child;
555 gboolean signal_return;
557 from = purple_xmlnode_get_attrib(packet, "from");
558 id = purple_xmlnode_get_attrib(packet, "id");
559 to = purple_xmlnode_get_attrib(packet, "to");
560 type = purple_xmlnode_get_attrib(packet, "type");
562 signal_return = GPOINTER_TO_INT(purple_signal_emit_return_1(purple_connection_get_protocol(js->gc),
563 "jabber-receiving-message", js->gc, type, id, from, to, packet));
564 if (signal_return)
565 return;
567 jm = g_new0(JabberMessage, 1);
568 jm->js = js;
569 jm->sent = time(NULL);
570 jm->delayed = FALSE;
571 jm->chat_state = JM_STATE_NONE;
573 if(type) {
574 if(!strcmp(type, "normal"))
575 jm->type = JABBER_MESSAGE_NORMAL;
576 else if(!strcmp(type, "chat"))
577 jm->type = JABBER_MESSAGE_CHAT;
578 else if(!strcmp(type, "groupchat"))
579 jm->type = JABBER_MESSAGE_GROUPCHAT;
580 else if(!strcmp(type, "headline"))
581 jm->type = JABBER_MESSAGE_HEADLINE;
582 else if(!strcmp(type, "error"))
583 jm->type = JABBER_MESSAGE_ERROR;
584 else
585 jm->type = JABBER_MESSAGE_OTHER;
586 } else {
587 jm->type = JABBER_MESSAGE_NORMAL;
590 jm->from = g_strdup(from);
591 jm->to = g_strdup(to);
592 jm->id = g_strdup(id);
594 for(child = packet->child; child; child = child->next) {
595 const char *xmlns = purple_xmlnode_get_namespace(child);
596 if(child->type != PURPLE_XMLNODE_TYPE_TAG)
597 continue;
599 if(!strcmp(child->name, "error")) {
600 const char *code = purple_xmlnode_get_attrib(child, "code");
601 char *code_txt = NULL;
602 char *text = purple_xmlnode_get_data(child);
603 if (!text) {
604 PurpleXmlNode *enclosed_text_node;
606 if ((enclosed_text_node = purple_xmlnode_get_child(child, "text")))
607 text = purple_xmlnode_get_data(enclosed_text_node);
610 if(code)
611 code_txt = g_strdup_printf(_("(Code %s)"), code);
613 if(!jm->error)
614 jm->error = g_strdup_printf("%s%s%s",
615 text ? text : "",
616 text && code_txt ? " " : "",
617 code_txt ? code_txt : "");
619 g_free(code_txt);
620 g_free(text);
621 } else if (xmlns == NULL) {
622 /* QuLogic: Not certain this is correct, but it would have happened
623 with the previous code. */
624 if(!strcmp(child->name, "x"))
625 jm->etc = g_list_append(jm->etc, child);
626 /* The following tests expect xmlns != NULL */
627 continue;
628 } else if(!strcmp(child->name, "subject") && !strcmp(xmlns, NS_XMPP_CLIENT)) {
629 if(!jm->subject) {
630 jm->subject = purple_xmlnode_get_data(child);
631 if(!jm->subject)
632 jm->subject = g_strdup("");
634 } else if(!strcmp(child->name, "thread") && !strcmp(xmlns, NS_XMPP_CLIENT)) {
635 if(!jm->thread_id)
636 jm->thread_id = purple_xmlnode_get_data(child);
637 } else if(!strcmp(child->name, "body") && !strcmp(xmlns, NS_XMPP_CLIENT)) {
638 if(!jm->body) {
639 char *msg = purple_xmlnode_get_data(child);
640 char *escaped = purple_markup_escape_text(msg, -1);
641 jm->body = purple_strdup_withhtml(escaped);
642 g_free(escaped);
643 g_free(msg);
645 } else if(!strcmp(child->name, "html") && !strcmp(xmlns, NS_XHTML_IM)) {
646 if(!jm->xhtml && purple_xmlnode_get_child(child, "body")) {
647 char *c;
649 const PurpleConnection *gc = js->gc;
650 PurpleAccount *account = purple_connection_get_account(gc);
651 PurpleConversation *conv = NULL;
652 GList *smiley_refs = NULL, *it;
653 gchar *reformatted_xhtml;
655 if (purple_account_get_bool(account, "custom_smileys", TRUE)) {
656 /* find a list of smileys ("cid" and "alt" text pairs)
657 occuring in the message */
658 smiley_refs = jabber_message_get_refs_from_xmlnode(child);
659 purple_debug_info("jabber", "found %d smileys\n",
660 g_list_length(smiley_refs));
662 if (smiley_refs) {
663 if (jm->type == JABBER_MESSAGE_GROUPCHAT) {
664 JabberID *jid = jabber_id_new(jm->from);
665 JabberChat *chat = NULL;
667 if (jid) {
668 chat = jabber_chat_find(js, jid->node, jid->domain);
669 if (chat)
670 conv = PURPLE_CONVERSATION(chat->conv);
671 jabber_id_free(jid);
673 } else if (jm->type == JABBER_MESSAGE_NORMAL ||
674 jm->type == JABBER_MESSAGE_CHAT) {
675 conv =
676 purple_conversations_find_with_account(from, account);
677 if (!conv) {
678 /* we need to create the conversation here */
679 conv = PURPLE_CONVERSATION(
680 purple_im_conversation_new(account, from));
685 /* process any newly provided smileys */
686 jabber_message_add_remote_smileys(js, to, packet);
689 purple_xmlnode_strip_prefixes(child);
691 /* reformat xhtml so that img tags with a "cid:" src gets
692 translated to the bare text of the emoticon (the "alt" attrib) */
693 /* this is done also when custom smiley retrieval is turned off,
694 this way the receiver always sees the shortcut instead */
695 reformatted_xhtml =
696 jabber_message_xml_to_string_strip_img_smileys(child);
698 jm->xhtml = reformatted_xhtml;
700 /* add known custom emoticons to the conversation */
701 /* note: if there were no smileys in the incoming message, or
702 if receiving custom smileys is turned off, smiley_refs will
703 be NULL */
704 for (it = smiley_refs; it; it = g_list_next(it)) {
705 JabberSmileyRef *ref = it->data;
707 if (conv) {
708 jabber_message_remote_smiley_add(js,
709 conv, from, ref->alt, ref->cid);
712 g_free(ref->cid);
713 g_free(ref->alt);
714 g_free(ref);
716 g_list_free(smiley_refs);
718 /* Convert all newlines to whitespace. Technically, even regular, non-XML HTML is supposed to ignore newlines, but Pidgin has, as convention
719 * treated \n as a newline for compatibility with other protocols
721 for (c = jm->xhtml; *c != '\0'; c++) {
722 if (*c == '\n')
723 *c = ' ';
726 } else if(!strcmp(child->name, "active") && !strcmp(xmlns,"http://jabber.org/protocol/chatstates")) {
727 jm->chat_state = JM_STATE_ACTIVE;
728 } else if(!strcmp(child->name, "composing") && !strcmp(xmlns,"http://jabber.org/protocol/chatstates")) {
729 jm->chat_state = JM_STATE_COMPOSING;
730 } else if(!strcmp(child->name, "paused") && !strcmp(xmlns,"http://jabber.org/protocol/chatstates")) {
731 jm->chat_state = JM_STATE_PAUSED;
732 } else if(!strcmp(child->name, "inactive") && !strcmp(xmlns,"http://jabber.org/protocol/chatstates")) {
733 jm->chat_state = JM_STATE_INACTIVE;
734 } else if(!strcmp(child->name, "gone") && !strcmp(xmlns,"http://jabber.org/protocol/chatstates")) {
735 jm->chat_state = JM_STATE_GONE;
736 } else if(!strcmp(child->name, "event") && !strcmp(xmlns,"http://jabber.org/protocol/pubsub#event")) {
737 PurpleXmlNode *items;
738 jm->type = JABBER_MESSAGE_EVENT;
739 for(items = purple_xmlnode_get_child(child,"items"); items; items = items->next)
740 jm->eventitems = g_list_append(jm->eventitems, items);
741 } else if(!strcmp(child->name, "attention") && !strcmp(xmlns, NS_ATTENTION)) {
742 jm->hasBuzz = TRUE;
743 } else if(!strcmp(child->name, "delay") && !strcmp(xmlns, NS_DELAYED_DELIVERY)) {
744 const char *timestamp = purple_xmlnode_get_attrib(child, "stamp");
745 jm->delayed = TRUE;
746 if(timestamp)
747 jm->sent = purple_str_to_time(timestamp, TRUE, NULL, NULL, NULL);
748 } else if(!strcmp(child->name, "x")) {
749 if(!strcmp(xmlns, NS_DELAYED_DELIVERY_LEGACY)) {
750 const char *timestamp = purple_xmlnode_get_attrib(child, "stamp");
751 jm->delayed = TRUE;
752 if(timestamp)
753 jm->sent = purple_str_to_time(timestamp, TRUE, NULL, NULL, NULL);
754 } else if(!strcmp(xmlns, "jabber:x:conference") &&
755 jm->type != JABBER_MESSAGE_GROUPCHAT_INVITE &&
756 jm->type != JABBER_MESSAGE_ERROR) {
757 const char *jid = purple_xmlnode_get_attrib(child, "jid");
758 if(jid) {
759 const char *reason = purple_xmlnode_get_attrib(child, "reason");
760 const char *password = purple_xmlnode_get_attrib(child, "password");
762 jm->type = JABBER_MESSAGE_GROUPCHAT_INVITE;
763 g_free(jm->to);
764 jm->to = g_strdup(jid);
766 if (reason) {
767 g_free(jm->body);
768 jm->body = g_strdup(reason);
771 if (password) {
772 g_free(jm->password);
773 jm->password = g_strdup(password);
776 } else if(!strcmp(xmlns, "http://jabber.org/protocol/muc#user") &&
777 jm->type != JABBER_MESSAGE_ERROR) {
778 PurpleXmlNode *invite = purple_xmlnode_get_child(child, "invite");
779 if(invite) {
780 PurpleXmlNode *reason, *password;
781 const char *jid = purple_xmlnode_get_attrib(invite, "from");
782 g_free(jm->to);
783 jm->to = jm->from;
784 jm->from = g_strdup(jid);
785 if((reason = purple_xmlnode_get_child(invite, "reason"))) {
786 g_free(jm->body);
787 jm->body = purple_xmlnode_get_data(reason);
789 if((password = purple_xmlnode_get_child(child, "password"))) {
790 g_free(jm->password);
791 jm->password = purple_xmlnode_get_data(password);
794 jm->type = JABBER_MESSAGE_GROUPCHAT_INVITE;
796 } else {
797 jm->etc = g_list_append(jm->etc, child);
799 } else if (g_str_equal(child->name, "query")) {
800 const char *node = purple_xmlnode_get_attrib(child, "node");
801 if (purple_strequal(xmlns, NS_DISCO_ITEMS)
802 && purple_strequal(node, "http://jabber.org/protocol/commands")) {
803 jabber_adhoc_got_list(js, jm->from, child);
808 if(jm->hasBuzz)
809 handle_buzz(jm);
811 switch(jm->type) {
812 case JABBER_MESSAGE_OTHER:
813 purple_debug_info("jabber",
814 "Received message of unknown type: %s\n", type);
815 /* Fall-through is intentional */
816 case JABBER_MESSAGE_NORMAL:
817 case JABBER_MESSAGE_CHAT:
818 handle_chat(jm);
819 break;
820 case JABBER_MESSAGE_HEADLINE:
821 handle_headline(jm);
822 break;
823 case JABBER_MESSAGE_GROUPCHAT:
824 handle_groupchat(jm);
825 break;
826 case JABBER_MESSAGE_GROUPCHAT_INVITE:
827 handle_groupchat_invite(jm);
828 break;
829 case JABBER_MESSAGE_EVENT:
830 jabber_handle_event(jm);
831 break;
832 case JABBER_MESSAGE_ERROR:
833 handle_error(jm);
834 break;
836 jabber_message_free(jm);
839 static gboolean
840 jabber_conv_support_custom_smileys(JabberStream *js,
841 PurpleConversation *conv,
842 const gchar *who)
844 JabberBuddy *jb;
845 JabberChat *chat;
847 if (PURPLE_IS_IM_CONVERSATION(conv)) {
848 jb = jabber_buddy_find(js, who, FALSE);
849 if (jb) {
850 return jabber_buddy_has_capability(jb, NS_BOB);
851 } else {
852 return FALSE;
854 } else if (PURPLE_IS_CHAT_CONVERSATION(conv)) {
855 chat = jabber_chat_find_by_conv(PURPLE_CHAT_CONVERSATION(conv));
856 if (chat) {
857 /* do not attempt to send custom smileys in a MUC with more than
858 10 people, to avoid getting too many BoB requests */
859 return jabber_chat_get_num_participants(chat) <= 10 &&
860 jabber_chat_all_participants_have_capability(chat,
861 NS_BOB);
862 } else {
863 return FALSE;
865 } else {
866 return FALSE;
870 static gboolean
871 jabber_message_smileyify_cb(GString *out, PurpleSmiley *smiley,
872 PurpleConversation *_empty, gpointer _unused)
874 const gchar *shortcut;
875 const JabberData *data;
876 PurpleXmlNode *smiley_node;
877 gchar *node_xml;
879 shortcut = purple_smiley_get_shortcut(smiley);
880 data = jabber_data_find_local_by_alt(shortcut);
882 if (!data)
883 return FALSE;
885 smiley_node = jabber_data_get_xhtml_im(data, shortcut);
886 node_xml = purple_xmlnode_to_str(smiley_node, NULL);
888 g_string_append(out, node_xml);
890 purple_xmlnode_free(smiley_node);
891 g_free(node_xml);
893 return TRUE;
896 static char *
897 jabber_message_smileyfy_xhtml(JabberMessage *jm, const char *xhtml)
899 PurpleAccount *account = purple_connection_get_account(jm->js->gc);
900 GList *found_smileys, *it, *it_next;
901 PurpleConversation *conv;
902 gboolean has_too_large_smiley = FALSE;
903 gchar *smileyfied_xhtml = NULL;
905 conv = purple_conversations_find_with_account(jm->to, account);
907 if (!jabber_conv_support_custom_smileys(jm->js, conv, jm->to))
908 return NULL;
910 found_smileys = purple_smiley_parser_find(
911 purple_smiley_custom_get_list(), xhtml, TRUE);
912 if (!found_smileys)
913 return NULL;
915 for (it = found_smileys; it; it = it_next) {
916 PurpleSmiley *smiley = it->data;
917 PurpleImage *smiley_image;
918 gboolean valid = TRUE;
920 it_next = g_list_next(it);
922 smiley_image = purple_smiley_get_image(smiley);
923 if (!smiley_image) {
924 valid = FALSE;
925 purple_debug_warning("jabber", "broken smiley %s",
926 purple_smiley_get_shortcut(smiley));
927 } else if (purple_image_get_size(smiley_image) >
928 JABBER_DATA_MAX_SIZE)
930 has_too_large_smiley = TRUE;
931 valid = FALSE;
932 purple_debug_warning("jabber", "Refusing to send "
933 "smiley %s (too large, max is %d)",
934 purple_smiley_get_shortcut(smiley),
935 JABBER_DATA_MAX_SIZE);
938 if (!valid)
939 found_smileys = g_list_delete_link(found_smileys, it);
942 if (has_too_large_smiley) {
943 purple_conversation_write_system_message(conv,
944 _("A custom smiley in the message is too large to send."),
945 PURPLE_MESSAGE_ERROR);
948 if (!found_smileys)
949 return NULL;
951 for (it = found_smileys; it; it = g_list_next(it)) {
952 PurpleSmiley *smiley = it->data;
953 PurpleImage *smiley_image;
954 const gchar *shortcut = purple_smiley_get_shortcut(smiley);
955 const gchar *mimetype;
956 JabberData *jdata;
958 /* the object has been sent before */
959 if (jabber_data_find_local_by_alt(shortcut))
960 continue;
962 smiley_image = purple_smiley_get_image(smiley);
963 g_assert(smiley_image != NULL);
965 mimetype = purple_image_get_mimetype(smiley_image);
966 if (!mimetype) {
967 purple_debug_error("jabber",
968 "unknown mime type for image");
969 continue;
972 jdata = jabber_data_create_from_data(
973 purple_image_get_data(smiley_image),
974 purple_image_get_size(smiley_image),
975 mimetype, FALSE, jm->js);
977 purple_debug_info("jabber", "cache local smiley alt=%s, cid=%s",
978 shortcut, jabber_data_get_cid(jdata));
979 jabber_data_associate_local(jdata, shortcut);
982 g_list_free(found_smileys);
984 smileyfied_xhtml = purple_smiley_parser_replace(
985 purple_smiley_custom_get_list(), xhtml,
986 jabber_message_smileyify_cb, NULL);
988 return smileyfied_xhtml;
991 void jabber_message_send(JabberMessage *jm)
993 PurpleXmlNode *message, *child;
994 const char *type = NULL;
996 message = purple_xmlnode_new("message");
998 switch(jm->type) {
999 case JABBER_MESSAGE_NORMAL:
1000 type = "normal";
1001 break;
1002 case JABBER_MESSAGE_CHAT:
1003 case JABBER_MESSAGE_GROUPCHAT_INVITE:
1004 type = "chat";
1005 break;
1006 case JABBER_MESSAGE_HEADLINE:
1007 type = "headline";
1008 break;
1009 case JABBER_MESSAGE_GROUPCHAT:
1010 type = "groupchat";
1011 break;
1012 case JABBER_MESSAGE_ERROR:
1013 type = "error";
1014 break;
1015 case JABBER_MESSAGE_OTHER:
1016 default:
1017 type = NULL;
1018 break;
1021 if(type)
1022 purple_xmlnode_set_attrib(message, "type", type);
1024 if (jm->id)
1025 purple_xmlnode_set_attrib(message, "id", jm->id);
1027 purple_xmlnode_set_attrib(message, "to", jm->to);
1029 if(jm->thread_id) {
1030 child = purple_xmlnode_new_child(message, "thread");
1031 purple_xmlnode_insert_data(child, jm->thread_id, -1);
1034 child = NULL;
1035 switch(jm->chat_state)
1037 case JM_STATE_ACTIVE:
1038 child = purple_xmlnode_new_child(message, "active");
1039 break;
1040 case JM_STATE_COMPOSING:
1041 child = purple_xmlnode_new_child(message, "composing");
1042 break;
1043 case JM_STATE_PAUSED:
1044 child = purple_xmlnode_new_child(message, "paused");
1045 break;
1046 case JM_STATE_INACTIVE:
1047 child = purple_xmlnode_new_child(message, "inactive");
1048 break;
1049 case JM_STATE_GONE:
1050 child = purple_xmlnode_new_child(message, "gone");
1051 break;
1052 case JM_STATE_NONE:
1053 /* yep, nothing */
1054 break;
1056 if(child)
1057 purple_xmlnode_set_namespace(child, "http://jabber.org/protocol/chatstates");
1059 if(jm->subject) {
1060 child = purple_xmlnode_new_child(message, "subject");
1061 purple_xmlnode_insert_data(child, jm->subject, -1);
1064 if(jm->body) {
1065 child = purple_xmlnode_new_child(message, "body");
1066 purple_xmlnode_insert_data(child, jm->body, -1);
1069 if(jm->xhtml) {
1070 if ((child = purple_xmlnode_from_str(jm->xhtml, -1))) {
1071 purple_xmlnode_insert_child(message, child);
1072 } else {
1073 purple_debug_error("jabber",
1074 "XHTML translation/validation failed, returning: %s\n",
1075 jm->xhtml);
1079 jabber_send(jm->js, message);
1081 purple_xmlnode_free(message);
1085 * Compare the XHTML and plain strings passed in for "equality". Any HTML markup
1086 * other than <br/> (matches a newline) in the XHTML will cause this to return
1087 * FALSE.
1089 static gboolean
1090 jabber_xhtml_plain_equal(const char *xhtml_escaped,
1091 const char *plain)
1093 int i = 0;
1094 int j = 0;
1095 gboolean ret;
1096 char *xhtml = purple_unescape_html(xhtml_escaped);
1098 while (xhtml[i] && plain[j]) {
1099 if (xhtml[i] == plain[j]) {
1100 i += 1;
1101 j += 1;
1102 continue;
1105 if (plain[j] == '\n' && !strncmp(xhtml+i, "<br/>", 5)) {
1106 i += 5;
1107 j += 1;
1108 continue;
1111 g_free(xhtml);
1112 return FALSE;
1115 /* Are we at the end of both strings? */
1116 ret = (xhtml[i] == plain[j]) && (xhtml[i] == '\0');
1117 g_free(xhtml);
1118 return ret;
1121 int jabber_message_send_im(PurpleConnection *gc, PurpleMessage *msg)
1123 JabberMessage *jm;
1124 JabberBuddy *jb;
1125 JabberBuddyResource *jbr;
1126 char *xhtml;
1127 char *tmp;
1128 char *resource;
1129 const gchar *rcpt = purple_message_get_recipient(msg);
1131 if (!rcpt || purple_message_is_empty(msg))
1132 return 0;
1134 resource = jabber_get_resource(rcpt);
1136 jb = jabber_buddy_find(purple_connection_get_protocol_data(gc), rcpt, TRUE);
1137 jbr = jabber_buddy_find_resource(jb, resource);
1139 g_free(resource);
1141 jm = g_new0(JabberMessage, 1);
1142 jm->js = purple_connection_get_protocol_data(gc);
1143 jm->type = JABBER_MESSAGE_CHAT;
1144 jm->chat_state = JM_STATE_ACTIVE;
1145 jm->to = g_strdup(rcpt);
1146 jm->id = jabber_get_next_id(jm->js);
1148 if(jbr) {
1149 if(jbr->thread_id)
1150 jm->thread_id = jbr->thread_id;
1152 if (jbr->chat_states == JABBER_CHAT_STATES_UNSUPPORTED)
1153 jm->chat_state = JM_STATE_NONE;
1154 else {
1155 /* if(JABBER_CHAT_STATES_UNKNOWN == jbr->chat_states)
1156 jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED; */
1160 tmp = purple_utf8_strip_unprintables(purple_message_get_contents(msg));
1161 purple_markup_html_to_xhtml(tmp, &xhtml, &jm->body);
1162 g_free(tmp);
1164 tmp = jabber_message_smileyfy_xhtml(jm, xhtml);
1165 if (tmp) {
1166 g_free(xhtml);
1167 xhtml = tmp;
1171 * For backward compatibility with user expectations or for those not on
1172 * the user's roster, allow sending XHTML-IM markup.
1174 if (!jbr || !jbr->caps.info ||
1175 jabber_resource_has_capability(jbr, NS_XHTML_IM)) {
1176 if (!jabber_xhtml_plain_equal(xhtml, jm->body))
1177 /* Wrap the message in <p/> for great interoperability justice. */
1178 jm->xhtml = g_strdup_printf("<html xmlns='" NS_XHTML_IM "'><body xmlns='" NS_XHTML "'><p>%s</p></body></html>", xhtml);
1181 g_free(xhtml);
1183 jabber_message_send(jm);
1184 jabber_message_free(jm);
1185 return 1;
1188 int jabber_message_send_chat(PurpleConnection *gc, int id, PurpleMessage *msg)
1190 JabberChat *chat;
1191 JabberMessage *jm;
1192 JabberStream *js;
1193 char *xhtml;
1194 char *tmp;
1196 if (!gc || purple_message_is_empty(msg))
1197 return 0;
1199 js = purple_connection_get_protocol_data(gc);
1200 chat = jabber_chat_find_by_id(js, id);
1202 if(!chat)
1203 return 0;
1205 jm = g_new0(JabberMessage, 1);
1206 jm->js = purple_connection_get_protocol_data(gc);
1207 jm->type = JABBER_MESSAGE_GROUPCHAT;
1208 jm->to = g_strdup_printf("%s@%s", chat->room, chat->server);
1209 jm->id = jabber_get_next_id(jm->js);
1211 tmp = purple_utf8_strip_unprintables(purple_message_get_contents(msg));
1212 purple_markup_html_to_xhtml(tmp, &xhtml, &jm->body);
1213 g_free(tmp);
1214 tmp = jabber_message_smileyfy_xhtml(jm, xhtml);
1215 if (tmp) {
1216 g_free(xhtml);
1217 xhtml = tmp;
1220 if (chat->xhtml && !jabber_xhtml_plain_equal(xhtml, jm->body))
1221 /* Wrap the message in <p/> for greater interoperability justice. */
1222 jm->xhtml = g_strdup_printf("<html xmlns='" NS_XHTML_IM "'><body xmlns='" NS_XHTML "'><p>%s</p></body></html>", xhtml);
1224 g_free(xhtml);
1226 jabber_message_send(jm);
1227 jabber_message_free(jm);
1229 return 1;
1232 unsigned int jabber_send_typing(PurpleConnection *gc, const char *who, PurpleIMTypingState state)
1234 JabberStream *js;
1235 JabberMessage *jm;
1236 JabberBuddy *jb;
1237 JabberBuddyResource *jbr;
1238 char *resource;
1240 js = purple_connection_get_protocol_data(gc);
1241 jb = jabber_buddy_find(js, who, TRUE);
1242 if (!jb)
1243 return 0;
1245 resource = jabber_get_resource(who);
1246 jbr = jabber_buddy_find_resource(jb, resource);
1247 g_free(resource);
1249 /* We know this entity doesn't support chat states */
1250 if (jbr && jbr->chat_states == JABBER_CHAT_STATES_UNSUPPORTED)
1251 return 0;
1253 /* *If* we don't have presence /and/ the buddy can't see our
1254 * presence, don't send typing notifications.
1256 if (!jbr && !(jb->subscription & JABBER_SUB_FROM))
1257 return 0;
1259 /* TODO: figure out threading */
1260 jm = g_new0(JabberMessage, 1);
1261 jm->js = js;
1262 jm->type = JABBER_MESSAGE_CHAT;
1263 jm->to = g_strdup(who);
1264 jm->id = jabber_get_next_id(jm->js);
1266 if(PURPLE_IM_TYPING == state)
1267 jm->chat_state = JM_STATE_COMPOSING;
1268 else if(PURPLE_IM_TYPED == state)
1269 jm->chat_state = JM_STATE_PAUSED;
1270 else
1271 jm->chat_state = JM_STATE_ACTIVE;
1273 /* if(JABBER_CHAT_STATES_UNKNOWN == jbr->chat_states)
1274 jbr->chat_states = JABBER_CHAT_STATES_UNSUPPORTED; */
1276 jabber_message_send(jm);
1277 jabber_message_free(jm);
1279 return 0;
1282 gboolean jabber_buzz_isenabled(JabberStream *js, const gchar *namespace) {
1283 return js->allowBuzz;
1286 gboolean jabber_custom_smileys_isenabled(JabberStream *js, const gchar *namespace)
1288 const PurpleConnection *gc = js->gc;
1289 PurpleAccount *account = purple_connection_get_account(gc);
1291 return purple_account_get_bool(account, "custom_smileys", TRUE);