/src/pidgin/libpurple/protocols/jabber/useravatar.c
Line | Count | Source |
1 | | /* |
2 | | * purple - Jabber Protocol Plugin |
3 | | * |
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. |
7 | | * |
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. |
12 | | * |
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. |
17 | | * |
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 |
21 | | * |
22 | | */ |
23 | | |
24 | | #include "internal.h" |
25 | | |
26 | | #include "glibcompat.h" |
27 | | |
28 | | #include "useravatar.h" |
29 | | #include "pep.h" |
30 | | #include "debug.h" |
31 | | |
32 | | #define MAX_HTTP_BUDDYICON_BYTES (200 * 1024) |
33 | | |
34 | | static void update_buddy_metadata(JabberStream *js, const char *from, xmlnode *items); |
35 | | |
36 | | void jabber_avatar_init(void) |
37 | 0 | { |
38 | 0 | jabber_add_feature(NS_AVATAR_1_1_METADATA, |
39 | 0 | jabber_pep_namespace_only_when_pep_enabled_cb); |
40 | 0 | jabber_add_feature(NS_AVATAR_1_1_DATA, |
41 | 0 | jabber_pep_namespace_only_when_pep_enabled_cb); |
42 | |
|
43 | 0 | jabber_pep_register_handler(NS_AVATAR_1_1_METADATA, |
44 | 0 | update_buddy_metadata); |
45 | 0 | } |
46 | | |
47 | | static void |
48 | | remove_avatar_0_12_nodes(JabberStream *js) |
49 | 0 | { |
50 | | #if 0 |
51 | | /* See note below for why this is #if 0'd */ |
52 | | |
53 | | /* Publish an empty avatar according to the XEP-0084 v0.12 semantics */ |
54 | | xmlnode *publish, *item, *metadata; |
55 | | /* publish the metadata */ |
56 | | publish = xmlnode_new("publish"); |
57 | | xmlnode_set_attrib(publish, "node", NS_AVATAR_0_12_METADATA); |
58 | | |
59 | | item = xmlnode_new_child(publish, "item"); |
60 | | xmlnode_set_attrib(item, "id", "stop"); |
61 | | |
62 | | metadata = xmlnode_new_child(item, "metadata"); |
63 | | xmlnode_set_namespace(metadata, NS_AVATAR_0_12_METADATA); |
64 | | |
65 | | xmlnode_new_child(metadata, "stop"); |
66 | | |
67 | | /* publish */ |
68 | | jabber_pep_publish(js, publish); |
69 | | #endif |
70 | | |
71 | | /* |
72 | | * This causes ejabberd 2.0.0 to kill the connection unceremoniously. |
73 | | * See https://support.process-one.net/browse/EJAB-623. When adiumx.com |
74 | | * was upgraded, the issue went away. |
75 | | * |
76 | | * I think it makes a lot of sense to not have an avatar at the old |
77 | | * node instead of having something interpreted as "no avatar". When |
78 | | * a contact with an older client logs in, in the latter situation, |
79 | | * there's a race between interpreting the <presence/> vcard-temp:x:update |
80 | | * avatar (non-empty) and the XEP-0084 v0.12 avatar (empty, so show no |
81 | | * avatar for the buddy) which leads to unhappy and confused users. |
82 | | * |
83 | | * A deluge of frustrating "Read error" bug reports may change my mind |
84 | | * about this. |
85 | | * --darkrain42 |
86 | | */ |
87 | 0 | jabber_pep_delete_node(js, NS_AVATAR_0_12_METADATA); |
88 | 0 | jabber_pep_delete_node(js, NS_AVATAR_0_12_DATA); |
89 | 0 | } |
90 | | |
91 | | void jabber_avatar_set(JabberStream *js, PurpleStoredImage *img) |
92 | 0 | { |
93 | 0 | xmlnode *publish, *metadata, *item; |
94 | |
|
95 | 0 | if (!js->pep) |
96 | 0 | return; |
97 | | |
98 | | /* Hmmm, not sure if this is worth the traffic, but meh */ |
99 | 0 | remove_avatar_0_12_nodes(js); |
100 | |
|
101 | 0 | if (!img) { |
102 | 0 | publish = xmlnode_new("publish"); |
103 | 0 | xmlnode_set_attrib(publish, "node", NS_AVATAR_1_1_METADATA); |
104 | |
|
105 | 0 | item = xmlnode_new_child(publish, "item"); |
106 | 0 | metadata = xmlnode_new_child(item, "metadata"); |
107 | 0 | xmlnode_set_namespace(metadata, NS_AVATAR_1_1_METADATA); |
108 | | |
109 | | /* publish */ |
110 | 0 | jabber_pep_publish(js, publish); |
111 | 0 | } else { |
112 | | /* |
113 | | * TODO: This is pretty gross. The Jabber PRPL really shouldn't |
114 | | * do voodoo to try to determine the image type, height |
115 | | * and width. |
116 | | */ |
117 | | /* A PNG header, including the IHDR, but nothing else */ |
118 | | /* ATTN: this is in network byte order! */ |
119 | 0 | const struct { |
120 | 0 | guchar signature[8]; /* must be hex 89 50 4E 47 0D 0A 1A 0A */ |
121 | 0 | struct { |
122 | 0 | guint32 length; /* must be 0x0d */ |
123 | 0 | guchar type[4]; /* must be 'I' 'H' 'D' 'R' */ |
124 | 0 | guint32 width; |
125 | 0 | guint32 height; |
126 | 0 | guchar bitdepth; |
127 | 0 | guchar colortype; |
128 | 0 | guchar compression; |
129 | 0 | guchar filter; |
130 | 0 | guchar interlace; |
131 | 0 | } ihdr; |
132 | 0 | } *png = NULL; |
133 | |
|
134 | 0 | if (purple_imgstore_get_size(img) > sizeof(*png)) |
135 | 0 | png = purple_imgstore_get_data(img); |
136 | | |
137 | | /* check if the data is a valid png file (well, at least to some extent) */ |
138 | 0 | if(png && png->signature[0] == 0x89 && |
139 | 0 | png->signature[1] == 0x50 && |
140 | 0 | png->signature[2] == 0x4e && |
141 | 0 | png->signature[3] == 0x47 && |
142 | 0 | png->signature[4] == 0x0d && |
143 | 0 | png->signature[5] == 0x0a && |
144 | 0 | png->signature[6] == 0x1a && |
145 | 0 | png->signature[7] == 0x0a && |
146 | 0 | ntohl(png->ihdr.length) == 0x0d && |
147 | 0 | png->ihdr.type[0] == 'I' && |
148 | 0 | png->ihdr.type[1] == 'H' && |
149 | 0 | png->ihdr.type[2] == 'D' && |
150 | 0 | png->ihdr.type[3] == 'R') { |
151 | | /* parse PNG header to get the size of the image (yes, this is required) */ |
152 | 0 | guint32 width = ntohl(png->ihdr.width); |
153 | 0 | guint32 height = ntohl(png->ihdr.height); |
154 | 0 | xmlnode *data, *info; |
155 | 0 | char *lengthstring, *widthstring, *heightstring; |
156 | | |
157 | | /* compute the sha1 hash */ |
158 | 0 | char *hash = jabber_calculate_data_hash(purple_imgstore_get_data(img), |
159 | 0 | purple_imgstore_get_size(img), |
160 | 0 | "sha1"); |
161 | 0 | char *base64avatar = purple_base64_encode(purple_imgstore_get_data(img), |
162 | 0 | purple_imgstore_get_size(img)); |
163 | |
|
164 | 0 | publish = xmlnode_new("publish"); |
165 | 0 | xmlnode_set_attrib(publish, "node", NS_AVATAR_1_1_DATA); |
166 | |
|
167 | 0 | item = xmlnode_new_child(publish, "item"); |
168 | 0 | xmlnode_set_attrib(item, "id", hash); |
169 | |
|
170 | 0 | data = xmlnode_new_child(item, "data"); |
171 | 0 | xmlnode_set_namespace(data, NS_AVATAR_1_1_DATA); |
172 | |
|
173 | 0 | xmlnode_insert_data(data, base64avatar, -1); |
174 | | /* publish the avatar itself */ |
175 | 0 | jabber_pep_publish(js, publish); |
176 | |
|
177 | 0 | g_free(base64avatar); |
178 | |
|
179 | 0 | lengthstring = g_strdup_printf("%" G_GSIZE_FORMAT, |
180 | 0 | purple_imgstore_get_size(img)); |
181 | 0 | widthstring = g_strdup_printf("%u", width); |
182 | 0 | heightstring = g_strdup_printf("%u", height); |
183 | | |
184 | | /* publish the metadata */ |
185 | 0 | publish = xmlnode_new("publish"); |
186 | 0 | xmlnode_set_attrib(publish, "node", NS_AVATAR_1_1_METADATA); |
187 | |
|
188 | 0 | item = xmlnode_new_child(publish, "item"); |
189 | 0 | xmlnode_set_attrib(item, "id", hash); |
190 | |
|
191 | 0 | metadata = xmlnode_new_child(item, "metadata"); |
192 | 0 | xmlnode_set_namespace(metadata, NS_AVATAR_1_1_METADATA); |
193 | |
|
194 | 0 | info = xmlnode_new_child(metadata, "info"); |
195 | 0 | xmlnode_set_attrib(info, "id", hash); |
196 | 0 | xmlnode_set_attrib(info, "type", "image/png"); |
197 | 0 | xmlnode_set_attrib(info, "bytes", lengthstring); |
198 | 0 | xmlnode_set_attrib(info, "width", widthstring); |
199 | 0 | xmlnode_set_attrib(info, "height", heightstring); |
200 | |
|
201 | 0 | jabber_pep_publish(js, publish); |
202 | |
|
203 | 0 | g_free(lengthstring); |
204 | 0 | g_free(widthstring); |
205 | 0 | g_free(heightstring); |
206 | 0 | g_free(hash); |
207 | 0 | } else { |
208 | 0 | purple_debug_error("jabber", "Cannot set PEP avatar to non-PNG data\n"); |
209 | 0 | } |
210 | 0 | } |
211 | 0 | } |
212 | | |
213 | | static void |
214 | | do_got_own_avatar_0_12_cb(JabberStream *js, const char *from, xmlnode *items) |
215 | 0 | { |
216 | 0 | if (items) |
217 | | /* It wasn't an error (i.e. 'item-not-found') */ |
218 | 0 | remove_avatar_0_12_nodes(js); |
219 | 0 | } |
220 | | |
221 | | static void |
222 | | do_got_own_avatar_cb(JabberStream *js, const char *from, xmlnode *items) |
223 | 0 | { |
224 | 0 | xmlnode *item = NULL, *metadata = NULL, *info = NULL; |
225 | 0 | PurpleAccount *account = purple_connection_get_account(js->gc); |
226 | 0 | const char *server_hash = NULL; |
227 | |
|
228 | 0 | if (items && (item = xmlnode_get_child(items, "item")) && |
229 | 0 | (metadata = xmlnode_get_child(item, "metadata")) && |
230 | 0 | (info = xmlnode_get_child(metadata, "info"))) { |
231 | 0 | server_hash = xmlnode_get_attrib(info, "id"); |
232 | 0 | } |
233 | | |
234 | | /* |
235 | | * If we have an avatar and the server returned an error/malformed data, |
236 | | * push our avatar. If the server avatar doesn't match the local one, push |
237 | | * our avatar. |
238 | | */ |
239 | 0 | if ((!items || !metadata) || |
240 | 0 | !purple_strequal(server_hash, js->initial_avatar_hash)) { |
241 | 0 | PurpleStoredImage *img = purple_buddy_icons_find_account_icon(account); |
242 | 0 | jabber_avatar_set(js, img); |
243 | 0 | purple_imgstore_unref(img); |
244 | 0 | } |
245 | 0 | } |
246 | | |
247 | | void jabber_avatar_fetch_mine(JabberStream *js) |
248 | 0 | { |
249 | 0 | if (js->initial_avatar_hash) { |
250 | 0 | jabber_pep_request_item(js, NULL, NS_AVATAR_0_12_METADATA, NULL, |
251 | 0 | do_got_own_avatar_0_12_cb); |
252 | 0 | jabber_pep_request_item(js, NULL, NS_AVATAR_1_1_METADATA, NULL, |
253 | 0 | do_got_own_avatar_cb); |
254 | 0 | } |
255 | 0 | } |
256 | | |
257 | | typedef struct _JabberBuddyAvatarUpdateURLInfo { |
258 | | JabberStream *js; |
259 | | char *from; |
260 | | char *id; |
261 | | } JabberBuddyAvatarUpdateURLInfo; |
262 | | |
263 | | static void |
264 | | do_buddy_avatar_update_fromurl(PurpleUtilFetchUrlData *url_data, |
265 | | gpointer user_data, const gchar *url_text, |
266 | | gsize len, const gchar *error_message) |
267 | 0 | { |
268 | 0 | JabberBuddyAvatarUpdateURLInfo *info = user_data; |
269 | 0 | gpointer icon_data; |
270 | |
|
271 | 0 | if(!url_text) { |
272 | 0 | purple_debug_error("jabber", |
273 | 0 | "do_buddy_avatar_update_fromurl got error \"%s\"", |
274 | 0 | error_message); |
275 | 0 | goto out; |
276 | 0 | } |
277 | | |
278 | 0 | icon_data = g_memdup2(url_text, len); |
279 | 0 | purple_buddy_icons_set_for_user(purple_connection_get_account(info->js->gc), info->from, icon_data, len, info->id); |
280 | |
|
281 | 0 | out: |
282 | 0 | g_free(info->from); |
283 | 0 | g_free(info->id); |
284 | 0 | g_free(info); |
285 | 0 | } |
286 | | |
287 | | static void |
288 | | do_buddy_avatar_update_data(JabberStream *js, const char *from, xmlnode *items) |
289 | 0 | { |
290 | 0 | xmlnode *item, *data; |
291 | 0 | const char *checksum; |
292 | 0 | char *b64data; |
293 | 0 | void *img; |
294 | 0 | size_t size; |
295 | 0 | if(!items) |
296 | 0 | return; |
297 | | |
298 | 0 | item = xmlnode_get_child(items, "item"); |
299 | 0 | if(!item) |
300 | 0 | return; |
301 | | |
302 | 0 | data = xmlnode_get_child(item, "data"); |
303 | 0 | if(!data) |
304 | 0 | return; |
305 | | |
306 | 0 | checksum = xmlnode_get_attrib(item,"id"); |
307 | 0 | if(!checksum) |
308 | 0 | return; |
309 | | |
310 | 0 | b64data = xmlnode_get_data(data); |
311 | 0 | if(!b64data) |
312 | 0 | return; |
313 | | |
314 | 0 | img = purple_base64_decode(b64data, &size); |
315 | 0 | if(!img) { |
316 | 0 | g_free(b64data); |
317 | 0 | return; |
318 | 0 | } |
319 | | |
320 | 0 | purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, img, size, checksum); |
321 | 0 | g_free(b64data); |
322 | 0 | } |
323 | | |
324 | | static void |
325 | | update_buddy_metadata(JabberStream *js, const char *from, xmlnode *items) |
326 | 0 | { |
327 | 0 | PurpleBuddy *buddy = purple_find_buddy(purple_connection_get_account(js->gc), from); |
328 | 0 | const char *checksum; |
329 | 0 | xmlnode *item, *metadata; |
330 | 0 | if(!buddy) |
331 | 0 | return; |
332 | | |
333 | 0 | if (!items) |
334 | 0 | return; |
335 | | |
336 | 0 | item = xmlnode_get_child(items,"item"); |
337 | 0 | if (!item) |
338 | 0 | return; |
339 | | |
340 | 0 | metadata = xmlnode_get_child(item, "metadata"); |
341 | 0 | if(!metadata) |
342 | 0 | return; |
343 | | |
344 | 0 | checksum = purple_buddy_icons_get_checksum_for_user(buddy); |
345 | | |
346 | | /* <stop/> was the pre-v1.1 method of publishing an empty avatar */ |
347 | 0 | if(xmlnode_get_child(metadata, "stop")) { |
348 | 0 | purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, NULL, 0, NULL); |
349 | 0 | } else { |
350 | 0 | xmlnode *info, *goodinfo = NULL; |
351 | 0 | gboolean has_children = FALSE; |
352 | | |
353 | | /* iterate over all info nodes to get one we can use */ |
354 | 0 | for(info = metadata->child; info; info = info->next) { |
355 | 0 | if(info->type == XMLNODE_TYPE_TAG) |
356 | 0 | has_children = TRUE; |
357 | 0 | if(info->type == XMLNODE_TYPE_TAG && purple_strequal(info->name,"info")) { |
358 | 0 | const char *type = xmlnode_get_attrib(info,"type"); |
359 | 0 | const char *id = xmlnode_get_attrib(info,"id"); |
360 | |
|
361 | 0 | if(checksum && id && purple_strequal(id, checksum)) { |
362 | | /* we already have that avatar, so we don't have to do anything */ |
363 | 0 | goodinfo = NULL; |
364 | 0 | break; |
365 | 0 | } |
366 | | /* We'll only pick the png one for now. It's a very nice image format anyways. */ |
367 | 0 | if(id && !goodinfo && purple_strequal(type, "image/png")) |
368 | 0 | goodinfo = info; |
369 | 0 | } |
370 | 0 | } |
371 | 0 | if(has_children == FALSE) { |
372 | 0 | purple_buddy_icons_set_for_user(purple_connection_get_account(js->gc), from, NULL, 0, NULL); |
373 | 0 | } else if(goodinfo) { |
374 | 0 | const char *url = xmlnode_get_attrib(goodinfo, "url"); |
375 | 0 | const char *id = xmlnode_get_attrib(goodinfo,"id"); |
376 | | |
377 | | /* the avatar might either be stored in a pep node, or on a HTTP(S) URL */ |
378 | 0 | if(!url) { |
379 | 0 | jabber_pep_request_item(js, from, NS_AVATAR_1_1_DATA, id, |
380 | 0 | do_buddy_avatar_update_data); |
381 | 0 | } else { |
382 | 0 | PurpleUtilFetchUrlData *url_data; |
383 | 0 | JabberBuddyAvatarUpdateURLInfo *info = g_new0(JabberBuddyAvatarUpdateURLInfo, 1); |
384 | 0 | info->js = js; |
385 | |
|
386 | 0 | url_data = purple_util_fetch_url_len(url, TRUE, NULL, TRUE, |
387 | 0 | MAX_HTTP_BUDDYICON_BYTES, |
388 | 0 | do_buddy_avatar_update_fromurl, info); |
389 | 0 | if (url_data) { |
390 | 0 | info->from = g_strdup(from); |
391 | 0 | info->id = g_strdup(id); |
392 | 0 | js->url_datas = g_slist_prepend(js->url_datas, url_data); |
393 | 0 | } else |
394 | 0 | g_free(info); |
395 | |
|
396 | 0 | } |
397 | 0 | } |
398 | 0 | } |
399 | 0 | } |