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>
33 #include "sipe-backend.h"
34 #include "sipe-buddy.h"
35 #include "sipe-common.h"
36 #include "sipe-core.h"
37 #include "sipe-core-private.h"
38 #include "sipe-digest.h"
39 #include "sipe-ews-autodiscover.h"
40 #include "sipe-group.h"
41 #include "sipe-http.h"
42 #include "sipe-subscriptions.h"
44 #include "sipe-utils.h"
47 struct sipe_ucs_transaction
{
48 GSList
*pending_requests
;
51 typedef void (ucs_callback
)(struct sipe_core_private
*sipe_private
,
52 struct sipe_ucs_transaction
*trans
,
54 gpointer callback_data
);
60 struct sipe_ucs_transaction
*transaction
;
61 struct sipe_http_request
*request
;
65 struct ucs_request
*active_request
;
67 GSList
*default_transaction
;
72 gboolean shutting_down
;
75 static void sipe_ucs_request_free(struct sipe_core_private
*sipe_private
,
76 struct ucs_request
*data
)
78 struct sipe_ucs
*ucs
= sipe_private
->ucs
;
79 struct sipe_ucs_transaction
*trans
= data
->transaction
;
81 /* remove request from transaction */
82 trans
->pending_requests
= g_slist_remove(trans
->pending_requests
,
84 sipe_private
->ucs
->active_request
= NULL
;
86 /* remove completed transactions (except default transaction) */
87 if (!trans
->pending_requests
&&
88 (trans
!= ucs
->default_transaction
->data
)) {
89 ucs
->transactions
= g_slist_remove(ucs
->transactions
,
95 sipe_http_request_cancel(data
->request
);
97 /* Callback: aborted */
98 (*data
->cb
)(sipe_private
, NULL
, NULL
, data
->cb_data
);
103 static void sipe_ucs_next_request(struct sipe_core_private
*sipe_private
);
104 static void sipe_ucs_http_response(struct sipe_core_private
*sipe_private
,
106 SIPE_UNUSED_PARAMETER GSList
*headers
,
108 gpointer callback_data
)
110 struct ucs_request
*data
= callback_data
;
112 SIPE_DEBUG_INFO("sipe_ucs_http_response: code %d", status
);
113 data
->request
= NULL
;
115 if ((status
== SIPE_HTTP_STATUS_OK
) && body
) {
116 sipe_xml
*xml
= sipe_xml_parse(body
, strlen(body
));
117 const sipe_xml
*soap_body
= sipe_xml_child(xml
, "Body");
118 /* Callback: success */
119 (*data
->cb
)(sipe_private
,
125 /* Callback: failed */
126 (*data
->cb
)(sipe_private
, NULL
, NULL
, data
->cb_data
);
129 /* already been called */
132 sipe_ucs_request_free(sipe_private
, data
);
133 sipe_ucs_next_request(sipe_private
);
136 static void sipe_ucs_next_request(struct sipe_core_private
*sipe_private
)
138 struct sipe_ucs
*ucs
= sipe_private
->ucs
;
139 struct sipe_ucs_transaction
*trans
;
141 if (ucs
->active_request
|| ucs
->shutting_down
|| !ucs
->ews_url
)
144 trans
= ucs
->transactions
->data
;
145 while (trans
->pending_requests
) {
146 struct ucs_request
*data
= trans
->pending_requests
->data
;
147 gchar
*soap
= g_strdup_printf("<?xml version=\"1.0\"?>\r\n"
149 " xmlns:m=\"http://schemas.microsoft.com/exchange/services/2006/messages\""
150 " xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\""
151 " xmlns:t=\"http://schemas.microsoft.com/exchange/services/2006/types\""
154 " <t:RequestServerVersion Version=\"Exchange2013\" />"
161 struct sipe_http_request
*request
= sipe_http_request_post(sipe_private
,
165 "text/xml; charset=UTF-8",
166 sipe_ucs_http_response
,
173 data
->request
= request
;
175 ucs
->active_request
= data
;
177 sipe_core_email_authentication(sipe_private
,
179 sipe_http_request_allow_redirect(request
);
180 sipe_http_request_ready(request
);
184 SIPE_DEBUG_ERROR_NOFORMAT("sipe_ucs_next_request: failed to create HTTP connection");
185 sipe_ucs_request_free(sipe_private
, data
);
190 static gboolean
sipe_ucs_http_request(struct sipe_core_private
*sipe_private
,
191 struct sipe_ucs_transaction
*trans
,
192 gchar
*body
, /* takes ownership */
193 ucs_callback
*callback
,
194 gpointer callback_data
)
196 struct sipe_ucs
*ucs
= sipe_private
->ucs
;
198 if (!ucs
|| ucs
->shutting_down
) {
199 SIPE_DEBUG_ERROR("sipe_ucs_http_request: new UCS request during shutdown: THIS SHOULD NOT HAPPEN! Debugging information:\n"
201 body
? body
: "<EMPTY>");
206 struct ucs_request
*data
= g_new0(struct ucs_request
, 1);
209 data
->cb_data
= callback_data
;
213 trans
= ucs
->default_transaction
->data
;
214 data
->transaction
= trans
;
215 trans
->pending_requests
= g_slist_append(trans
->pending_requests
,
218 sipe_ucs_next_request(sipe_private
);
223 struct sipe_ucs_transaction
*sipe_ucs_transaction(struct sipe_core_private
*sipe_private
)
225 struct sipe_ucs
*ucs
= sipe_private
->ucs
;
226 struct sipe_ucs_transaction
*trans
;
231 /* always insert new transactions before default transaction */
232 trans
= g_new0(struct sipe_ucs_transaction
, 1);
233 ucs
->transactions
= g_slist_insert_before(ucs
->transactions
,
234 ucs
->default_transaction
,
240 static void sipe_ucs_get_user_photo_response(struct sipe_core_private
*sipe_private
,
241 SIPE_UNUSED_PARAMETER
struct sipe_ucs_transaction
*trans
,
242 const sipe_xml
*body
,
243 gpointer callback_data
)
245 gchar
*uri
= callback_data
;
246 const sipe_xml
*node
= sipe_xml_child(body
,
247 "GetUserPhotoResponse/PictureData");
253 guchar digest
[SIPE_DIGEST_SHA1_LENGTH
];
254 gchar
*digest_string
;
256 /* decode photo data */
257 base64
= sipe_xml_data(node
);
258 photo
= g_base64_decode(base64
, &photo_size
);
261 /* EWS doesn't provide a hash -> calculate SHA-1 digest */
262 sipe_digest_sha1(photo
, photo_size
, digest
);
263 digest_string
= buff_to_hex_str(digest
,
264 SIPE_DIGEST_SHA1_LENGTH
);
266 /* backend frees "photo" */
267 sipe_backend_buddy_set_photo(SIPE_CORE_PUBLIC
,
272 g_free(digest_string
);
278 void sipe_ucs_get_photo(struct sipe_core_private
*sipe_private
,
281 gchar
*payload
= g_strdup(uri
);
282 gchar
*body
= g_strdup_printf("<m:GetUserPhoto>"
283 " <m:Email>%s</m:Email>"
284 " <m:SizeRequested>HR48x48</m:SizeRequested>"
286 sipe_get_no_sip_uri(uri
));
288 if (!sipe_ucs_http_request(sipe_private
,
291 sipe_ucs_get_user_photo_response
,
296 static void sipe_ucs_ignore_response(struct sipe_core_private
*sipe_private
,
297 SIPE_UNUSED_PARAMETER
struct sipe_ucs_transaction
*trans
,
298 SIPE_UNUSED_PARAMETER
const sipe_xml
*body
,
299 SIPE_UNUSED_PARAMETER gpointer callback_data
)
301 SIPE_DEBUG_INFO_NOFORMAT("sipe_ucs_ignore_response: done");
302 sipe_private
->ucs
->last_response
= time(NULL
);
305 static void ucs_extract_keys(const sipe_xml
*persona_node
,
307 const gchar
**change
)
309 const sipe_xml
*attr_node
;
312 * extract Exchange key - play the guessing game :-(
314 * We can't use the "DisplayName" node, because the text is localized.
316 * Assume that IsQuickContact == "true" and IsHidden == "false" means
317 * this Attribution node contains the information for the Lync contact.
319 for (attr_node
= sipe_xml_child(persona_node
,
320 "Attributions/Attribution");
322 attr_node
= sipe_xml_twin(attr_node
)) {
323 const sipe_xml
*id_node
= sipe_xml_child(attr_node
,
325 gchar
*hidden
= sipe_xml_data(sipe_xml_child(attr_node
,
327 gchar
*quick
= sipe_xml_data(sipe_xml_child(attr_node
,
330 sipe_strcase_equal(hidden
, "false") &&
331 sipe_strcase_equal(quick
, "true")) {
332 *key
= sipe_xml_attribute(id_node
, "Id");
333 *change
= sipe_xml_attribute(id_node
, "ChangeKey");
343 static void sipe_ucs_add_new_im_contact_to_group_response(struct sipe_core_private
*sipe_private
,
344 SIPE_UNUSED_PARAMETER
struct sipe_ucs_transaction
*trans
,
345 const sipe_xml
*body
,
346 gpointer callback_data
)
348 gchar
*who
= callback_data
;
349 struct sipe_buddy
*buddy
= sipe_buddy_find_by_uri(sipe_private
, who
);
350 const sipe_xml
*persona_node
= sipe_xml_child(body
,
351 "AddNewImContactToGroupResponse/Persona");
353 sipe_private
->ucs
->last_response
= time(NULL
);
357 is_empty(buddy
->exchange_key
) &&
358 is_empty(buddy
->change_key
)) {
359 const gchar
*key
= NULL
;
360 const gchar
*change
= NULL
;
362 ucs_extract_keys(persona_node
, &key
, &change
);
364 if (!is_empty(key
) && !is_empty(change
)) {
366 sipe_buddy_add_keys(sipe_private
,
371 SIPE_DEBUG_INFO("sipe_ucs_add_new_im_contact_to_group_response: persona URI '%s' key '%s' change '%s'",
372 buddy
->name
, key
, change
);
379 void sipe_ucs_group_add_buddy(struct sipe_core_private
*sipe_private
,
380 struct sipe_ucs_transaction
*trans
,
381 struct sipe_group
*group
,
382 struct sipe_buddy
*buddy
,
385 /* existing or new buddy? */
386 if (buddy
&& buddy
->exchange_key
) {
387 gchar
*body
= g_strdup_printf("<m:AddImContactToGroup>"
388 " <m:ContactId Id=\"%s\" ChangeKey=\"%s\"/>"
389 " <m:GroupId Id=\"%s\" ChangeKey=\"%s\"/>"
390 "</m:AddImContactToGroup>",
396 sipe_ucs_http_request(sipe_private
,
399 sipe_ucs_ignore_response
,
402 gchar
*payload
= g_strdup(who
);
403 gchar
*body
= g_strdup_printf("<m:AddNewImContactToGroup>"
404 " <m:ImAddress>%s</m:ImAddress>"
405 " <m:GroupId Id=\"%s\" ChangeKey=\"%s\"/>"
406 "</m:AddNewImContactToGroup>",
407 sipe_get_no_sip_uri(who
),
411 if (!sipe_ucs_http_request(sipe_private
,
414 sipe_ucs_add_new_im_contact_to_group_response
,
420 void sipe_ucs_group_remove_buddy(struct sipe_core_private
*sipe_private
,
421 struct sipe_ucs_transaction
*trans
,
422 struct sipe_group
*group
,
423 struct sipe_buddy
*buddy
)
427 * If a contact is removed from last group, it will also be
428 * removed from contact list completely. The documentation has
429 * a RemoveContactFromImList operation, but that doesn't seem
430 * to work at all, i.e. it is always rejected by the server.
432 gchar
*body
= g_strdup_printf("<m:RemoveImContactFromGroup>"
433 " <m:ContactId Id=\"%s\" ChangeKey=\"%s\"/>"
434 " <m:GroupId Id=\"%s\" ChangeKey=\"%s\"/>"
435 "</m:RemoveImContactFromGroup>",
441 sipe_ucs_http_request(sipe_private
,
444 sipe_ucs_ignore_response
,
449 static struct sipe_group
*ucs_create_group(struct sipe_core_private
*sipe_private
,
450 const sipe_xml
*group_node
)
452 const sipe_xml
*id_node
= sipe_xml_child(group_node
,
454 const gchar
*key
= sipe_xml_attribute(id_node
, "Id");
455 const gchar
*change
= sipe_xml_attribute(id_node
, "ChangeKey");
456 struct sipe_group
*group
= NULL
;
458 if (!(is_empty(key
) || is_empty(change
))) {
459 gchar
*name
= sipe_xml_data(sipe_xml_child(group_node
,
461 group
= sipe_group_add(sipe_private
,
465 /* sipe_group must have unique ID */
466 ++sipe_private
->ucs
->group_id
);
473 static void sipe_ucs_add_im_group_response(struct sipe_core_private
*sipe_private
,
474 struct sipe_ucs_transaction
*trans
,
475 const sipe_xml
*body
,
476 gpointer callback_data
)
478 gchar
*who
= callback_data
;
479 const sipe_xml
*group_node
= sipe_xml_child(body
,
480 "AddImGroupResponse/ImGroup");
481 struct sipe_group
*group
= ucs_create_group(sipe_private
, group_node
);
483 sipe_private
->ucs
->last_response
= time(NULL
);
486 struct sipe_buddy
*buddy
= sipe_buddy_find_by_uri(sipe_private
,
490 sipe_buddy_insert_group(buddy
, group
);
492 sipe_ucs_group_add_buddy(sipe_private
,
502 void sipe_ucs_group_create(struct sipe_core_private
*sipe_private
,
503 struct sipe_ucs_transaction
*trans
,
507 gchar
*payload
= g_strdup(who
);
508 /* new_name can contain restricted characters */
509 gchar
*body
= g_markup_printf_escaped("<m:AddImGroup>"
510 " <m:DisplayName>%s</m:DisplayName>"
514 if (!sipe_ucs_http_request(sipe_private
,
517 sipe_ucs_add_im_group_response
,
522 void sipe_ucs_group_rename(struct sipe_core_private
*sipe_private
,
523 struct sipe_group
*group
,
524 const gchar
*new_name
)
526 /* new_name can contain restricted characters */
527 gchar
*body
= g_markup_printf_escaped("<m:SetImGroup>"
528 " <m:GroupId Id=\"%s\" ChangeKey=\"%s\"/>"
529 " <m:NewDisplayName>%s</m:NewDisplayName>"
535 sipe_ucs_http_request(sipe_private
,
538 sipe_ucs_ignore_response
,
542 void sipe_ucs_group_remove(struct sipe_core_private
*sipe_private
,
543 struct sipe_group
*group
)
545 gchar
*body
= g_strdup_printf("<m:RemoveImGroup>"
546 " <m:GroupId Id=\"%s\" ChangeKey=\"%s\"/>"
547 "</m:RemoveImGroup>",
551 sipe_ucs_http_request(sipe_private
,
554 sipe_ucs_ignore_response
,
558 static void sipe_ucs_get_im_item_list_response(struct sipe_core_private
*sipe_private
,
559 SIPE_UNUSED_PARAMETER
struct sipe_ucs_transaction
*trans
,
560 const sipe_xml
*body
,
561 SIPE_UNUSED_PARAMETER gpointer callback_data
)
563 const sipe_xml
*node
= sipe_xml_child(body
,
564 "GetImItemListResponse/ImItemList");
567 const sipe_xml
*persona_node
;
568 const sipe_xml
*group_node
;
569 GHashTable
*uri_to_alias
= g_hash_table_new_full(g_str_hash
,
574 /* Start processing contact list */
575 if (SIPE_CORE_PRIVATE_FLAG_IS(SUBSCRIBED_BUDDIES
)) {
576 sipe_group_update_start(sipe_private
);
577 sipe_buddy_update_start(sipe_private
);
579 sipe_backend_buddy_list_processing_start(SIPE_CORE_PUBLIC
);
581 for (persona_node
= sipe_xml_child(node
, "Personas/Persona");
583 persona_node
= sipe_xml_twin(persona_node
)) {
584 gchar
*address
= sipe_xml_data(sipe_xml_child(persona_node
,
586 const gchar
*key
= NULL
;
587 const gchar
*change
= NULL
;
589 ucs_extract_keys(persona_node
, &key
, &change
);
591 if (!(is_empty(address
) || is_empty(key
) || is_empty(change
))) {
592 gchar
*alias
= sipe_xml_data(sipe_xml_child(persona_node
,
595 * it seems to be undefined if ImAddress node
596 * contains "sip:" prefix or not...
598 gchar
*uri
= sip_uri(address
);
599 struct sipe_buddy
*buddy
= sipe_buddy_add(sipe_private
,
605 /* hash table takes ownership of alias */
606 g_hash_table_insert(uri_to_alias
,
610 SIPE_DEBUG_INFO("sipe_ucs_get_im_item_list_response: persona URI '%s' key '%s' change '%s'",
611 buddy
->name
, key
, change
);
616 for (group_node
= sipe_xml_child(node
, "Groups/ImGroup");
618 group_node
= sipe_xml_twin(group_node
)) {
619 struct sipe_group
*group
= ucs_create_group(sipe_private
,
623 const sipe_xml
*member_node
;
625 for (member_node
= sipe_xml_child(group_node
,
626 "MemberCorrelationKey/ItemId");
628 member_node
= sipe_xml_twin(member_node
)) {
629 struct sipe_buddy
*buddy
= sipe_buddy_find_by_exchange_key(sipe_private
,
630 sipe_xml_attribute(member_node
,
633 sipe_buddy_add_to_group(sipe_private
,
636 g_hash_table_lookup(uri_to_alias
,
642 g_hash_table_destroy(uri_to_alias
);
644 /* Finished processing contact list */
645 if (SIPE_CORE_PRIVATE_FLAG_IS(SUBSCRIBED_BUDDIES
)) {
646 sipe_buddy_update_finish(sipe_private
);
647 sipe_group_update_finish(sipe_private
);
649 sipe_buddy_cleanup_local_list(sipe_private
);
650 sipe_backend_buddy_list_processing_finish(SIPE_CORE_PUBLIC
);
651 sipe_subscribe_presence_initial(sipe_private
);
656 static void ucs_get_im_item_list(struct sipe_core_private
*sipe_private
)
658 if (sipe_private
->ucs
->migrated
)
659 sipe_ucs_http_request(sipe_private
,
660 /* prioritize over pending default requests */
661 sipe_ucs_transaction(sipe_private
),
662 g_strdup("<m:GetImItemList/>"),
663 sipe_ucs_get_im_item_list_response
,
667 static void ucs_ews_autodiscover_cb(struct sipe_core_private
*sipe_private
,
668 const struct sipe_ews_autodiscover_data
*ews_data
,
669 SIPE_UNUSED_PARAMETER gpointer callback_data
)
671 struct sipe_ucs
*ucs
= sipe_private
->ucs
;
672 const gchar
*ews_url
;
674 if (!ucs
|| !ews_data
)
677 ews_url
= ews_data
->ews_url
;
678 if (is_empty(ews_url
)) {
679 SIPE_DEBUG_ERROR_NOFORMAT("ucs_ews_autodiscover_cb: can't detect EWS URL, contact list operations will not work!");
683 SIPE_DEBUG_INFO("ucs_ews_autodiscover_cb: EWS URL '%s'", ews_url
);
684 ucs
->ews_url
= g_strdup(ews_url
);
686 /* this will trigger sending of the first deferred request */
687 ucs_get_im_item_list(sipe_private
);
690 gboolean
sipe_ucs_is_migrated(struct sipe_core_private
*sipe_private
)
692 return(sipe_private
->ucs
? sipe_private
->ucs
->migrated
: FALSE
);
695 void sipe_ucs_init(struct sipe_core_private
*sipe_private
,
698 struct sipe_ucs
*ucs
;
700 if (sipe_private
->ucs
) {
701 struct sipe_ucs
*ucs
= sipe_private
->ucs
;
704 * contact list update trigger -> request list again
706 * If the trigger arrives less than 10 seconds after our
707 * last UCS response, then ignore it, because it is caused
708 * by our own changes to the contact list.
710 if (SIPE_CORE_PRIVATE_FLAG_IS(SUBSCRIBED_BUDDIES
)) {
711 if ((time(NULL
) - ucs
->last_response
) >= 10)
712 ucs_get_im_item_list(sipe_private
);
714 SIPE_DEBUG_INFO_NOFORMAT("sipe_ucs_init: ignoring this contact list update - triggered by our last change");
717 ucs
->last_response
= 0;
721 sipe_private
->ucs
= ucs
= g_new0(struct sipe_ucs
, 1);
722 ucs
->migrated
= migrated
;
724 /* create default transaction */
725 sipe_ucs_transaction(sipe_private
);
726 ucs
->default_transaction
= ucs
->transactions
;
728 sipe_ews_autodiscover_start(sipe_private
,
729 ucs_ews_autodiscover_cb
,
733 void sipe_ucs_free(struct sipe_core_private
*sipe_private
)
735 struct sipe_ucs
*ucs
= sipe_private
->ucs
;
741 /* UCS stack is shutting down: reject all new requests */
742 ucs
->shutting_down
= TRUE
;
744 entry
= ucs
->transactions
;
746 struct sipe_ucs_transaction
*trans
= entry
->data
;
747 GSList
*entry2
= trans
->pending_requests
;
749 /* transactions get deleted by sipe_ucs_request_free() */
753 struct ucs_request
*request
= entry2
->data
;
755 /* transactions get deleted by sipe_ucs_request_free() */
756 entry2
= entry2
->next
;
758 sipe_ucs_request_free(sipe_private
, request
);
762 /* only default transaction is left... */
763 sipe_utils_slist_free_full(ucs
->transactions
, g_free
);
765 g_free(ucs
->ews_url
);
767 sipe_private
->ucs
= NULL
;