Coverage Report

Created: 2026-10-13 06:48

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}