ucs: add AddNewImContactToGroup operation
[siplcs.git] / src / core / sipe-ucs.c
blob0aa66fced606945e92b7993be2a329b8973194a4
1 /**
2 * @file sipe-ucs.c
4 * pidgin-sipe
6 * Copyright (C) 2013 SIPE Project <http://sipe.sourceforge.net/>
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
24 * Implementation for Unified Contact Store [MS-OXWSCOS]
25 * <http://msdn.microsoft.com/en-us/library/jj194130.aspx>
28 #include <string.h>
30 #include <glib.h>
32 #include "sipe-backend.h"
33 #include "sipe-buddy.h"
34 #include "sipe-common.h"
35 #include "sipe-core.h"
36 #include "sipe-core-private.h"
37 #include "sipe-digest.h"
38 #include "sipe-ews-autodiscover.h"
39 #include "sipe-group.h"
40 #include "sipe-http.h"
41 #include "sipe-subscriptions.h"
42 #include "sipe-ucs.h"
43 #include "sipe-utils.h"
44 #include "sipe-xml.h"
46 typedef void (ucs_callback)(struct sipe_core_private *sipe_private,
47 const sipe_xml *body,
48 gpointer callback_data);
50 struct ucs_deferred {
51 ucs_callback *cb;
52 gpointer cb_data;
53 gchar *body;
56 struct ucs_request {
57 ucs_callback *cb;
58 gpointer cb_data;
59 struct sipe_http_request *request;
62 struct sipe_ucs {
63 gchar *ews_url;
64 GSList *deferred_requests;
65 GSList *pending_requests;
66 guint group_id;
67 gboolean migrated;
68 gboolean shutting_down;
71 static void sipe_ucs_deferred_free(struct sipe_core_private *sipe_private,
72 struct ucs_deferred *data)
74 if (data->cb)
75 /* Callback: aborted */
76 (*data->cb)(sipe_private, NULL, data->cb_data);
77 g_free(data->body);
78 g_free(data);
81 static void sipe_ucs_request_free(struct sipe_core_private *sipe_private,
82 struct ucs_request *data)
84 if (data->request)
85 sipe_http_request_cancel(data->request);
86 if (data->cb)
87 /* Callback: aborted */
88 (*data->cb)(sipe_private, NULL, data->cb_data);
89 g_free(data);
92 static void sipe_ucs_http_response(struct sipe_core_private *sipe_private,
93 guint status,
94 SIPE_UNUSED_PARAMETER GSList *headers,
95 const gchar *body,
96 gpointer callback_data)
98 struct ucs_request *data = callback_data;
99 struct sipe_ucs *ucs = sipe_private->ucs;
101 SIPE_DEBUG_INFO("sipe_ucs_http_response: code %d", status);
102 data->request = NULL;
104 if ((status == SIPE_HTTP_STATUS_OK) && body) {
105 sipe_xml *xml = sipe_xml_parse(body, strlen(body));
106 const sipe_xml *soap_body = sipe_xml_child(xml, "Body");
107 /* Callback: success */
108 (*data->cb)(sipe_private, soap_body, data->cb_data);
109 sipe_xml_free(xml);
110 } else {
111 /* Callback: failed */
112 (*data->cb)(sipe_private, NULL, data->cb_data);
115 /* already been called */
116 data->cb = NULL;
118 ucs->pending_requests = g_slist_remove(ucs->pending_requests,
119 data);
120 sipe_ucs_request_free(sipe_private, data);
123 static gboolean sipe_ucs_http_request(struct sipe_core_private *sipe_private,
124 const gchar *body,
125 ucs_callback *callback,
126 gpointer callback_data)
128 struct sipe_ucs *ucs = sipe_private->ucs;
129 gboolean success = FALSE;
131 if (ucs->shutting_down) {
132 SIPE_DEBUG_ERROR("sipe_ucs_http_request: new UCS request during shutdown: THIS SHOULD NOT HAPPEN! Debugging information:\n"
133 "Body: %s\n",
134 body ? body : "<EMPTY>");
136 } else if (ucs->ews_url) {
137 struct ucs_request *data = g_new0(struct ucs_request, 1);
138 gchar *soap = g_strdup_printf("<?xml version=\"1.0\"?>\r\n"
139 "<soap:Envelope"
140 " xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\""
141 " xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""
142 " xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\""
143 " >"
144 " <soap:Header>"
145 " <t:RequestServerVersion Version=\"Exchange2013\" />"
146 " </soap:Header>"
147 " <soap:Body>"
148 " %s"
149 " </soap:Body>"
150 "</soap:Envelope>",
151 body);
152 struct sipe_http_request *request = sipe_http_request_post(sipe_private,
153 ucs->ews_url,
154 NULL,
155 soap,
156 "text/xml; charset=UTF-8",
157 sipe_ucs_http_response,
158 data);
159 g_free(soap);
161 if (request) {
162 data->cb = callback;
163 data->cb_data = callback_data;
164 data->request = request;
166 ucs->pending_requests = g_slist_prepend(ucs->pending_requests,
167 data);
169 sipe_core_email_authentication(sipe_private,
170 request);
171 sipe_http_request_allow_redirect(request);
172 sipe_http_request_ready(request);
174 success = TRUE;
175 } else {
176 SIPE_DEBUG_ERROR_NOFORMAT("sipe_ucs_http_request: failed to create HTTP connection");
177 g_free(data);
180 } else {
181 struct ucs_deferred *data = g_new0(struct ucs_deferred, 1);
182 data->cb = callback;
183 data->cb_data = callback_data;
184 data->body = g_strdup(body);
186 ucs->deferred_requests = g_slist_prepend(ucs->deferred_requests,
187 data);
188 success = TRUE;
191 return(success);
194 static void sipe_ucs_get_user_photo_response(struct sipe_core_private *sipe_private,
195 const sipe_xml *body,
196 gpointer callback_data)
198 gchar *uri = callback_data;
199 const sipe_xml *node = sipe_xml_child(body,
200 "GetUserPhotoResponse/PictureData");
202 if (node) {
203 gchar *base64;
204 gsize photo_size;
205 guchar *photo;
206 guchar digest[SIPE_DIGEST_SHA1_LENGTH];
207 gchar *digest_string;
209 /* decode photo data */
210 base64 = sipe_xml_data(node);
211 photo = g_base64_decode(base64, &photo_size);
212 g_free(base64);
214 /* EWS doesn't provide a hash -> calculate SHA-1 digest */
215 sipe_digest_sha1(photo, photo_size, digest);
216 digest_string = buff_to_hex_str(digest,
217 SIPE_DIGEST_SHA1_LENGTH);
219 /* backend frees "photo" */
220 sipe_backend_buddy_set_photo(SIPE_CORE_PUBLIC,
221 uri,
222 photo,
223 photo_size,
224 digest_string);
225 g_free(digest_string);
228 g_free(uri);
231 void sipe_ucs_get_photo(struct sipe_core_private *sipe_private,
232 const gchar *uri)
234 gchar *payload = g_strdup(uri);
235 gchar *body = g_strdup_printf("<m:GetUserPhoto>"
236 " <m:Email>%s</m:Email>"
237 " <m:SizeRequested>HR48x48</m:SizeRequested>"
238 "</m:GetUserPhoto>",
239 sipe_get_no_sip_uri(uri));
241 if (!sipe_ucs_http_request(sipe_private,
242 body,
243 sipe_ucs_get_user_photo_response,
244 payload))
245 g_free(payload);
247 g_free(body);
250 static void sipe_ucs_ignore_response(SIPE_UNUSED_PARAMETER struct sipe_core_private *sipe_private,
251 SIPE_UNUSED_PARAMETER const sipe_xml *body,
252 SIPE_UNUSED_PARAMETER gpointer callback_data)
254 SIPE_DEBUG_INFO_NOFORMAT("sipe_ucs_ignore_response: done");
257 static void ucs_extract_keys(const sipe_xml *persona_node,
258 const gchar **key,
259 const gchar **change)
261 const sipe_xml *attr_node;
263 /* extract Exchange key - not sure if this is correct */
264 for (attr_node = sipe_xml_child(persona_node,
265 "Attributions/Attribution");
266 attr_node;
267 attr_node = sipe_xml_twin(attr_node)) {
268 const sipe_xml *id_node = sipe_xml_child(attr_node,
269 "SourceId");
270 gchar *type = sipe_xml_data(sipe_xml_child(attr_node,
271 "DisplayName"));
272 if (id_node &&
273 sipe_strequal(type, "Lync Contacts")) {
274 *key = sipe_xml_attribute(id_node, "Id");
275 *change = sipe_xml_attribute(id_node, "ChangeKey");
276 g_free(type);
277 break;
279 g_free(type);
283 static void sipe_ucs_add_new_im_contact_to_group_response(struct sipe_core_private *sipe_private,
284 const sipe_xml *body,
285 gpointer callback_data)
287 gchar *who = callback_data;
288 struct sipe_buddy *buddy = sipe_buddy_find_by_uri(sipe_private, who);
289 const sipe_xml *persona_node = sipe_xml_child(body,
290 "AddNewImContactToGroupResponse/Persona");
292 if (persona_node &&
293 buddy &&
294 is_empty(buddy->exchange_key) &&
295 is_empty(buddy->change_key)) {
296 const gchar *key = NULL;
297 const gchar *change = NULL;
299 ucs_extract_keys(persona_node, &key, &change);
301 if (!is_empty(key) && !is_empty(change)) {
303 sipe_buddy_add_keys(sipe_private,
304 buddy,
305 key,
306 change);
308 SIPE_DEBUG_INFO("sipe_ucs_add_new_im_contact_to_group_response: persona URI '%s' key '%s' change '%s'",
309 buddy->name, key, change);
313 g_free(who);
316 void sipe_ucs_group_add_buddy(struct sipe_core_private *sipe_private,
317 struct sipe_group *group,
318 struct sipe_buddy *buddy)
320 /* existing or new byddy? */
321 if (buddy->exchange_key) {
322 gchar *body = g_strdup_printf("<m:AddImContactToGroup>"
323 " <m:ContactId Id=\"%s\" ChangeKey=\"%s\"/>"
324 " <m:GroupId Id=\"%s\" ChangeKey=\"%s\"/>"
325 "</m:AddImContactToGroup>",
326 buddy->exchange_key,
327 buddy->change_key,
328 group->exchange_key,
329 group->change_key);
331 sipe_ucs_http_request(sipe_private,
332 body,
333 sipe_ucs_ignore_response,
334 NULL);
335 g_free(body);
336 } else {
337 gchar *body = g_strdup_printf("<m:AddNewImContactToGroup>"
338 " <m:ImAddress>%s</m:ImAddress>"
339 " <m:GroupId Id=\"%s\" ChangeKey=\"%s\"/>"
340 "</m:AddNewImContactToGroup>",
341 sipe_get_no_sip_uri(buddy->name),
342 group->exchange_key,
343 group->change_key);
345 sipe_ucs_http_request(sipe_private,
346 body,
347 sipe_ucs_add_new_im_contact_to_group_response,
348 g_strdup(buddy->name));
349 g_free(body);
353 void sipe_ucs_group_remove_buddy(struct sipe_core_private *sipe_private,
354 struct sipe_group *group,
355 struct sipe_buddy *buddy)
357 if (group) {
359 * If a contact is removed from last group, it will also be
360 * removed from contact list completely. The documentation has
361 * a RemoveContactFromImList operation, but that doesn't seem
362 * to work at all, i.e. it is always rejected by the server.
364 gchar *body = g_strdup_printf("<m:RemoveImContactFromGroup>"
365 " <m:ContactId Id=\"%s\" ChangeKey=\"%s\"/>"
366 " <m:GroupId Id=\"%s\" ChangeKey=\"%s\"/>"
367 "</m:RemoveImContactFromGroup>",
368 buddy->exchange_key,
369 buddy->change_key,
370 group->exchange_key,
371 group->change_key);
373 sipe_ucs_http_request(sipe_private,
374 body,
375 sipe_ucs_ignore_response,
376 NULL);
377 g_free(body);
381 static struct sipe_group *ucs_create_group(struct sipe_core_private *sipe_private,
382 const sipe_xml *group_node)
384 const sipe_xml *id_node = sipe_xml_child(group_node,
385 "ExchangeStoreId");
386 const gchar *key = sipe_xml_attribute(id_node, "Id");
387 const gchar *change = sipe_xml_attribute(id_node, "ChangeKey");
388 struct sipe_group *group = NULL;
390 if (!(is_empty(key) || is_empty(change))) {
391 gchar *name = sipe_xml_data(sipe_xml_child(group_node,
392 "DisplayName"));
393 group = sipe_group_add(sipe_private,
394 name,
395 key,
396 change,
397 /* sipe_group must have unique ID */
398 ++sipe_private->ucs->group_id);
399 g_free(name);
402 return(group);
405 static void sipe_ucs_add_im_group_response(struct sipe_core_private *sipe_private,
406 const sipe_xml *body,
407 gpointer callback_data)
409 gchar *who = callback_data;
410 const sipe_xml *group_node = sipe_xml_child(body,
411 "AddImGroupResponse/ImGroup");
412 struct sipe_group *group = ucs_create_group(sipe_private, group_node);
414 if (group) {
415 struct sipe_buddy *buddy = sipe_buddy_find_by_uri(sipe_private,
416 who);
418 if (buddy) {
419 sipe_buddy_insert_group(buddy, group);
420 sipe_ucs_group_add_buddy(sipe_private,
421 group,
422 buddy);
426 g_free(who);
429 void sipe_ucs_group_create(struct sipe_core_private *sipe_private,
430 const gchar *name,
431 const gchar *who)
433 /* new_name can contain restricted characters */
434 gchar *body = g_markup_printf_escaped("<m:AddImGroup>"
435 " <m:DisplayName>%s</m:DisplayName>"
436 "</m:AddImGroup>",
437 name);
439 sipe_ucs_http_request(sipe_private,
440 body,
441 sipe_ucs_add_im_group_response,
442 g_strdup(who));
443 g_free(body);
446 void sipe_ucs_group_rename(struct sipe_core_private *sipe_private,
447 struct sipe_group *group,
448 const gchar *new_name)
450 /* new_name can contain restricted characters */
451 gchar *body = g_markup_printf_escaped("<m:SetImGroup>"
452 " <m:GroupId Id=\"%s\" ChangeKey=\"%s\"/>"
453 " <m:NewDisplayName>%s</m:NewDisplayName>"
454 "</m:SetImGroup>",
455 group->exchange_key,
456 group->change_key,
457 new_name);
459 sipe_ucs_http_request(sipe_private,
460 body,
461 sipe_ucs_ignore_response,
462 NULL);
463 g_free(body);
466 void sipe_ucs_group_remove(struct sipe_core_private *sipe_private,
467 struct sipe_group *group)
469 gchar *body = g_strdup_printf("<m:RemoveImGroup>"
470 " <m:GroupId Id=\"%s\" ChangeKey=\"%s\"/>"
471 "</m:RemoveImGroup>",
472 group->exchange_key,
473 group->change_key);
475 sipe_ucs_http_request(sipe_private,
476 body,
477 sipe_ucs_ignore_response,
478 NULL);
479 g_free(body);
482 static void sipe_ucs_get_im_item_list_response(struct sipe_core_private *sipe_private,
483 const sipe_xml *body,
484 SIPE_UNUSED_PARAMETER gpointer callback_data)
486 const sipe_xml *node = sipe_xml_child(body,
487 "GetImItemListResponse/ImItemList");
489 if (node) {
490 const sipe_xml *persona_node;
491 const sipe_xml *group_node;
493 /* Start processing contact list */
494 if (SIPE_CORE_PRIVATE_FLAG_IS(SUBSCRIBED_BUDDIES)) {
495 sipe_group_update_start(sipe_private);
496 sipe_buddy_update_start(sipe_private);
497 } else
498 sipe_backend_buddy_list_processing_start(SIPE_CORE_PUBLIC);
500 for (persona_node = sipe_xml_child(node, "Personas/Persona");
501 persona_node;
502 persona_node = sipe_xml_twin(persona_node)) {
503 gchar *address = sipe_xml_data(sipe_xml_child(persona_node,
504 "ImAddress"));
505 const gchar *key = NULL;
506 const gchar *change = NULL;
508 ucs_extract_keys(persona_node, &key, &change);
510 if (!(is_empty(address) || is_empty(key) || is_empty(change))) {
511 gchar *uri = sip_uri_from_name(address);
512 struct sipe_buddy *buddy = sipe_buddy_add(sipe_private,
513 uri,
514 key,
515 change);
516 g_free(uri);
518 SIPE_DEBUG_INFO("sipe_ucs_get_im_item_list_response: persona URI '%s' key '%s' change '%s'",
519 buddy->name, key, change);
521 g_free(address);
524 for (group_node = sipe_xml_child(node, "Groups/ImGroup");
525 group_node;
526 group_node = sipe_xml_twin(group_node)) {
527 struct sipe_group *group = ucs_create_group(sipe_private,
528 group_node);
530 if (group) {
531 const sipe_xml *member_node;
533 for (member_node = sipe_xml_child(group_node,
534 "MemberCorrelationKey/ItemId");
535 member_node;
536 member_node = sipe_xml_twin(member_node)) {
537 struct sipe_buddy *buddy = sipe_buddy_find_by_exchange_key(sipe_private,
538 sipe_xml_attribute(member_node,
539 "Id"));
540 if (buddy)
541 sipe_buddy_add_to_group(sipe_private,
542 buddy,
543 group,
544 /* alias will be set via buddy presence update */
545 NULL);
550 /* Finished processing contact list */
551 if (SIPE_CORE_PRIVATE_FLAG_IS(SUBSCRIBED_BUDDIES)) {
552 sipe_buddy_update_finish(sipe_private);
553 sipe_group_update_finish(sipe_private);
554 } else {
555 sipe_buddy_cleanup_local_list(sipe_private);
556 sipe_backend_buddy_list_processing_finish(SIPE_CORE_PUBLIC);
557 sipe_subscribe_presence_initial(sipe_private);
562 static void ucs_get_im_item_list(struct sipe_core_private *sipe_private)
564 if (sipe_private->ucs->migrated)
565 sipe_ucs_http_request(sipe_private,
566 "<m:GetImItemList/>",
567 sipe_ucs_get_im_item_list_response,
568 NULL);
571 static void ucs_ews_autodiscover_cb(struct sipe_core_private *sipe_private,
572 const struct sipe_ews_autodiscover_data *ews_data,
573 SIPE_UNUSED_PARAMETER gpointer callback_data)
575 struct sipe_ucs *ucs = sipe_private->ucs;
576 const gchar *ews_url;
578 if (!ucs || !ews_data)
579 return;
581 ews_url = ews_data->ews_url;
582 if (is_empty(ews_url)) {
583 SIPE_DEBUG_ERROR_NOFORMAT("ucs_ews_autodiscover_cb: can't detect EWS URL, contact list operations will not work!");
584 return;
587 SIPE_DEBUG_INFO("ucs_ews_autodiscover_cb: EWS URL '%s'", ews_url);
588 ucs->ews_url = g_strdup(ews_url);
590 ucs_get_im_item_list(sipe_private);
592 /* EWS URL is valid, send all deferred requests now */
593 if (ucs->deferred_requests) {
594 GSList *entry = ucs->deferred_requests;
595 while (entry) {
596 struct ucs_deferred *data = entry->data;
598 sipe_ucs_http_request(sipe_private,
599 data->body,
600 data->cb,
601 data->cb_data);
603 /* callback & data has been forwarded */
604 data->cb = NULL;
605 sipe_ucs_deferred_free(sipe_private, data);
607 entry = entry->next;
609 g_slist_free(ucs->deferred_requests);
610 ucs->deferred_requests = NULL;
614 gboolean sipe_ucs_is_migrated(struct sipe_core_private *sipe_private)
616 return(sipe_private->ucs ? sipe_private->ucs->migrated : FALSE);
619 void sipe_ucs_init(struct sipe_core_private *sipe_private,
620 gboolean migrated)
622 struct sipe_ucs *ucs;
624 if (sipe_private->ucs) {
625 /* contact list update trigger -> request list again */
626 if (SIPE_CORE_PRIVATE_FLAG_IS(SUBSCRIBED_BUDDIES))
627 ucs_get_im_item_list(sipe_private);
629 return;
632 sipe_private->ucs = ucs = g_new0(struct sipe_ucs, 1);
633 ucs->migrated = migrated;
635 sipe_ews_autodiscover_start(sipe_private,
636 ucs_ews_autodiscover_cb,
637 NULL);
640 void sipe_ucs_free(struct sipe_core_private *sipe_private)
642 struct sipe_ucs *ucs = sipe_private->ucs;
644 if (!ucs)
645 return;
647 /* UCS stack is shutting down: reject all new requests */
648 ucs->shutting_down = TRUE;
650 if (ucs->deferred_requests) {
651 GSList *entry = ucs->deferred_requests;
652 while (entry) {
653 sipe_ucs_deferred_free(sipe_private, entry->data);
654 entry = entry->next;
656 g_slist_free(ucs->deferred_requests);
659 if (ucs->pending_requests) {
660 GSList *entry = ucs->pending_requests;
661 while (entry) {
662 sipe_ucs_request_free(sipe_private, entry->data);
663 entry = entry->next;
665 g_slist_free(ucs->pending_requests);
668 g_free(ucs->ews_url);
669 g_free(ucs);
670 sipe_private->ucs = NULL;
674 Local Variables:
675 mode: c
676 c-file-style: "bsd"
677 indent-tabs-mode: t
678 tab-width: 8
679 End: