1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
3 /* Copyright (C) 2001-2004 Novell, Inc.
5 * This program is free software; you can redistribute it and/or
6 * modify it under the terms of version 2 of the GNU Lesser General Public
7 * License as published by the Free Software Foundation.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * General Public License for more details.
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with this program; if not, write to the
16 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
30 #include <sys/types.h>
34 #include <sys/socket.h>
35 #include <netinet/in.h>
36 #include <arpa/inet.h>
43 #include "e2k-context.h"
44 #include "e2k-encoding-utils.h"
45 #include "e2k-marshal.h"
46 #include "e2k-propnames.h"
47 #include "e2k-restriction.h"
49 #include "e2k-utils.h"
50 #include "e2k-xml-utils.h"
52 #include <libsoup/soup-address.h>
53 #include <libsoup/soup-message-filter.h>
54 #include <libsoup/soup-session-async.h>
55 #include <libsoup/soup-session-sync.h>
56 #include <libsoup/soup-socket.h>
57 #include <libsoup/soup-uri.h>
58 #include <libxml/parser.h>
59 #include <libxml/tree.h>
60 #include <libxml/xmlmemory.h>
63 /* The strtok() in Microsoft's C library is MT-safe (not stateless,
64 * but that is not needed here).
66 #define strtok_r(s,sep,lasts ) (*(lasts) = strtok((s),(sep)))
70 #define CLOSE_SOCKET(socket) closesocket (socket)
71 #define STATUS_IS_SOCKET_ERROR(status) ((status) == SOCKET_ERROR)
72 #define SOCKET_IS_INVALID(socket) ((socket) == INVALID_SOCKET)
73 #define BIND_STATUS_IS_ADDRINUSE() (WSAGetLastError () == WSAEADDRINUSE)
75 #define CLOSE_SOCKET(socket) close (socket)
76 #define STATUS_IS_SOCKET_ERROR(status) ((status) == -1)
77 #define SOCKET_IS_INVALID(socket) ((socket) < 0)
78 #define BIND_STATUS_IS_ADDRINUSE() (errno == EADDRINUSE)
81 #define PARENT_TYPE G_TYPE_OBJECT
82 static GObjectClass *parent_class;
89 static guint signals [LAST_SIGNAL] = { 0 };
91 struct _E2kContextPrivate {
92 SoupSession *session, *async_session;
93 char *owa_uri, *username, *password;
94 time_t last_timestamp;
96 /* Notification listener */
97 SoupSocket *get_local_address_sock;
98 GIOChannel *listener_channel;
99 int listener_watch_id;
101 char *notification_uri;
102 GHashTable *subscriptions_by_id, *subscriptions_by_uri;
104 /* Forms-based authentication */
106 gboolean cookie_verified;
109 /* For operations with progress */
110 #define E2K_CONTEXT_MIN_BATCH_SIZE 25
111 #define E2K_CONTEXT_MAX_BATCH_SIZE 100
113 /* For soup sync session timeout */
114 #define E2K_SOUP_SESSION_TIMEOUT 30
121 static gboolean renew_subscription (gpointer user_data);
122 static void unsubscribe_internal (E2kContext *ctx, const char *uri, GList *sub_list);
123 static gboolean do_notification (GIOChannel *source, GIOCondition condition, gpointer data);
125 static void setup_message (SoupMessageFilter *filter, SoupMessage *msg);
128 init (GObject *object)
130 E2kContext *ctx = E2K_CONTEXT (object);
132 ctx->priv = g_new0 (E2kContextPrivate, 1);
133 ctx->priv->subscriptions_by_id =
134 g_hash_table_new (g_str_hash, g_str_equal);
135 ctx->priv->subscriptions_by_uri =
136 g_hash_table_new (g_str_hash, g_str_equal);
140 destroy_sub_list (gpointer uri, gpointer sub_list, gpointer ctx)
142 unsubscribe_internal (ctx, uri, sub_list);
143 g_list_free (sub_list);
147 dispose (GObject *object)
149 E2kContext *ctx = E2K_CONTEXT (object);
152 if (ctx->priv->owa_uri)
153 g_free (ctx->priv->owa_uri);
154 if (ctx->priv->username)
155 g_free (ctx->priv->username);
156 if (ctx->priv->password)
157 g_free (ctx->priv->password);
159 if (ctx->priv->get_local_address_sock)
160 g_object_unref (ctx->priv->get_local_address_sock);
162 g_hash_table_foreach (ctx->priv->subscriptions_by_uri,
163 destroy_sub_list, ctx);
164 g_hash_table_destroy (ctx->priv->subscriptions_by_uri);
166 g_hash_table_destroy (ctx->priv->subscriptions_by_id);
168 if (ctx->priv->listener_watch_id)
169 g_source_remove (ctx->priv->listener_watch_id);
170 if (ctx->priv->listener_channel) {
171 g_io_channel_shutdown (ctx->priv->listener_channel,
173 g_io_channel_unref (ctx->priv->listener_channel);
176 if (ctx->priv->session)
177 g_object_unref (ctx->priv->session);
178 if (ctx->priv->async_session)
179 g_object_unref (ctx->priv->async_session);
181 g_free (ctx->priv->cookie);
187 G_OBJECT_CLASS (parent_class)->dispose (object);
191 class_init (GObjectClass *object_class)
193 parent_class = g_type_class_ref (PARENT_TYPE);
195 /* virtual method override */
196 object_class->dispose = dispose;
199 g_signal_new ("redirect",
200 G_OBJECT_CLASS_TYPE (object_class),
202 G_STRUCT_OFFSET (E2kContextClass, redirect),
204 e2k_marshal_NONE__INT_STRING_STRING,
212 filter_iface_init (SoupMessageFilterClass *filter_class)
214 /* interface implementation */
215 filter_class->setup_message = setup_message;
218 e2k_debug = getenv ("E2K_DEBUG");
220 e2k_debug_level = atoi (e2k_debug);
224 E2K_MAKE_TYPE_WITH_IFACE (e2k_context, E2kContext, class_init, init, PARENT_TYPE, filter_iface_init, SOUP_TYPE_MESSAGE_FILTER)
228 renew_sub_list (gpointer key, gpointer value, gpointer data)
232 for (sub_list = value; sub_list; sub_list = sub_list->next)
233 renew_subscription (sub_list->data);
237 got_connection (SoupSocket *sock, guint status, gpointer user_data)
239 E2kContext *ctx = user_data;
241 struct sockaddr_in sin;
242 const char *local_ipaddr;
246 ctx->priv->get_local_address_sock = NULL;
248 if (status != SOUP_STATUS_OK)
251 addr = soup_socket_get_local_address (sock);
252 local_ipaddr = soup_address_get_physical (addr);
254 s = socket (AF_INET, SOCK_DGRAM, IPPROTO_UDP);
255 if (SOCKET_IS_INVALID (s))
258 memset (&sin, 0, sizeof (sin));
259 sin.sin_family = AF_INET;
261 port = (short)getpid ();
266 sin.sin_port = htons (port);
267 ret = bind (s, (struct sockaddr *)&sin, sizeof (sin));
268 } while (STATUS_IS_SOCKET_ERROR (ret) && BIND_STATUS_IS_ADDRINUSE ());
276 ctx->priv->listener_channel = g_io_channel_unix_new (s);
278 ctx->priv->listener_channel = g_io_channel_win32_new_socket (s);
280 g_io_channel_set_encoding (ctx->priv->listener_channel, NULL, NULL);
281 g_io_channel_set_buffered (ctx->priv->listener_channel, FALSE);
283 ctx->priv->listener_watch_id =
284 g_io_add_watch (ctx->priv->listener_channel,
285 G_IO_IN, do_notification, ctx);
287 ctx->priv->notification_uri = g_strdup_printf ("httpu://%s:%u/",
291 g_hash_table_foreach (ctx->priv->subscriptions_by_uri,
292 renew_sub_list, ctx);
296 g_object_unref (sock);
297 g_object_unref (ctx);
302 * @uri: OWA uri to connect to
304 * Creates a new #E2kContext based at @uri
306 * Return value: the new context
309 e2k_context_new (const char *uri)
314 suri = soup_uri_new (uri);
319 soup_uri_free (suri);
323 ctx = g_object_new (E2K_TYPE_CONTEXT, NULL);
324 ctx->priv->owa_uri = g_strdup (uri);
327 ctx->priv->get_local_address_sock =
328 soup_socket_client_new_async (
329 suri->host, suri->port, NULL,
330 got_connection, ctx);
331 soup_uri_free (suri);
337 session_authenticate (SoupSession *session, SoupMessage *msg,
338 const char *auth_type, const char *auth_realm,
339 char **username, char **password, gpointer user_data)
341 E2kContext *ctx = user_data;
343 *username = g_strdup (ctx->priv->username);
344 *password = g_strdup (ctx->priv->password);
348 * e2k_context_set_auth:
350 * @username: the Windows username (not including domain) of the user
351 * @domain: the NT domain, or %NULL to use the default (if using NTLM)
352 * @authmech: the HTTP Authorization type to use; either "Basic" or "NTLM"
353 * @password: the user's password
355 * Sets the authentication information on @ctx. This will have the
356 * side effect of cancelling any pending requests on @ctx.
359 e2k_context_set_auth (E2kContext *ctx, const char *username,
360 const char *domain, const char *authmech,
361 const char *password)
363 guint timeout = E2K_SOUP_SESSION_TIMEOUT;
365 g_return_if_fail (E2K_IS_CONTEXT (ctx));
368 g_free (ctx->priv->username);
370 ctx->priv->username =
371 g_strdup_printf ("%s\\%s", domain,
374 ctx->priv->username = g_strdup (username);
378 g_free (ctx->priv->password);
379 ctx->priv->password = g_strdup (password);
382 /* Destroy the old sessions so we don't reuse old auths */
383 if (ctx->priv->session)
384 g_object_unref (ctx->priv->session);
385 if (ctx->priv->async_session)
386 g_object_unref (ctx->priv->async_session);
388 /* Set a default timeout value of 30 seconds.
389 FIXME: Make timeout configurable
391 if (g_getenv ("SOUP_SESSION_TIMEOUT"))
392 timeout = atoi (g_getenv ("SOUP_SESSION_TIMEOUT"));
394 ctx->priv->session = soup_session_sync_new_with_options (
395 SOUP_SESSION_USE_NTLM, !authmech || !strcmp (authmech, "NTLM"),
396 SOUP_SESSION_TIMEOUT, timeout,
398 g_signal_connect (ctx->priv->session, "authenticate",
399 G_CALLBACK (session_authenticate), ctx);
400 soup_session_add_filter (ctx->priv->session,
401 SOUP_MESSAGE_FILTER (ctx));
403 ctx->priv->async_session = soup_session_async_new_with_options (
404 SOUP_SESSION_USE_NTLM, !authmech || !strcmp (authmech, "NTLM"),
406 g_signal_connect (ctx->priv->async_session, "authenticate",
407 G_CALLBACK (session_authenticate), ctx);
408 soup_session_add_filter (ctx->priv->async_session,
409 SOUP_MESSAGE_FILTER (ctx));
413 * e2k_context_get_last_timestamp:
416 * Returns a %time_t corresponding to the last "Date" header
417 * received from the server.
419 * Return value: the timestamp
422 e2k_context_get_last_timestamp (E2kContext *ctx)
424 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), -1);
426 return ctx->priv->last_timestamp;
432 * 1 - Basic request and response
433 * 2 - 1 plus all headers
434 * 3 - 2 plus all bodies
435 * 4 - 3 plus Global Catalog debug too
439 print_header (gpointer name, gpointer value, gpointer data)
441 printf ("%s: %s\n", (char *)name, (char *)value);
445 e2k_debug_print_request (SoupMessage *msg, const char *note)
449 uri = soup_message_get_uri (msg);
450 printf ("%s %s%s%s HTTP/1.1\nE2k-Debug: %p @ %lu",
451 msg->method, uri->path,
452 uri->query ? "?" : "",
453 uri->query ? uri->query : "",
454 msg, (unsigned long)time (NULL));
456 printf (" [%s]\n", note);
459 if (e2k_debug_level > 1) {
460 print_header ("Host", uri->host, NULL);
461 soup_message_foreach_header (msg->request_headers,
464 if (e2k_debug_level > 2 && msg->request.length &&
465 strcmp (msg->method, "POST")) {
467 fwrite (msg->request.body, 1, msg->request.length, stdout);
468 if (msg->request.body[msg->request.length - 1] != '\n')
475 e2k_debug_print_response (SoupMessage *msg)
477 printf ("%d %s\nE2k-Debug: %p @ %lu\n",
478 msg->status_code, msg->reason_phrase,
480 if (e2k_debug_level > 1) {
481 soup_message_foreach_header (msg->response_headers,
484 if (e2k_debug_level > 2 && msg->response.length &&
485 E2K_HTTP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
486 const char *content_type =
487 soup_message_get_header (msg->response_headers,
489 if (!content_type || e2k_debug_level > 4 ||
490 g_ascii_strcasecmp (content_type, "text/html")) {
492 fwrite (msg->response.body, 1, msg->response.length, stdout);
493 if (msg->response.body[msg->response.length - 1] != '\n')
501 e2k_debug_handler (SoupMessage *msg, gpointer user_data)
503 gboolean restarted = GPOINTER_TO_INT (user_data);
505 e2k_debug_print_response (msg);
507 e2k_debug_print_request (msg, "restarted");
511 e2k_debug_setup (SoupMessage *msg)
513 if (!e2k_debug_level)
516 e2k_debug_print_request (msg, NULL);
518 g_signal_connect (msg, "finished",
519 G_CALLBACK (e2k_debug_handler),
520 GINT_TO_POINTER (FALSE));
521 g_signal_connect (msg, "restarted",
522 G_CALLBACK (e2k_debug_handler),
523 GINT_TO_POINTER (TRUE));
527 #define E2K_FBA_FLAG_FORCE_DOWNLEVEL 1
528 #define E2K_FBA_FLAG_TRUSTED 4
533 * @failed_msg: a message that received a 440 status code
535 * Attempts to synchronously perform Exchange 2003 forms-based
538 * Return value: %FALSE if authentication failed, %TRUE if it
539 * succeeded, in which case @failed_msg can be requeued.
542 e2k_context_fba (E2kContext *ctx, SoupMessage *failed_msg)
544 static gboolean in_fba_auth = FALSE;
547 char *action, *method, *name, *value;
550 SoupMessage *post_msg;
551 GString *form_body, *cookie_str;
552 const GSList *cookies, *c;
554 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), FALSE);
559 if (ctx->priv->cookie) {
560 g_free (ctx->priv->cookie);
561 ctx->priv->cookie = NULL;
562 if (!ctx->priv->cookie_verified) {
563 /* New cookie failed on the first try. Must
568 /* Otherwise, it's just expired. */
571 if (!ctx->priv->username || !ctx->priv->password)
576 status = e2k_context_get_owa (ctx, NULL, ctx->priv->owa_uri,
578 if (!SOUP_STATUS_IS_SUCCESSFUL (status) || len == 0)
581 doc = e2k_parse_html (body, len);
584 node = e2k_xml_find (doc->children, "form");
588 method = xmlGetProp (node, "method");
589 if (!method || g_ascii_strcasecmp (method, "post") != 0) {
596 value = xmlGetProp (node, "action");
597 if (!value || !*value)
602 suri = soup_uri_new (ctx->priv->owa_uri);
604 suri->path = g_strdup (value);
605 action = soup_uri_to_string (suri, FALSE);
606 soup_uri_decode (action);
607 soup_uri_free (suri);
609 action = g_strdup (value);
612 form_body = g_string_new (NULL);
613 while ((node = e2k_xml_find (node, "input"))) {
614 name = xmlGetProp (node, "name");
617 value = xmlGetProp (node, "value");
619 if (!g_ascii_strcasecmp (name, "destination") && value) {
620 g_string_append (form_body, name);
621 g_string_append_c (form_body, '=');
622 e2k_uri_append_encoded (form_body, value, FALSE, NULL);
623 g_string_append_c (form_body, '&');
624 } else if (!g_ascii_strcasecmp (name, "flags")) {
625 g_string_append_printf (form_body, "flags=%d",
626 E2K_FBA_FLAG_TRUSTED);
627 g_string_append_c (form_body, '&');
628 } else if (!g_ascii_strcasecmp (name, "username")) {
629 g_string_append (form_body, "username=");
630 e2k_uri_append_encoded (form_body, ctx->priv->username, FALSE, NULL);
631 g_string_append_c (form_body, '&');
632 } else if (!g_ascii_strcasecmp (name, "password")) {
633 g_string_append (form_body, "password=");
634 e2k_uri_append_encoded (form_body, ctx->priv->password, FALSE, NULL);
635 g_string_append_c (form_body, '&');
642 g_string_append_printf (form_body, "trusted=%d", E2K_FBA_FLAG_TRUSTED);
646 post_msg = e2k_soup_message_new_full (ctx, action, "POST",
647 "application/x-www-form-urlencoded",
648 SOUP_BUFFER_SYSTEM_OWNED,
649 form_body->str, form_body->len);
650 soup_message_set_flags (post_msg, SOUP_MESSAGE_NO_REDIRECT);
651 e2k_context_send_message (ctx, NULL /* FIXME? */, post_msg);
652 g_string_free (form_body, FALSE);
655 if (!SOUP_STATUS_IS_SUCCESSFUL (post_msg->status_code) &&
656 !SOUP_STATUS_IS_REDIRECTION (post_msg->status_code)) {
657 g_object_unref (post_msg);
661 /* Extract the cookies */
662 cookies = soup_message_get_header_list (post_msg->response_headers,
664 cookie_str = g_string_new (NULL);
666 for (c = cookies; c; c = c->next) {
668 len = strcspn (value, ";");
671 g_string_append (cookie_str, "; ");
672 g_string_append_len (cookie_str, value, len);
674 ctx->priv->cookie = cookie_str->str;
675 ctx->priv->cookie_verified = FALSE;
676 g_string_free (cookie_str, FALSE);
677 g_object_unref (post_msg);
681 /* Set up the failed message to be requeued */
682 soup_message_remove_header (failed_msg->request_headers, "Cookie");
683 soup_message_add_header (failed_msg->request_headers,
684 "Cookie", ctx->priv->cookie);
695 fba_timeout_handler (SoupMessage *msg, gpointer user_data)
697 E2kContext *ctx = user_data;
701 e2k_debug_print_response (msg);
704 if (e2k_context_fba (ctx, msg))
705 soup_session_requeue_message (ctx->priv->session, msg);
707 soup_message_set_status (msg, SOUP_STATUS_UNAUTHORIZED);
711 timestamp_handler (SoupMessage *msg, gpointer user_data)
713 E2kContext *ctx = user_data;
716 date = soup_message_get_header (msg->response_headers, "Date");
718 ctx->priv->last_timestamp = e2k_http_parse_date (date);
722 redirect_handler (SoupMessage *msg, gpointer user_data)
724 E2kContext *ctx = user_data;
729 if (soup_message_get_flags (msg) & SOUP_MESSAGE_NO_REDIRECT)
732 new_uri = soup_message_get_header (msg->response_headers, "Location");
734 soup_uri = soup_uri_copy (soup_message_get_uri (msg));
735 old_uri = soup_uri_to_string (soup_uri, FALSE);
737 g_signal_emit (ctx, signals[REDIRECT], 0,
738 msg->status_code, old_uri, new_uri);
739 soup_uri_free (soup_uri);
745 setup_message (SoupMessageFilter *filter, SoupMessage *msg)
747 E2kContext *ctx = E2K_CONTEXT (filter);
750 if (ctx->priv->cookie) {
751 soup_message_remove_header (msg->request_headers, "Cookie");
752 soup_message_add_header (msg->request_headers,
753 "Cookie", ctx->priv->cookie);
756 /* Only do this the first time through */
757 if (!soup_message_get_header (msg->request_headers, "User-Agent")) {
758 soup_message_add_handler (msg, SOUP_HANDLER_PRE_BODY,
759 timestamp_handler, ctx);
760 soup_message_add_status_class_handler (msg, SOUP_STATUS_CLASS_REDIRECT,
761 SOUP_HANDLER_PRE_BODY,
762 redirect_handler, ctx);
763 soup_message_add_status_code_handler (msg, E2K_HTTP_TIMEOUT,
764 SOUP_HANDLER_PRE_BODY,
765 fba_timeout_handler, ctx);
766 soup_message_add_header (msg->request_headers, "User-Agent",
767 "Evolution/" VERSION);
770 e2k_debug_setup (msg);
776 * e2k_soup_message_new:
779 * @method: the HTTP method
781 * Creates a new %SoupMessage for @ctx.
783 * Return value: a new %SoupMessage, set up for connector use
786 e2k_soup_message_new (E2kContext *ctx, const char *uri, const char *method)
790 if (method[0] == 'B') {
791 char *slash_uri = e2k_strdup_with_trailing_slash (uri);
792 msg = soup_message_new (method, slash_uri);
795 msg = soup_message_new (method, uri);
801 * e2k_soup_message_new_full:
804 * @method: the HTTP method
805 * @content_type: MIME Content-Type of @body
806 * @owner: ownership of @body
807 * @body: request body
808 * @length: length of @body
810 * Creates a new %SoupMessage with the given body.
812 * Return value: a new %SoupMessage with a request body, set up for
816 e2k_soup_message_new_full (E2kContext *ctx, const char *uri,
817 const char *method, const char *content_type,
818 SoupOwnership owner, const char *body,
823 msg = e2k_soup_message_new (ctx, uri, method);
824 soup_message_set_request (msg, content_type,
825 owner, (char *)body, length);
831 * e2k_context_queue_message:
833 * @msg: the message to queue
834 * @callback: callback to invoke when @msg is done
835 * @user_data: data for @callback
837 * Asynchronously queues @msg in @ctx's session.
840 e2k_context_queue_message (E2kContext *ctx, SoupMessage *msg,
841 SoupMessageCallbackFn callback,
844 g_return_if_fail (E2K_IS_CONTEXT (ctx));
846 soup_session_queue_message (ctx->priv->async_session, msg,
847 callback, user_data);
851 context_canceller (E2kOperation *op, gpointer owner, gpointer data)
853 E2kContext *ctx = owner;
854 SoupMessage *msg = data;
856 soup_message_set_status (msg, SOUP_STATUS_CANCELLED);
857 soup_session_cancel_message (ctx->priv->session, msg);
861 * e2k_context_send_message:
863 * @op: an #E2kOperation to use for cancellation
864 * @msg: the message to send
866 * Synchronously sends @msg in @ctx's session.
868 * Return value: the HTTP status of the message
871 e2k_context_send_message (E2kContext *ctx, E2kOperation *op, SoupMessage *msg)
873 E2kHTTPStatus status;
875 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
877 if (e2k_operation_is_cancelled (op)) {
878 soup_message_set_status (msg, E2K_HTTP_CANCELLED);
879 return E2K_HTTP_CANCELLED;
882 e2k_operation_start (op, context_canceller, ctx, msg);
883 status = soup_session_send_message (ctx->priv->session, msg);
884 e2k_operation_finish (op);
891 update_unique_uri (E2kContext *ctx, SoupMessage *msg,
892 const char *folder_uri, const char *encoded_name, int *count,
893 E2kContextTestCallback test_callback, gpointer user_data)
901 uri = g_strdup_printf ("%s%s.EML", folder_uri,
904 uri = g_strdup_printf ("%s%s-%d.EML", folder_uri,
905 encoded_name, *count);
908 } while (test_callback && !test_callback (ctx, uri, user_data));
910 suri = soup_uri_new (uri);
911 soup_message_set_uri (msg, suri);
912 soup_uri_free (suri);
920 get_msg (E2kContext *ctx, const char *uri, gboolean owa, gboolean claim_ie)
924 msg = e2k_soup_message_new (ctx, uri, "GET");
926 soup_message_add_header (msg->request_headers, "Translate", "F");
928 soup_message_remove_header (msg->request_headers, "User-Agent");
929 soup_message_add_header (msg->request_headers, "User-Agent",
930 "MSIE 6.0b (Windows NT 5.0; compatible; "
931 "Evolution/" VERSION ")");
940 * @op: pointer to an #E2kOperation to use for cancellation
941 * @uri: URI of the object to GET
942 * @content_type: if not %NULL, will contain the Content-Type of the
943 * response on return.
944 * @body: if not %NULL, will contain the response body on return
945 * @len: if not %NULL, will contain the response body length on return
947 * Performs a GET on @ctx for @uri. If successful (2xx status code),
948 * the Content-Type, body and length will be returned. The body is not
949 * terminated by a '\0'. If the GET is not successful, @content_type,
950 * @body and @len will be untouched (even if the error response
953 * Return value: the HTTP status
956 e2k_context_get (E2kContext *ctx, E2kOperation *op, const char *uri,
957 char **content_type, char **body, int *len)
960 E2kHTTPStatus status;
962 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
963 g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
965 msg = get_msg (ctx, uri, FALSE, FALSE);
966 status = e2k_context_send_message (ctx, op, msg);
968 if (E2K_HTTP_STATUS_IS_SUCCESSFUL (status)) {
971 header = soup_message_get_header (msg->response_headers,
973 *content_type = g_strdup (header);
976 *body = msg->response.body;
977 msg->response.owner = SOUP_BUFFER_USER_OWNED;
980 *len = msg->response.length;
983 g_object_unref (msg);
988 * e2k_context_get_owa:
990 * @op: pointer to an #E2kOperation to use for cancellation
991 * @uri: URI of the object to GET
992 * @claim_ie: whether or not to claim to be IE
993 * @body: if not %NULL, will contain the response body on return
994 * @len: if not %NULL, will contain the response body length on return
996 * As with e2k_context_get(), but used when you need the HTML or XML
997 * data that would be returned to OWA rather than the raw object data.
999 * Return value: the HTTP status
1002 e2k_context_get_owa (E2kContext *ctx, E2kOperation *op,
1003 const char *uri, gboolean claim_ie,
1004 char **body, int *len)
1007 E2kHTTPStatus status;
1009 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1010 g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1012 msg = get_msg (ctx, uri, TRUE, claim_ie);
1013 status = e2k_context_send_message (ctx, op, msg);
1015 if (E2K_HTTP_STATUS_IS_SUCCESSFUL (status)) {
1017 *body = msg->response.body;
1018 msg->response.owner = SOUP_BUFFER_USER_OWNED;
1021 *len = msg->response.length;
1024 g_object_unref (msg);
1030 static SoupMessage *
1031 put_msg (E2kContext *ctx, const char *uri, const char *content_type,
1032 SoupOwnership buffer_type, const char *body, int length)
1036 msg = e2k_soup_message_new_full (ctx, uri, "PUT", content_type,
1037 buffer_type, body, length);
1038 soup_message_add_header (msg->request_headers, "Translate", "f");
1043 static SoupMessage *
1044 post_msg (E2kContext *ctx, const char *uri, const char *content_type,
1045 SoupOwnership buffer_type, const char *body, int length)
1049 msg = e2k_soup_message_new_full (ctx, uri, "POST", content_type,
1050 buffer_type, body, length);
1051 soup_message_set_flags (msg, SOUP_MESSAGE_NO_REDIRECT);
1057 extract_put_results (SoupMessage *msg, char **location, char **repl_uid)
1061 if (!E2K_HTTP_STATUS_IS_SUCCESSFUL (msg->status_code))
1065 header = soup_message_get_header (msg->response_headers,
1067 *repl_uid = g_strdup (header);
1070 header = soup_message_get_header (msg->response_headers,
1072 *location = g_strdup (header);
1079 * @op: pointer to an #E2kOperation to use for cancellation
1080 * @uri: the URI to PUT to
1081 * @content_type: MIME Content-Type of the data
1082 * @body: data to PUT
1083 * @length: length of @body
1084 * @repl_uid: if not %NULL, will contain the Repl-UID of the PUT
1087 * Performs a PUT operation on @ctx for @uri.
1089 * Return value: the HTTP status
1092 e2k_context_put (E2kContext *ctx, E2kOperation *op, const char *uri,
1093 const char *content_type, const char *body, int length,
1097 E2kHTTPStatus status;
1099 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1100 g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1101 g_return_val_if_fail (content_type != NULL, E2K_HTTP_MALFORMED);
1102 g_return_val_if_fail (body != NULL, E2K_HTTP_MALFORMED);
1104 msg = put_msg (ctx, uri, content_type,
1105 SOUP_BUFFER_USER_OWNED,
1107 status = e2k_context_send_message (ctx, op, msg);
1108 extract_put_results (msg, NULL, repl_uid);
1110 g_object_unref (msg);
1115 * e2k_context_put_new:
1117 * @op: pointer to an #E2kOperation to use for cancellation
1118 * @folder_uri: the URI of the folder to PUT into
1119 * @object_name: base name of the new object (not URI-encoded)
1120 * @test_callback: callback to use to test possible object URIs
1121 * @user_data: data for @test_callback
1122 * @content_type: MIME Content-Type of the data
1123 * @body: data to PUT
1124 * @length: length of @body
1125 * @location: if not %NULL, will contain the Location of the PUT
1127 * @repl_uid: if not %NULL, will contain the Repl-UID of the PUT
1130 * PUTs data into @folder_uri on @ctx with a new name based on
1131 * @object_name. If @test_callback is non-%NULL, it will be called
1132 * with each URI that is considered for the object so that the caller
1133 * can check its summary data to see if that URI is in use
1134 * (potentially saving one or more round-trips to the server).
1136 * Return value: the HTTP status
1139 e2k_context_put_new (E2kContext *ctx, E2kOperation *op,
1140 const char *folder_uri, const char *object_name,
1141 E2kContextTestCallback test_callback, gpointer user_data,
1142 const char *content_type, const char *body, int length,
1143 char **location, char **repl_uid)
1146 E2kHTTPStatus status;
1147 char *slash_uri, *encoded_name;
1150 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1151 g_return_val_if_fail (folder_uri != NULL, E2K_HTTP_MALFORMED);
1152 g_return_val_if_fail (object_name != NULL, E2K_HTTP_MALFORMED);
1153 g_return_val_if_fail (content_type != NULL, E2K_HTTP_MALFORMED);
1154 g_return_val_if_fail (body != NULL, E2K_HTTP_MALFORMED);
1156 slash_uri = e2k_strdup_with_trailing_slash (folder_uri);
1157 encoded_name = e2k_uri_encode (object_name, TRUE, NULL);
1159 /* folder_uri is a dummy here */
1160 msg = put_msg (ctx, folder_uri, content_type,
1161 SOUP_BUFFER_USER_OWNED, body, length);
1162 soup_message_add_header (msg->request_headers, "If-None-Match", "*");
1166 update_unique_uri (ctx, msg, slash_uri, encoded_name, &count,
1167 test_callback, user_data);
1168 status = e2k_context_send_message (ctx, op, msg);
1169 } while (status == E2K_HTTP_PRECONDITION_FAILED);
1171 extract_put_results (msg, location, repl_uid);
1173 g_object_unref (msg);
1175 g_free (encoded_name);
1182 * @op: pointer to an #E2kOperation to use for cancellation
1183 * @uri: the URI to POST to
1184 * @content_type: MIME Content-Type of the data
1185 * @body: data to PUT
1186 * @length: length of @body
1187 * @location: if not %NULL, will contain the Location of the POSTed
1189 * @repl_uid: if not %NULL, will contain the Repl-UID of the POSTed
1192 * Performs a POST operation on @ctx for @uri.
1194 * Note that POSTed objects will be irrevocably(?) marked as "unsent",
1195 * If you open a POSTed message in Outlook, it will open in the
1196 * composer rather than in the message viewer.
1198 * Return value: the HTTP status
1201 e2k_context_post (E2kContext *ctx, E2kOperation *op, const char *uri,
1202 const char *content_type, const char *body, int length,
1203 char **location, char **repl_uid)
1206 E2kHTTPStatus status;
1208 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1209 g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1210 g_return_val_if_fail (content_type != NULL, E2K_HTTP_MALFORMED);
1211 g_return_val_if_fail (body != NULL, E2K_HTTP_MALFORMED);
1213 msg = post_msg (ctx, uri, content_type,
1214 SOUP_BUFFER_USER_OWNED,
1217 status = e2k_context_send_message (ctx, op, msg);
1218 extract_put_results (msg, location, repl_uid);
1220 g_object_unref (msg);
1227 add_namespaces (const char *namespace, char abbrev, gpointer user_data)
1229 GString *propxml = user_data;
1231 g_string_append_printf (propxml, " xmlns:%c=\"%s\"", abbrev, namespace);
1235 write_prop (GString *xml, const char *propertyname,
1236 E2kPropType type, gpointer value, gboolean set)
1238 const char *namespace, *name, *typestr;
1239 char *encoded, abbrev;
1240 gboolean b64enc, need_type;
1245 if (set && (value == NULL))
1248 namespace = e2k_prop_namespace_name (propertyname);
1249 abbrev = e2k_prop_namespace_abbrev (propertyname);
1250 name = e2k_prop_property_name (propertyname);
1252 g_string_append_printf (xml, "<%c:%s", abbrev, name);
1254 /* This means we are removing the property, so just return
1256 g_string_append (xml, "/>");
1260 need_type = (strstr (namespace, "/mapi/id/") != NULL);
1262 g_string_append_c (xml, '>');
1265 case E2K_PROP_TYPE_BINARY:
1267 g_string_append (xml, " T:dt=\"bin.base64\">");
1269 encoded = e2k_base64_encode (data->data, data->len);
1270 g_string_append (xml, encoded);
1274 case E2K_PROP_TYPE_STRING_ARRAY:
1275 typestr = " T:dt=\"mv.string\">";
1279 case E2K_PROP_TYPE_INT_ARRAY:
1280 typestr = " T:dt=\"mv.int\">";
1284 case E2K_PROP_TYPE_BINARY_ARRAY:
1285 typestr = " T:dt=\"mv.bin.base64\">";
1290 g_string_append (xml, typestr);
1292 for (i = 0; i < array->len; i++) {
1293 g_string_append (xml, "<X:v>");
1296 data = array->pdata[i];
1297 encoded = e2k_base64_encode (data->data,
1299 g_string_append (xml, encoded);
1302 e2k_g_string_append_xml_escaped (xml, array->pdata[i]);
1304 g_string_append (xml, "</X:v>");
1308 case E2K_PROP_TYPE_XML:
1309 g_assert_not_reached ();
1312 case E2K_PROP_TYPE_STRING:
1316 case E2K_PROP_TYPE_INT:
1317 typestr = " T:dt=\"int\">";
1319 case E2K_PROP_TYPE_BOOL:
1320 typestr = " T:dt=\"boolean\">";
1322 case E2K_PROP_TYPE_FLOAT:
1323 typestr = " T:dt=\"float\">";
1325 case E2K_PROP_TYPE_DATE:
1326 typestr = " T:dt=\"dateTime.tz\">";
1332 g_string_append (xml, typestr);
1334 e2k_g_string_append_xml_escaped (xml, value);
1339 g_string_append_printf (xml, "</%c:%s>", abbrev, name);
1343 add_set_props (const char *propertyname, E2kPropType type,
1344 gpointer value, gpointer user_data)
1346 GString **props = user_data;
1349 *props = g_string_new (NULL);
1351 write_prop (*props, propertyname, type, value, TRUE);
1355 add_remove_props (const char *propertyname, E2kPropType type,
1356 gpointer value, gpointer user_data)
1358 GString **props = user_data;
1361 *props = g_string_new (NULL);
1363 write_prop (*props, propertyname, type, value, FALSE);
1366 static SoupMessage *
1367 patch_msg (E2kContext *ctx, const char *uri, const char *method,
1368 const char **hrefs, int nhrefs, E2kProperties *props,
1372 GString *propxml, *subxml;
1375 propxml = g_string_new (E2K_XML_HEADER);
1376 g_string_append (propxml, "<D:propertyupdate xmlns:D=\"DAV:\"");
1378 /* Iterate over the properties, noting each namespace once,
1379 * then add them all to the header.
1381 e2k_properties_foreach_namespace (props, add_namespaces, propxml);
1382 g_string_append (propxml, ">\r\n");
1384 /* If this is a BPROPPATCH, add the <target> section. */
1386 g_string_append (propxml, "<D:target>\r\n");
1387 for (i = 0; i < nhrefs; i++) {
1388 g_string_append_printf (propxml, "<D:href>%s</D:href>",
1391 g_string_append (propxml, "\r\n</D:target>\r\n");
1394 /* Add <set> properties. */
1396 e2k_properties_foreach (props, add_set_props, &subxml);
1398 g_string_append (propxml, "<D:set><D:prop>\r\n");
1399 g_string_append (propxml, subxml->str);
1400 g_string_append (propxml, "\r\n</D:prop></D:set>");
1401 g_string_free (subxml, TRUE);
1404 /* Add <remove> properties. */
1406 e2k_properties_foreach_removed (props, add_remove_props, &subxml);
1408 g_string_append (propxml, "<D:remove><D:prop>\r\n");
1409 g_string_append (propxml, subxml->str);
1410 g_string_append (propxml, "\r\n</D:prop></D:remove>");
1411 g_string_free (subxml, TRUE);
1415 g_string_append (propxml, "\r\n</D:propertyupdate>");
1417 /* And build the message. */
1418 msg = e2k_soup_message_new_full (ctx, uri, method,
1419 "text/xml", SOUP_BUFFER_SYSTEM_OWNED,
1420 propxml->str, propxml->len);
1421 g_string_free (propxml, FALSE);
1422 soup_message_add_header (msg->request_headers, "Brief", "t");
1424 soup_message_add_header (msg->request_headers, "If-Match", "*");
1430 * e2k_context_proppatch:
1432 * @op: pointer to an #E2kOperation to use for cancellation
1433 * @uri: the URI to PROPPATCH
1434 * @props: the properties to set/remove
1435 * @create: whether or not to create @uri if it does not exist
1436 * @repl_uid: if not %NULL, will contain the Repl-UID of the
1437 * PROPPATCHed object on return
1439 * Performs a PROPPATCH operation on @ctx for @uri.
1441 * If @create is %FALSE and @uri does not already exist, the response
1442 * code will be %E2K_HTTP_PRECONDITION_FAILED.
1444 * Return value: the HTTP status
1447 e2k_context_proppatch (E2kContext *ctx, E2kOperation *op,
1448 const char *uri, E2kProperties *props,
1449 gboolean create, char **repl_uid)
1452 E2kHTTPStatus status;
1454 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1455 g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1456 g_return_val_if_fail (props != NULL, E2K_HTTP_MALFORMED);
1458 msg = patch_msg (ctx, uri, "PROPPATCH", NULL, 0, props, create);
1459 status = e2k_context_send_message (ctx, op, msg);
1460 extract_put_results (msg, NULL, repl_uid);
1462 g_object_unref (msg);
1467 * e2k_context_proppatch_new:
1469 * @op: pointer to an #E2kOperation to use for cancellation
1470 * @folder_uri: the URI of the folder to PROPPATCH a new object in
1471 * @object_name: base name of the new object (not URI-encoded)
1472 * @test_callback: callback to use to test possible object URIs
1473 * @user_data: data for @test_callback
1474 * @props: the properties to set/remove
1475 * @location: if not %NULL, will contain the Location of the
1476 * PROPPATCHed object on return
1477 * @repl_uid: if not %NULL, will contain the Repl-UID of the
1478 * PROPPATCHed object on return
1480 * PROPPATCHes data into @folder_uri on @ctx with a new name based on
1481 * @object_name. If @test_callback is non-%NULL, it will be called
1482 * with each URI that is considered for the object so that the caller
1483 * can check its summary data to see if that URI is in use
1484 * (potentially saving one or more round-trips to the server).
1486 * Return value: the HTTP status
1489 e2k_context_proppatch_new (E2kContext *ctx, E2kOperation *op,
1490 const char *folder_uri, const char *object_name,
1491 E2kContextTestCallback test_callback,
1493 E2kProperties *props,
1494 char **location, char **repl_uid)
1497 E2kHTTPStatus status;
1498 char *slash_uri, *encoded_name;
1501 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1502 g_return_val_if_fail (folder_uri != NULL, E2K_HTTP_MALFORMED);
1503 g_return_val_if_fail (object_name != NULL, E2K_HTTP_MALFORMED);
1504 g_return_val_if_fail (props != NULL, E2K_HTTP_MALFORMED);
1506 slash_uri = e2k_strdup_with_trailing_slash (folder_uri);
1507 encoded_name = e2k_uri_encode (object_name, TRUE, NULL);
1509 /* folder_uri is a dummy here */
1510 msg = patch_msg (ctx, folder_uri, "PROPPATCH", NULL, 0, props, TRUE);
1511 soup_message_add_header (msg->request_headers, "If-None-Match", "*");
1515 update_unique_uri (ctx, msg, slash_uri, encoded_name, &count,
1516 test_callback, user_data);
1517 status = e2k_context_send_message (ctx, op, msg);
1518 } while (status == E2K_HTTP_PRECONDITION_FAILED);
1521 *location = soup_uri_to_string (soup_message_get_uri (msg), FALSE);
1522 extract_put_results (msg, NULL, repl_uid);
1524 g_object_unref (msg);
1526 g_free (encoded_name);
1530 static E2kHTTPStatus
1531 bproppatch_fetch (E2kResultIter *iter,
1532 E2kContext *ctx, E2kOperation *op,
1533 E2kResult **results, int *nresults,
1534 int *first, int *total,
1537 SoupMessage *msg = user_data;
1538 E2kHTTPStatus status;
1540 if (msg->status != SOUP_MESSAGE_STATUS_IDLE)
1543 status = e2k_context_send_message (ctx, op, msg);
1544 if (status == E2K_HTTP_MULTI_STATUS) {
1545 e2k_results_from_multistatus (msg, results, nresults);
1552 bproppatch_free (E2kResultIter *iter, gpointer msg)
1554 g_object_unref (msg);
1558 * e2k_context_bproppatch_start:
1560 * @op: pointer to an #E2kOperation to use for cancellation
1561 * @uri: the base URI
1562 * @hrefs: array of URIs, possibly relative to @uri
1563 * @nhrefs: length of @hrefs
1564 * @props: the properties to set/remove
1565 * @create: whether or not to create @uri if it does not exist
1567 * Begins a BPROPPATCH (bulk PROPPATCH) of @hrefs based at @uri.
1569 * Return value: an iterator for getting the results of the BPROPPATCH
1572 e2k_context_bproppatch_start (E2kContext *ctx, E2kOperation *op,
1573 const char *uri, const char **hrefs, int nhrefs,
1574 E2kProperties *props, gboolean create)
1578 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), NULL);
1579 g_return_val_if_fail (uri != NULL, NULL);
1580 g_return_val_if_fail (props != NULL, NULL);
1582 msg = patch_msg (ctx, uri, "BPROPPATCH", hrefs, nhrefs, props, create);
1583 return e2k_result_iter_new (ctx, op, TRUE, -1,
1584 bproppatch_fetch, bproppatch_free,
1590 static SoupMessage *
1591 propfind_msg (E2kContext *ctx, const char *base_uri,
1592 const char **props, int nprops, const char **hrefs, int nhrefs)
1596 GData *set_namespaces;
1601 propxml = g_string_new (E2K_XML_HEADER);
1602 g_string_append (propxml, "<D:propfind xmlns:D=\"DAV:\"");
1604 set_namespaces = NULL;
1605 for (i = 0; i < nprops; i++) {
1606 name = e2k_prop_namespace_name (props[i]);
1607 abbrev = e2k_prop_namespace_abbrev (props[i]);
1609 if (!g_datalist_get_data (&set_namespaces, name)) {
1610 g_datalist_set_data (&set_namespaces, name,
1611 GINT_TO_POINTER (1));
1612 g_string_append_printf (propxml, " xmlns:%c=\"%s\"",
1616 g_datalist_clear (&set_namespaces);
1617 g_string_append (propxml, ">\r\n");
1620 g_string_append (propxml, "<D:target>\r\n");
1621 for (i = 0; i < nhrefs; i++) {
1622 g_string_append_printf (propxml, "<D:href>%s</D:href>",
1625 g_string_append (propxml, "\r\n</D:target>\r\n");
1628 g_string_append (propxml, "<D:prop>\r\n");
1629 for (i = 0; i < nprops; i++) {
1630 abbrev = e2k_prop_namespace_abbrev (props[i]);
1631 name = e2k_prop_property_name (props[i]);
1632 g_string_append_printf (propxml, "<%c:%s/>", abbrev, name);
1634 g_string_append (propxml, "\r\n</D:prop>\r\n</D:propfind>");
1636 msg = e2k_soup_message_new_full (ctx, base_uri,
1637 hrefs ? "BPROPFIND" : "PROPFIND",
1638 "text/xml", SOUP_BUFFER_SYSTEM_OWNED,
1639 propxml->str, propxml->len);
1640 g_string_free (propxml, FALSE);
1641 soup_message_add_header (msg->request_headers, "Brief", "t");
1642 soup_message_add_header (msg->request_headers, "Depth", "0");
1648 * e2k_context_propfind:
1650 * @op: pointer to an #E2kOperation to use for cancellation
1651 * @uri: the URI to PROPFIND on
1652 * @props: array of properties to find
1653 * @nprops: length of @props
1654 * @results: on return, the results
1655 * @nresults: length of @results
1657 * Performs a PROPFIND operation on @ctx for @uri. If successful, the
1658 * results are returned as an array of #E2kResult (which you must free
1659 * with e2k_results_free()), but the array will always have either 0
1662 * Return value: the HTTP status
1665 e2k_context_propfind (E2kContext *ctx, E2kOperation *op,
1666 const char *uri, const char **props, int nprops,
1667 E2kResult **results, int *nresults)
1670 E2kHTTPStatus status;
1672 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1673 g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1674 g_return_val_if_fail (props != NULL, E2K_HTTP_MALFORMED);
1676 msg = propfind_msg (ctx, uri, props, nprops, NULL, 0);
1677 status = e2k_context_send_message (ctx, op, msg);
1679 if (msg->status_code == E2K_HTTP_MULTI_STATUS)
1680 e2k_results_from_multistatus (msg, results, nresults);
1681 g_object_unref (msg);
1685 static E2kHTTPStatus
1686 bpropfind_fetch (E2kResultIter *iter,
1687 E2kContext *ctx, E2kOperation *op,
1688 E2kResult **results, int *nresults,
1689 int *first, int *total,
1692 GSList **msgs = user_data;
1693 E2kHTTPStatus status;
1699 msg = (*msgs)->data;
1700 *msgs = g_slist_remove (*msgs, msg);
1702 status = e2k_context_send_message (ctx, op, msg);
1703 if (status == E2K_HTTP_MULTI_STATUS)
1704 e2k_results_from_multistatus (msg, results, nresults);
1705 g_object_unref (msg);
1711 bpropfind_free (E2kResultIter *iter, gpointer user_data)
1713 GSList **msgs = user_data, *m;
1715 for (m = *msgs; m; m = m->next)
1716 g_object_unref (m->data);
1717 g_slist_free (*msgs);
1722 * e2k_context_bpropfind_start:
1724 * @op: pointer to an #E2kOperation to use for cancellation
1725 * @uri: the base URI
1726 * @hrefs: array of URIs, possibly relative to @uri
1727 * @nhrefs: length of @hrefs
1728 * @props: array of properties to find
1729 * @nprops: length of @props
1731 * Begins a BPROPFIND (bulk PROPFIND) operation on @ctx for @hrefs.
1733 * Return value: an iterator for getting the results
1736 e2k_context_bpropfind_start (E2kContext *ctx, E2kOperation *op,
1737 const char *uri, const char **hrefs, int nhrefs,
1738 const char **props, int nprops)
1744 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), NULL);
1745 g_return_val_if_fail (uri != NULL, NULL);
1746 g_return_val_if_fail (props != NULL, NULL);
1747 g_return_val_if_fail (hrefs != NULL, NULL);
1749 msgs = g_new0 (GSList *, 1);
1750 for (i = 0; i < nhrefs; i += E2K_CONTEXT_MAX_BATCH_SIZE) {
1751 msg = propfind_msg (ctx, uri, props, nprops,
1752 hrefs + i, MIN (E2K_CONTEXT_MAX_BATCH_SIZE, nhrefs - i));
1753 *msgs = g_slist_append (*msgs, msg);
1756 return e2k_result_iter_new (ctx, op, TRUE, nhrefs,
1757 bpropfind_fetch, bpropfind_free,
1763 static SoupMessage *
1764 search_msg (E2kContext *ctx, const char *uri,
1765 SoupOwnership buffer_type, const char *searchxml,
1766 int size, gboolean ascending, int offset)
1770 msg = e2k_soup_message_new_full (ctx, uri, "SEARCH", "text/xml",
1771 buffer_type, searchxml,
1772 strlen (searchxml));
1773 soup_message_add_header (msg->request_headers, "Brief", "t");
1778 if (offset == INT_MAX) {
1779 range = g_strdup_printf ("rows=-%u", size);
1781 range = g_strdup_printf ("rows=%u-%u",
1782 offset, offset + size - 1);
1784 soup_message_add_header (msg->request_headers, "Range", range);
1792 search_xml (const char **props, int nprops,
1793 E2kRestriction *rn, const char *orderby)
1799 xml = g_string_new (E2K_XML_HEADER);
1800 g_string_append (xml, "<searchrequest xmlns=\"DAV:\"><sql>\r\n");
1801 g_string_append (xml, "SELECT ");
1803 for (i = 0; i < nprops; i++) {
1805 g_string_append (xml, ", ");
1806 g_string_append_c (xml, '"');
1807 g_string_append (xml, props[i]);
1808 g_string_append_c (xml, '"');
1811 if (e2k_restriction_folders_only (rn))
1812 g_string_append_printf (xml, "\r\nFROM SCOPE('hierarchical traversal of \"\"')\r\n");
1814 g_string_append (xml, "\r\nFROM \"\"\r\n");
1817 where = e2k_restriction_to_sql (rn);
1819 e2k_g_string_append_xml_escaped (xml, where);
1820 g_string_append (xml, "\r\n");
1826 g_string_append_printf (xml, "ORDER BY \"%s\"\r\n", orderby);
1828 g_string_append (xml, "</sql></searchrequest>");
1831 g_string_free (xml, FALSE);
1837 search_result_get_range (SoupMessage *msg, int *first, int *total)
1839 const char *range, *p;
1841 range = soup_message_get_header (msg->response_headers,
1845 p = strstr (range, "rows ");
1850 *first = atoi (p + 5);
1853 p = strstr (range, "total=");
1855 *total = atoi (p + 6);
1866 int batch_size, next;
1869 static E2kHTTPStatus
1870 search_fetch (E2kResultIter *iter,
1871 E2kContext *ctx, E2kOperation *op,
1872 E2kResult **results, int *nresults,
1873 int *first, int *total,
1876 E2kSearchData *search_data = user_data;
1877 E2kHTTPStatus status;
1880 if (search_data->batch_size == 0)
1883 msg = search_msg (ctx, search_data->uri,
1884 SOUP_BUFFER_USER_OWNED, search_data->xml,
1885 search_data->batch_size,
1886 search_data->ascending, search_data->next);
1887 status = e2k_context_send_message (ctx, op, msg);
1888 if (msg->status_code == E2K_HTTP_REQUESTED_RANGE_NOT_SATISFIABLE)
1889 status = E2K_HTTP_OK;
1890 else if (status == E2K_HTTP_MULTI_STATUS) {
1891 search_result_get_range (msg, first, total);
1895 e2k_results_from_multistatus (msg, results, nresults);
1897 *total = *first + *nresults;
1899 if (search_data->ascending && *first + *nresults < *total)
1900 search_data->next = *first + *nresults;
1901 else if (!search_data->ascending && *first > 0) {
1902 if (*first >= search_data->batch_size)
1903 search_data->next = *first - search_data->batch_size;
1905 search_data->batch_size = *first;
1906 search_data->next = 0;
1909 search_data->batch_size = 0;
1913 g_object_unref (msg);
1918 search_free (E2kResultIter *iter, gpointer user_data)
1920 E2kSearchData *search_data = user_data;
1922 g_free (search_data->uri);
1923 g_free (search_data->xml);
1924 g_free (search_data);
1928 * e2k_context_search_start:
1930 * @op: pointer to an #E2kOperation to use for cancellation
1931 * @uri: the folder to search
1932 * @props: the properties to search for
1933 * @nprops: size of @props array
1934 * @rn: the search restriction
1935 * @orderby: if non-%NULL, the field to sort the search results by
1936 * @ascending: %TRUE for an ascending search, %FALSE for descending.
1938 * Begins a SEARCH on @ctx at @uri.
1940 * Return value: an iterator for returning the search results
1943 e2k_context_search_start (E2kContext *ctx, E2kOperation *op, const char *uri,
1944 const char **props, int nprops, E2kRestriction *rn,
1945 const char *orderby, gboolean ascending)
1947 E2kSearchData *search_data;
1949 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), NULL);
1950 g_return_val_if_fail (uri != NULL, NULL);
1951 g_return_val_if_fail (props != NULL, NULL);
1953 search_data = g_new0 (E2kSearchData, 1);
1954 search_data->uri = g_strdup (uri);
1955 search_data->xml = search_xml (props, nprops, rn, orderby);
1956 search_data->ascending = ascending;
1957 search_data->batch_size = E2K_CONTEXT_MAX_BATCH_SIZE;
1958 search_data->next = ascending ? 0 : INT_MAX;
1960 return e2k_result_iter_new (ctx, op, ascending, -1,
1961 search_fetch, search_free,
1969 static SoupMessage *
1970 delete_msg (E2kContext *ctx, const char *uri)
1972 return e2k_soup_message_new (ctx, uri, "DELETE");
1976 * e2k_context_delete:
1978 * @op: pointer to an #E2kOperation to use for cancellation
1979 * @uri: URI to DELETE
1981 * Attempts to DELETE @uri on @ctx.
1983 * Return value: the HTTP status
1986 e2k_context_delete (E2kContext *ctx, E2kOperation *op, const char *uri)
1989 E2kHTTPStatus status;
1991 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
1992 g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
1994 msg = delete_msg (ctx, uri);
1995 status = e2k_context_send_message (ctx, op, msg);
1997 g_object_unref (msg);
2003 static SoupMessage *
2004 bdelete_msg (E2kContext *ctx, const char *uri, const char **hrefs, int nhrefs)
2010 xml = g_string_new (E2K_XML_HEADER "<delete xmlns=\"DAV:\"><target>");
2012 for (i = 0; i < nhrefs; i++) {
2013 g_string_append (xml, "<href>");
2014 e2k_g_string_append_xml_escaped (xml, hrefs[i]);
2015 g_string_append (xml, "</href>");
2018 g_string_append (xml, "</target></delete>");
2020 msg = e2k_soup_message_new_full (ctx, uri, "BDELETE", "text/xml",
2021 SOUP_BUFFER_SYSTEM_OWNED,
2022 xml->str, xml->len);
2023 g_string_free (xml, FALSE);
2028 static E2kHTTPStatus
2029 bdelete_fetch (E2kResultIter *iter,
2030 E2kContext *ctx, E2kOperation *op,
2031 E2kResult **results, int *nresults,
2032 int *first, int *total,
2035 GSList **msgs = user_data;
2036 E2kHTTPStatus status;
2042 msg = (*msgs)->data;
2043 *msgs = g_slist_remove (*msgs, msg);
2045 status = e2k_context_send_message (ctx, op, msg);
2046 if (status == E2K_HTTP_MULTI_STATUS)
2047 e2k_results_from_multistatus (msg, results, nresults);
2048 g_object_unref (msg);
2054 bdelete_free (E2kResultIter *iter, gpointer user_data)
2056 GSList **msgs = user_data, *m;
2058 for (m = (*msgs); m; m = m->next)
2059 g_object_unref (m->data);
2060 g_slist_free (*msgs);
2065 * e2k_context_bdelete_start:
2067 * @op: pointer to an #E2kOperation to use for cancellation
2068 * @uri: the base URI
2069 * @hrefs: array of URIs, possibly relative to @uri, to delete
2070 * @nhrefs: length of @hrefs
2072 * Begins a BDELETE (bulk DELETE) operation on @ctx for @hrefs.
2074 * Return value: an iterator for returning the results
2077 e2k_context_bdelete_start (E2kContext *ctx, E2kOperation *op,
2078 const char *uri, const char **hrefs, int nhrefs)
2084 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), NULL);
2085 g_return_val_if_fail (uri != NULL, NULL);
2086 g_return_val_if_fail (hrefs != NULL, NULL);
2088 batchsize = (nhrefs + 9) / 10;
2089 if (batchsize < E2K_CONTEXT_MIN_BATCH_SIZE)
2090 batchsize = E2K_CONTEXT_MIN_BATCH_SIZE;
2091 else if (batchsize > E2K_CONTEXT_MAX_BATCH_SIZE)
2092 batchsize = E2K_CONTEXT_MAX_BATCH_SIZE;
2094 msgs = g_new0 (GSList *, 1);
2095 for (i = 0; i < nhrefs; i += batchsize) {
2096 batchsize = MIN (batchsize, nhrefs - i);
2097 msg = bdelete_msg (ctx, uri, hrefs + i, batchsize);
2098 *msgs = g_slist_prepend (*msgs, msg);
2101 return e2k_result_iter_new (ctx, op, TRUE, nhrefs,
2102 bdelete_fetch, bdelete_free,
2109 * e2k_context_mkcol:
2111 * @op: pointer to an #E2kOperation to use for cancellation
2112 * @uri: URI of the new folder
2113 * @props: properties to set on the new folder, or %NULL
2114 * @permanent_url: if not %NULL, will contain the permanent URL of the
2115 * new folder on return
2117 * Performs a MKCOL operation on @ctx to create @uri, with optional
2118 * additional properties.
2120 * Return value: the HTTP status
2123 e2k_context_mkcol (E2kContext *ctx, E2kOperation *op,
2124 const char *uri, E2kProperties *props,
2125 char **permanent_url)
2128 E2kHTTPStatus status;
2130 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
2131 g_return_val_if_fail (uri != NULL, E2K_HTTP_MALFORMED);
2134 msg = e2k_soup_message_new (ctx, uri, "MKCOL");
2136 msg = patch_msg (ctx, uri, "MKCOL", NULL, 0, props, TRUE);
2138 status = e2k_context_send_message (ctx, op, msg);
2139 if (E2K_HTTP_STATUS_IS_SUCCESSFUL (status) && permanent_url) {
2142 header = soup_message_get_header (msg->response_headers,
2143 "MS-Exchange-Permanent-URL");
2144 *permanent_url = g_strdup (header);
2147 g_object_unref (msg);
2153 static SoupMessage *
2154 transfer_msg (E2kContext *ctx,
2155 const char *source_uri, const char *dest_uri,
2156 const char **source_hrefs, int nhrefs,
2157 gboolean delete_originals)
2163 xml = g_string_new (E2K_XML_HEADER);
2164 g_string_append (xml, delete_originals ? "<move" : "<copy");
2165 g_string_append (xml, " xmlns=\"DAV:\"><target>");
2166 for (i = 0; i < nhrefs; i++) {
2167 g_string_append (xml, "<href>");
2168 e2k_g_string_append_xml_escaped (xml, source_hrefs[i]);
2169 g_string_append (xml, "</href>");
2171 g_string_append (xml, "</target></");
2172 g_string_append (xml, delete_originals ? "move>" : "copy>");
2174 msg = e2k_soup_message_new_full (ctx, source_uri,
2175 delete_originals ? "BMOVE" : "BCOPY",
2177 SOUP_BUFFER_SYSTEM_OWNED,
2178 xml->str, xml->len);
2179 soup_message_add_header (msg->request_headers, "Overwrite", "f");
2180 soup_message_add_header (msg->request_headers, "Allow-Rename", "t");
2181 soup_message_add_header (msg->request_headers, "Destination", dest_uri);
2182 g_string_free (xml, FALSE);
2187 static E2kHTTPStatus
2188 transfer_next (E2kResultIter *iter,
2189 E2kContext *ctx, E2kOperation *op,
2190 E2kResult **results, int *nresults,
2191 int *first, int *total,
2194 GSList **msgs = user_data;
2196 E2kHTTPStatus status;
2201 msg = (*msgs)->data;
2202 *msgs = g_slist_remove (*msgs, msg);
2204 status = e2k_context_send_message (ctx, op, msg);
2205 if (status == E2K_HTTP_MULTI_STATUS)
2206 e2k_results_from_multistatus (msg, results, nresults);
2208 g_object_unref (msg);
2213 transfer_free (E2kResultIter *iter, gpointer user_data)
2215 GSList **msgs = user_data, *m;
2217 for (m = *msgs; m; m = m->next)
2218 g_object_unref (m->data);
2219 g_slist_free (*msgs);
2224 * e2k_context_transfer_start:
2226 * @op: pointer to an #E2kOperation to use for cancellation
2227 * @source_folder: URI of the source folder
2228 * @dest_folder: URI of the destination folder
2229 * @source_hrefs: an array of hrefs to move, relative to @source_folder
2230 * @delete_originals: whether or not to delete the original objects
2232 * Starts a BMOVE or BCOPY (depending on @delete_originals) operation
2233 * on @ctx for @source_folder. The objects in @source_folder described
2234 * by @source_hrefs will be moved or copied to @dest_folder.
2235 * e2k_result_iter_next() can be used to check the success or failure
2236 * of each move/copy. (The #E2K_PR_DAV_LOCATION property for each
2237 * result will show the new location of the object.)
2239 * NB: may not work correctly if @source_hrefs contains folders
2241 * Return value: the iterator for the results
2244 e2k_context_transfer_start (E2kContext *ctx, E2kOperation *op,
2245 const char *source_folder, const char *dest_folder,
2246 GPtrArray *source_hrefs, gboolean delete_originals)
2254 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), NULL);
2255 g_return_val_if_fail (source_folder != NULL, NULL);
2256 g_return_val_if_fail (dest_folder != NULL, NULL);
2257 g_return_val_if_fail (source_hrefs != NULL, NULL);
2259 dest_uri = e2k_strdup_with_trailing_slash (dest_folder);
2260 hrefs = (const char **)source_hrefs->pdata;
2262 msgs = g_new0 (GSList *, 1);
2263 for (i = 0; i < source_hrefs->len; i += E2K_CONTEXT_MAX_BATCH_SIZE) {
2264 msg = transfer_msg (ctx, source_folder, dest_uri,
2265 hrefs + i, MIN (E2K_CONTEXT_MAX_BATCH_SIZE, source_hrefs->len - i),
2267 *msgs = g_slist_append (*msgs, msg);
2271 return e2k_result_iter_new (ctx, op, TRUE, source_hrefs->len,
2272 transfer_next, transfer_free,
2277 * e2k_context_transfer_dir:
2279 * @op: pointer to an #E2kOperation to use for cancellation
2280 * @source_href: URI of the source folder
2281 * @dest_href: URI of the destination folder
2282 * @delete_original: whether or not to delete the original folder
2283 * @permanent_url: if not %NULL, will contain the permanent URL of the
2284 * new folder on return
2286 * Performs a MOVE or COPY (depending on @delete_original) operation
2287 * on @ctx for @source_href. The folder itself will be moved, renamed,
2288 * or copied to @dest_href (which is the name of the new folder
2289 * itself, not its parent).
2291 * Return value: the HTTP status
2294 e2k_context_transfer_dir (E2kContext *ctx, E2kOperation *op,
2295 const char *source_href, const char *dest_href,
2296 gboolean delete_original,
2297 char **permanent_url)
2300 E2kHTTPStatus status;
2302 g_return_val_if_fail (E2K_IS_CONTEXT (ctx), E2K_HTTP_MALFORMED);
2303 g_return_val_if_fail (source_href != NULL, E2K_HTTP_MALFORMED);
2304 g_return_val_if_fail (dest_href != NULL, E2K_HTTP_MALFORMED);
2306 msg = e2k_soup_message_new (ctx, source_href, delete_original ? "MOVE" : "COPY");
2307 soup_message_add_header (msg->request_headers, "Overwrite", "f");
2308 soup_message_add_header (msg->request_headers, "Destination", dest_href);
2310 status = e2k_context_send_message (ctx, op, msg);
2311 if (E2K_HTTP_STATUS_IS_SUCCESSFUL (status) && permanent_url) {
2314 header = soup_message_get_header (msg->response_headers,
2315 "MS-Exchange-Permanent-URL");
2316 *permanent_url = g_strdup (header);
2319 g_object_unref (msg);
2329 E2kContextChangeType type;
2330 int lifetime, min_interval;
2331 time_t last_notification;
2333 E2kContextChangeCallback callback;
2336 guint renew_timeout;
2337 SoupMessage *renew_msg;
2339 SoupMessage *poll_msg;
2340 guint notification_timeout;
2344 belated_notification (gpointer user_data)
2346 E2kSubscription *sub = user_data;
2348 sub->notification_timeout = 0;
2349 sub->callback (sub->ctx, sub->uri, sub->type, sub->user_data);
2354 maybe_notification (E2kSubscription *sub)
2356 time_t now = time (NULL);
2357 int delay = sub->last_notification + sub->min_interval - now;
2360 if (sub->notification_timeout)
2361 g_source_remove (sub->notification_timeout);
2362 sub->notification_timeout = g_timeout_add (delay * 1000,
2363 belated_notification,
2367 sub->last_notification = now;
2369 sub->callback (sub->ctx, sub->uri, sub->type, sub->user_data);
2373 polled (SoupMessage *msg, gpointer user_data)
2375 E2kSubscription *sub = user_data;
2376 E2kContext *ctx = sub->ctx;
2382 sub->poll_msg = NULL;
2383 if (msg->status_code != E2K_HTTP_MULTI_STATUS) {
2384 g_warning ("Unexpected error %d %s from POLL",
2385 msg->status_code, msg->reason_phrase);
2389 e2k_results_from_multistatus (msg, &results, &nresults);
2390 for (i = 0; i < nresults; i++) {
2391 if (results[i].status != E2K_HTTP_OK)
2394 ids = e2k_properties_get_prop (results[i].props, E2K_PR_SUBSCRIPTION_ID);
2397 for (ids = ids->xmlChildrenNode; ids; ids = ids->next) {
2398 if (strcmp (ids->name, "li") != 0 ||
2399 !ids->xmlChildrenNode ||
2400 !ids->xmlChildrenNode->content)
2402 id = ids->xmlChildrenNode->content;
2403 sub = g_hash_table_lookup (ctx->priv->subscriptions_by_id, id);
2405 maybe_notification (sub);
2408 e2k_results_free (results, nresults);
2412 timeout_notification (gpointer user_data)
2414 E2kSubscription *sub = user_data, *sub2;
2415 E2kContext *ctx = sub->ctx;
2417 GString *subscription_ids;
2419 sub->poll_timeout = 0;
2420 subscription_ids = g_string_new (sub->id);
2422 /* Find all subscriptions at this URI that are awaiting a
2423 * POLL so we can POLL them all at once.
2425 sub_list = g_hash_table_lookup (ctx->priv->subscriptions_by_uri,
2427 for (; sub_list; sub_list = sub_list->next) {
2428 sub2 = sub_list->data;
2431 if (!sub2->poll_timeout)
2433 g_source_remove (sub2->poll_timeout);
2434 sub2->poll_timeout = 0;
2435 g_string_append_printf (subscription_ids, ",%s", sub2->id);
2438 sub->poll_msg = e2k_soup_message_new (ctx, sub->uri, "POLL");
2439 soup_message_add_header (sub->poll_msg->request_headers,
2440 "Subscription-id", subscription_ids->str);
2441 e2k_context_queue_message (ctx, sub->poll_msg, polled, sub);
2443 g_string_free (subscription_ids, TRUE);
2448 do_notification (GIOChannel *source, GIOCondition condition, gpointer data)
2450 E2kContext *ctx = data;
2451 E2kSubscription *sub;
2452 char buffer[1024], *id, *lasts;
2456 status = g_io_channel_read_chars (source, buffer, sizeof (buffer) - 1, &len, NULL);
2457 if (status != G_IO_STATUS_NORMAL && status != G_IO_STATUS_AGAIN) {
2458 g_warning ("do_notification I/O error: %d (%s)", status,
2459 g_strerror (errno));
2465 if (e2k_debug_level) {
2466 if (e2k_debug_level == 1) {
2467 fwrite (buffer, 1, strcspn (buffer, "\r\n"), stdout);
2468 fputs ("\n\n", stdout);
2470 fputs (buffer, stdout);
2474 if (g_ascii_strncasecmp (buffer, "NOTIFY ", 7) != 0)
2479 id = strchr (id, '\n');
2482 if (g_ascii_strncasecmp (id, "Subscription-id: ", 17) == 0)
2487 for (id = strtok_r (id, ",\r", &lasts); id; id = strtok_r (NULL, ",\r", &lasts)) {
2488 sub = g_hash_table_lookup (ctx->priv->subscriptions_by_id, id);
2492 /* We don't want to POLL right away in case there are
2493 * several changes in a row. So we just bump up the
2494 * timeout to be one second from now. (Using an idle
2495 * handler here doesn't actually work to prevent
2498 if (sub->poll_timeout)
2499 g_source_remove (sub->poll_timeout);
2501 g_timeout_add (1000, timeout_notification, sub);
2508 renew_cb (SoupMessage *msg, gpointer user_data)
2510 E2kSubscription *sub = user_data;
2512 sub->renew_msg = NULL;
2513 if (!E2K_HTTP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
2514 g_warning ("renew_subscription: %d %s", msg->status_code,
2515 msg->reason_phrase);
2520 g_hash_table_remove (sub->ctx->priv->subscriptions_by_id, sub->id);
2523 sub->id = g_strdup (soup_message_get_header (msg->response_headers,
2524 "Subscription-id"));
2525 g_return_if_fail (sub->id != NULL);
2526 g_hash_table_insert (sub->ctx->priv->subscriptions_by_id,
2530 #define E2K_SUBSCRIPTION_INITIAL_LIFETIME 3600 /* 1 hour */
2531 #define E2K_SUBSCRIPTION_MAX_LIFETIME 57600 /* 16 hours */
2533 /* This must be kept in sync with E2kSubscriptionType */
2534 static char *subscription_type[] = {
2535 "update", /* E2K_SUBSCRIPTION_OBJECT_CHANGED */
2536 "update/newmember", /* E2K_SUBSCRIPTION_OBJECT_ADDED */
2537 "delete", /* E2K_SUBSCRIPTION_OBJECT_REMOVED */
2538 "move" /* E2K_SUBSCRIPTION_OBJECT_MOVED */
2542 renew_subscription (gpointer user_data)
2544 E2kSubscription *sub = user_data;
2545 E2kContext *ctx = sub->ctx;
2548 if (!ctx->priv->notification_uri)
2551 if (sub->lifetime < E2K_SUBSCRIPTION_MAX_LIFETIME)
2554 sub->renew_msg = e2k_soup_message_new (ctx, sub->uri, "SUBSCRIBE");
2555 sprintf (ltbuf, "%d", sub->lifetime);
2556 soup_message_add_header (sub->renew_msg->request_headers,
2557 "Subscription-lifetime", ltbuf);
2558 soup_message_add_header (sub->renew_msg->request_headers,
2559 "Notification-type",
2560 subscription_type[sub->type]);
2561 if (sub->min_interval > 1) {
2562 sprintf (ltbuf, "%d", sub->min_interval);
2563 soup_message_add_header (sub->renew_msg->request_headers,
2564 "Notification-delay", ltbuf);
2566 soup_message_add_header (sub->renew_msg->request_headers,
2567 "Call-back", ctx->priv->notification_uri);
2569 e2k_context_queue_message (ctx, sub->renew_msg, renew_cb, sub);
2570 sub->renew_timeout = g_timeout_add ((sub->lifetime - 60) * 1000,
2571 renew_subscription, sub);
2576 * e2k_context_subscribe:
2578 * @uri: the folder URI to subscribe to notifications on
2579 * @type: the type of notification to subscribe to
2580 * @min_interval: the minimum interval (in seconds) between
2582 * @callback: the callback to call when a notification has been
2584 * @user_data: data to pass to @callback.
2586 * This subscribes to change notifications of the given @type on @uri.
2587 * @callback will (eventually) be invoked any time the folder changes
2588 * in the given way: whenever an object is added to it for
2589 * %E2K_CONTEXT_OBJECT_ADDED, whenever an object is deleted (but
2590 * not moved) from it (or the folder itself is deleted) for
2591 * %E2K_CONTEXT_OBJECT_REMOVED, whenever an object is moved in or
2592 * out of the folder for %E2K_CONTEXT_OBJECT_MOVED, and whenever
2593 * any of the above happens, or the folder or one of its items is
2594 * modified, for %E2K_CONTEXT_OBJECT_CHANGED. (This means that if
2595 * you subscribe to both CHANGED and some other notification on the
2596 * same folder that multiple callbacks may be invoked every time an
2597 * object is added/removed/moved/etc.)
2599 * Notifications can be used *only* to discover changes made by other
2600 * clients! The code cannot assume that it will receive a notification
2601 * for every change that it makes to the server, for two reasons:
2603 * First, if multiple notifications occur within @min_interval seconds
2604 * of each other, the later ones will be suppressed, to avoid
2605 * excessive traffic between the client and the server as the client
2606 * tries to sync. Second, if there is a firewall between the client
2607 * and the server, it is possible that all notifications will be lost.
2610 e2k_context_subscribe (E2kContext *ctx, const char *uri,
2611 E2kContextChangeType type, int min_interval,
2612 E2kContextChangeCallback callback,
2615 E2kSubscription *sub;
2617 gpointer key, value;
2619 g_return_if_fail (E2K_IS_CONTEXT (ctx));
2621 sub = g_new0 (E2kSubscription, 1);
2623 sub->uri = g_strdup (uri);
2625 sub->lifetime = E2K_SUBSCRIPTION_INITIAL_LIFETIME / 2;
2626 sub->min_interval = min_interval;
2627 sub->callback = callback;
2628 sub->user_data = user_data;
2630 if (g_hash_table_lookup_extended (ctx->priv->subscriptions_by_uri,
2631 uri, &key, &value)) {
2633 sub_list = g_list_prepend (sub_list, sub);
2634 g_hash_table_insert (ctx->priv->subscriptions_by_uri,
2637 g_hash_table_insert (ctx->priv->subscriptions_by_uri,
2638 sub->uri, g_list_prepend (NULL, sub));
2641 renew_subscription (sub);
2645 free_subscription (E2kSubscription *sub)
2647 SoupSession *session = sub->ctx->priv->session;
2649 if (sub->renew_timeout)
2650 g_source_remove (sub->renew_timeout);
2652 soup_session_cancel_message (session, sub->renew_msg);
2653 if (sub->poll_timeout)
2654 g_source_remove (sub->poll_timeout);
2655 if (sub->notification_timeout)
2656 g_source_remove (sub->notification_timeout);
2658 soup_session_cancel_message (session, sub->poll_msg);
2665 unsubscribed (SoupMessage *msg, gpointer user_data)
2671 unsubscribe_internal (E2kContext *ctx, const char *uri, GList *sub_list)
2674 E2kSubscription *sub;
2676 GString *subscription_ids = NULL;
2678 for (l = sub_list; l; l = l->next) {
2681 if (!subscription_ids)
2682 subscription_ids = g_string_new (sub->id);
2684 g_string_append_printf (subscription_ids,
2687 g_hash_table_remove (ctx->priv->subscriptions_by_id, sub->id);
2689 free_subscription (sub);
2692 if (subscription_ids) {
2693 msg = e2k_soup_message_new (ctx, uri, "UNSUBSCRIBE");
2694 soup_message_add_header (msg->request_headers,
2696 subscription_ids->str);
2697 e2k_context_queue_message (ctx, msg, unsubscribed, NULL);
2698 g_string_free (subscription_ids, TRUE);
2703 * e2k_context_unsubscribe:
2705 * @uri: the URI to unsubscribe from
2707 * Unsubscribes to all notifications on @ctx for @uri.
2710 e2k_context_unsubscribe (E2kContext *ctx, const char *uri)
2714 g_return_if_fail (E2K_IS_CONTEXT (ctx));
2716 sub_list = g_hash_table_lookup (ctx->priv->subscriptions_by_uri, uri);
2717 g_hash_table_remove (ctx->priv->subscriptions_by_uri, uri);
2718 unsubscribe_internal (ctx, uri, sub_list);
2719 g_list_free (sub_list);