Initial submission of GUPnP to Tizen IVI
[profile/ivi/GUPnP.git] / libgupnp / gupnp-service-proxy.c
1 /*
2  * Copyright (C) 2006, 2007, 2008 OpenedHand Ltd.
3  *
4  * Author: Jorn Baayen <jorn@openedhand.com>
5  *
6  * This library is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Library General Public
8  * License as published by the Free Software Foundation; either
9  * version 2 of the License, or (at your option) any later version.
10  *
11  * This library is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Library General Public License for more details.
15  *
16  * You should have received a copy of the GNU Library General Public
17  * License along with this library; if not, write to the
18  * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19  * Boston, MA 02110-1301, USA.
20  */
21
22 /**
23  * SECTION:gupnp-service-proxy
24  * @short_description: Proxy class for remote services.
25  *
26  * #GUPnPServiceProxy sends commands to a remote UPnP service and handles
27  * incoming event notifications. #GUPnPServiceProxy implements the
28  * #GUPnPServiceInfo interface.
29  */
30
31 #include <libsoup/soup.h>
32 #include <gobject/gvaluecollector.h>
33 #include <string.h>
34 #include <locale.h>
35
36 #include "gupnp-service-proxy.h"
37 #include "gupnp-context-private.h"
38 #include "gupnp-error.h"
39 #include "gupnp-error-private.h"
40 #include "gupnp-types.h"
41 #include "xml-util.h"
42 #include "gena-protocol.h"
43 #include "http-headers.h"
44 #include "gvalue-util.h"
45
46 G_DEFINE_TYPE (GUPnPServiceProxy,
47                gupnp_service_proxy,
48                GUPNP_TYPE_SERVICE_INFO);
49
50 struct _GUPnPServiceProxyPrivate {
51         gboolean subscribed;
52
53         GList *pending_actions;
54
55         char *path; /* Path to this proxy */
56
57         char *sid; /* Subscription ID */
58         GSource *subscription_timeout_src;
59
60         int seq; /* Event sequence number */
61
62         GHashTable *notify_hash;
63
64         GList *pending_messages; /* Pending SoupMessages from this proxy */
65
66         GList *pending_notifies; /* Pending notifications to be sent (xmlDoc) */
67         GSource *notify_idle_src; /* Idle handler src of notification emiter */
68 };
69
70 enum {
71         PROP_0,
72         PROP_SUBSCRIBED
73 };
74
75 enum {
76         SUBSCRIPTION_LOST,
77         LAST_SIGNAL
78 };
79
80 static guint signals[LAST_SIGNAL];
81
82 struct _GUPnPServiceProxyAction {
83         GUPnPServiceProxy *proxy;
84
85         SoupMessage *msg;
86         GString *msg_str;
87
88         GUPnPServiceProxyActionCallback callback;
89         gpointer user_data;
90
91         GError *error;    /* If non-NULL, description of error that
92                              occurred when preparing message */
93
94         va_list var_args; /* The va_list after begin_action_valist has
95                              gone through it. Used by send_action_valist(). */
96 };
97
98 typedef struct {
99         GType type;
100
101         GList *callbacks;
102 } NotifyData;
103
104 typedef struct {
105         GUPnPServiceProxyNotifyCallback callback;
106         gpointer user_data;
107 } CallbackData;
108
109 typedef struct {
110         char *sid;
111         int seq;
112
113         xmlDoc *doc;
114 } EmitNotifyData;
115
116 static void
117 subscribe_got_response (SoupSession       *session,
118                         SoupMessage       *msg,
119                         GUPnPServiceProxy *proxy);
120 static void
121 subscribe (GUPnPServiceProxy *proxy);
122 static void
123 unsubscribe (GUPnPServiceProxy *proxy);
124
125 static void
126 gupnp_service_proxy_action_free (GUPnPServiceProxyAction *action)
127 {
128         action->proxy->priv->pending_actions =
129                 g_list_remove (action->proxy->priv->pending_actions, action);
130
131         if (action->msg != NULL)
132                 g_object_unref (action->msg);
133
134         g_slice_free (GUPnPServiceProxyAction, action);
135 }
136
137 static void
138 notify_data_free (NotifyData *data)
139 {
140         g_list_free (data->callbacks);
141
142         g_slice_free (NotifyData, data);
143 }
144
145 /* Steals doc reference */
146 static EmitNotifyData *
147 emit_notify_data_new (const char *sid,
148                       int         seq,
149                       xmlDoc     *doc)
150 {
151         EmitNotifyData *data;
152
153         data = g_slice_new (EmitNotifyData);
154
155         data->sid = g_strdup (sid);
156         data->seq = seq;
157         data->doc = doc;
158
159         return data;
160 }
161
162 static void
163 emit_notify_data_free (EmitNotifyData *data)
164 {
165         g_free (data->sid);
166         xmlFreeDoc (data->doc);
167
168         g_slice_free (EmitNotifyData, data);
169 }
170
171 static void
172 gupnp_service_proxy_init (GUPnPServiceProxy *proxy)
173 {
174         static int proxy_counter = 0;
175
176         proxy->priv = G_TYPE_INSTANCE_GET_PRIVATE (proxy,
177                                                    GUPNP_TYPE_SERVICE_PROXY,
178                                                    GUPnPServiceProxyPrivate);
179
180         /* Generate unique path */
181         proxy->priv->path = g_strdup_printf ("/ServiceProxy%d", proxy_counter);
182         proxy_counter++;
183
184         /* Set up notify hash */
185         proxy->priv->notify_hash =
186                 g_hash_table_new_full (g_str_hash,
187                                        g_str_equal,
188                                        g_free,
189                                        (GDestroyNotify) notify_data_free);
190 }
191
192 static void
193 gupnp_service_proxy_set_property (GObject      *object,
194                                   guint         property_id,
195                                   const GValue *value,
196                                   GParamSpec   *pspec)
197 {
198         GUPnPServiceProxy *proxy;
199
200         proxy = GUPNP_SERVICE_PROXY (object);
201
202         switch (property_id) {
203         case PROP_SUBSCRIBED:
204                 gupnp_service_proxy_set_subscribed
205                                 (proxy, g_value_get_boolean (value));
206                 break;
207         default:
208                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
209                 break;
210         }
211 }
212
213 static void
214 gupnp_service_proxy_get_property (GObject    *object,
215                                   guint       property_id,
216                                   GValue     *value,
217                                   GParamSpec *pspec)
218 {
219         GUPnPServiceProxy *proxy;
220
221         proxy = GUPNP_SERVICE_PROXY (object);
222
223         switch (property_id) {
224         case PROP_SUBSCRIBED:
225                 g_value_set_boolean (value,
226                                      proxy->priv->subscribed);
227                 break;
228         default:
229                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
230                 break;
231         }
232 }
233
234 static void
235 gupnp_service_proxy_dispose (GObject *object)
236 {
237         GUPnPServiceProxy *proxy;
238         GObjectClass *object_class;
239         GUPnPContext *context;
240         SoupSession *session;
241
242         proxy = GUPNP_SERVICE_PROXY (object);
243
244         /* Unsubscribe */
245         if (proxy->priv->subscribed) {
246                 unsubscribe (proxy);
247
248                 proxy->priv->subscribed = FALSE;
249         }
250
251         /* Cancel pending actions */
252         while (proxy->priv->pending_actions) {
253                 GUPnPServiceProxyAction *action;
254
255                 action = proxy->priv->pending_actions->data;
256
257                 gupnp_service_proxy_cancel_action (proxy, action);
258         }
259
260         /* Cancel pending messages */
261         context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy));
262         if (context)
263                 session = gupnp_context_get_session (context);
264         else
265                 session = NULL; /* Not the first time dispose is called. */
266
267         while (proxy->priv->pending_messages) {
268                 SoupMessage *msg;
269
270                 msg = proxy->priv->pending_messages->data;
271
272                 soup_session_cancel_message (session,
273                                              msg,
274                                              SOUP_STATUS_CANCELLED);
275
276                 proxy->priv->pending_messages =
277                         g_list_delete_link (proxy->priv->pending_messages,
278                                             proxy->priv->pending_messages);
279         }
280
281         /* Cancel pending notifications */
282         if (proxy->priv->notify_idle_src) {
283                 g_source_destroy (proxy->priv->notify_idle_src);
284                 proxy->priv->notify_idle_src = NULL;
285         }
286         
287         while (proxy->priv->pending_notifies) {
288                 emit_notify_data_free (proxy->priv->pending_notifies->data);
289                 proxy->priv->pending_notifies =
290                         g_list_delete_link (proxy->priv->pending_notifies,
291                                             proxy->priv->pending_notifies);
292         }
293         
294         /* Call super */
295         object_class = G_OBJECT_CLASS (gupnp_service_proxy_parent_class);
296         object_class->dispose (object);
297 }
298
299 static void
300 gupnp_service_proxy_finalize (GObject *object)
301 {
302         GUPnPServiceProxy *proxy;
303         GObjectClass *object_class;
304
305         proxy = GUPNP_SERVICE_PROXY (object);
306
307         g_free (proxy->priv->path);
308
309         g_hash_table_destroy (proxy->priv->notify_hash);
310
311         /* Call super */
312         object_class = G_OBJECT_CLASS (gupnp_service_proxy_parent_class);
313         object_class->finalize (object);
314 }
315
316 static void
317 gupnp_service_proxy_class_init (GUPnPServiceProxyClass *klass)
318 {
319         GObjectClass *object_class;
320
321         object_class = G_OBJECT_CLASS (klass);
322
323         object_class->set_property = gupnp_service_proxy_set_property;
324         object_class->get_property = gupnp_service_proxy_get_property;
325         object_class->dispose      = gupnp_service_proxy_dispose;
326         object_class->finalize     = gupnp_service_proxy_finalize;
327
328         g_type_class_add_private (klass, sizeof (GUPnPServiceProxyPrivate));
329
330         /**
331          * GUPnPServiceProxy:subscribed:
332          *
333          * Whether we are subscribed to this service.
334          **/
335         g_object_class_install_property
336                 (object_class,
337                  PROP_SUBSCRIBED,
338                  g_param_spec_boolean ("subscribed",
339                                        "Subscribed",
340                                        "Whether we are subscribed to this "
341                                        "service",
342                                        FALSE,
343                                        G_PARAM_READWRITE |
344                                        G_PARAM_STATIC_NAME |
345                                        G_PARAM_STATIC_NICK |
346                                        G_PARAM_STATIC_BLURB));
347
348         /**
349          * GUPnPServiceProxy::subscription-lost:
350          * @proxy: The #GUPnPServiceProxy that received the signal
351          * @error: A pointer to a #GError describing why the subscription has
352          * been lost
353          *
354          * Emitted whenever the subscription to this service has been lost due
355          * to an error condition.
356          **/
357         signals[SUBSCRIPTION_LOST] =
358                 g_signal_new ("subscription-lost",
359                               GUPNP_TYPE_SERVICE_PROXY,
360                               G_SIGNAL_RUN_LAST,
361                               G_STRUCT_OFFSET (GUPnPServiceProxyClass,
362                                                subscription_lost),
363                               NULL,
364                               NULL,
365                               g_cclosure_marshal_VOID__POINTER,
366                               G_TYPE_NONE,
367                               1,
368                               G_TYPE_POINTER);
369 }
370
371 /**
372  * gupnp_service_proxy_send_action:
373  * @proxy: A #GUPnPServiceProxy
374  * @action: An action
375  * @error: The location where to store any error, or %NULL
376  * @Varargs: tuples of in parameter name, in parameter type, and in parameter
377  * value, followed by %NULL, and then tuples of out parameter name,
378  * out parameter type, and out parameter value location, terminated with %NULL
379  *
380  * Sends action @action with parameters @Varargs to the service exposed by
381  * @proxy synchronously. If an error occurred, @error will be set. In case of
382  * a UPnPError the error code will be the same in @error.
383  *
384  * Return value: %TRUE if sending the action was succesful.
385  **/
386 gboolean
387 gupnp_service_proxy_send_action (GUPnPServiceProxy *proxy,
388                                  const char        *action,
389                                  GError           **error,
390                                  ...)
391 {
392         va_list var_args;
393         gboolean ret;
394
395         va_start (var_args, error);
396         ret = gupnp_service_proxy_send_action_valist (proxy,
397                                                       action,
398                                                       error,
399                                                       var_args);
400         va_end (var_args);
401
402         return ret;
403 }
404
405 static void
406 stop_main_loop (GUPnPServiceProxy       *proxy,
407                 GUPnPServiceProxyAction *action,
408                 gpointer                 user_data)
409 {
410         g_main_loop_quit ((GMainLoop *) user_data);
411 }
412
413 /**
414  * gupnp_service_proxy_send_action_valist:
415  * @proxy: A #GUPnPServiceProxy
416  * @action: An action
417  * @error: The location where to store any error, or %NULL
418  * @var_args: va_list of tuples of in parameter name, in parameter type, and in
419  * parameter value, followed by %NULL, and then tuples of out parameter name,
420  * out parameter type, and out parameter value location
421  *
422  * See gupnp_service_proxy_send_action(); this version takes a va_list for
423  * use by language bindings.
424  *
425  * Return value: %TRUE if sending the action was succesful.
426  **/
427 gboolean
428 gupnp_service_proxy_send_action_valist (GUPnPServiceProxy *proxy,
429                                         const char        *action,
430                                         GError           **error,
431                                         va_list            var_args)
432 {
433         GMainLoop *main_loop;
434         GUPnPServiceProxyAction *handle;
435
436         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
437         g_return_val_if_fail (action, FALSE);
438
439         main_loop = g_main_loop_new (g_main_context_get_thread_default (),
440                                      TRUE);
441
442         handle = gupnp_service_proxy_begin_action_valist (proxy,
443                                                           action,
444                                                           stop_main_loop,
445                                                           main_loop,
446                                                           var_args);
447
448         /* Loop till we get a reply (or time out) */
449         if (g_main_loop_is_running (main_loop))
450                 g_main_loop_run (main_loop);
451
452         g_main_loop_unref (main_loop);
453
454         if (!gupnp_service_proxy_end_action_valist (proxy,
455                                                     handle,
456                                                     error,
457                                                     handle->var_args))
458                 return FALSE;
459
460         return TRUE;
461 }
462
463 /**
464  * gupnp_service_proxy_send_action_hash:
465  * @proxy: A #GUPnPServiceProxy
466  * @action: An action
467  * @error: The location where to store any error, or %NULL
468  * @in_hash: (element-type utf8 GValue) (transfer none): A #GHashTable of in
469  * parameter name and #GValue pairs
470  * @out_hash: (inout) (element-type utf8 GValue) (transfer full): A #GHashTable
471  * of out parameter name and initialized #GValue pairs
472  *
473  * See gupnp_service_proxy_send_action(); this version takes a pair of
474  * #GHashTable<!-- -->s for runtime determined parameter lists.
475  *
476  * Return value: %TRUE if sending the action was succesful.
477  **/
478 gboolean
479 gupnp_service_proxy_send_action_hash (GUPnPServiceProxy *proxy,
480                                       const char        *action,
481                                       GError           **error,
482                                       GHashTable        *in_hash,
483                                       GHashTable        *out_hash)
484 {
485         GMainLoop *main_loop;
486         GUPnPServiceProxyAction *handle;
487
488         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
489         g_return_val_if_fail (action, FALSE);
490
491         main_loop = g_main_loop_new (g_main_context_get_thread_default (),
492                                      TRUE);
493
494         handle = gupnp_service_proxy_begin_action_hash (proxy,
495                                                         action,
496                                                         stop_main_loop,
497                                                         main_loop,
498                                                         in_hash);
499         if (!handle) {
500                 g_main_loop_unref (main_loop);
501
502                 return FALSE;
503         }
504
505         /* Loop till we get a reply (or time out) */
506         if (g_main_loop_is_running (main_loop))
507                 g_main_loop_run (main_loop);
508
509         g_main_loop_unref (main_loop);
510
511         if (!gupnp_service_proxy_end_action_hash (proxy,
512                                                   handle,
513                                                   error,
514                                                   out_hash))
515                 return FALSE;
516
517         return TRUE;
518 }
519
520 /**
521  * gupnp_service_proxy_send_action_list:
522  * @proxy: (transfer none) : A #GUPnPServiceProxy
523  * @action: An action
524  * @error: The location where to store any error, or %NULL
525  * @in_names: (element-type utf8) (transfer none): #GList of 'in' parameter
526  * names (as strings)
527  * @in_values: (element-type GValue) (transfer none): #GList of values (as
528  * #GValue) that line up with @in_names
529  * @out_names: (element-type utf8) (transfer none): #GList of 'out' parameter
530  * names (as strings)
531  * @out_types: (element-type GType) (transfer none): #GList of types (as #GType)
532  * that line up with @out_names
533  * @out_values: (element-type GValue) (transfer full) (out): #GList of values
534  * (as #GValue) that line up with @out_names and @out_types.
535  *
536  * The synchronous variant of #gupnp_service_proxy_begin_action_list and
537  * #gupnp_service_proxy_end_action_list.
538  *
539  * Return value: %TRUE if sending the action was succesful.
540  **/
541 gboolean
542 gupnp_service_proxy_send_action_list (GUPnPServiceProxy *proxy,
543                                       const char        *action,
544                                       GError           **error,
545                                       GList             *in_names,
546                                       GList             *in_values,
547                                       GList             *out_names,
548                                       GList             *out_types,
549                                       GList            **out_values)
550 {
551         GMainLoop *main_loop;
552         GUPnPServiceProxyAction *handle;
553
554         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
555         g_return_val_if_fail (action, FALSE);
556
557         main_loop = g_main_loop_new (g_main_context_get_thread_default (),
558                                      TRUE);
559
560         handle = gupnp_service_proxy_begin_action_list (proxy,
561                                                         action,
562                                                         in_names,
563                                                         in_values,
564                                                         stop_main_loop,
565                                                         main_loop);
566         if (!handle) {
567                 g_main_loop_unref (main_loop);
568
569                 return FALSE;
570         }
571
572         /* Loop till we get a reply (or time out) */
573         if (g_main_loop_is_running (main_loop))
574                 g_main_loop_run (main_loop);
575
576         g_main_loop_unref (main_loop);
577
578         if (!gupnp_service_proxy_end_action_list (proxy,
579                                                   handle,
580                                                   error,
581                                                   out_names,
582                                                   out_types,
583                                                   out_values))
584                 return FALSE;
585
586         return TRUE;
587 }
588
589 /**
590  * gupnp_service_proxy_begin_action:
591  * @proxy: A #GUPnPServiceProxy
592  * @action: An action
593  * @callback: (scope async): The callback to call when sending the action has succeeded
594  * or failed
595  * @user_data: User data for @callback
596  * @Varargs: tuples of in parameter name, in parameter type, and in parameter
597  * value, terminated with %NULL
598  *
599  * Sends action @action with parameters @Varargs to the service exposed by
600  * @proxy asynchronously, calling @callback on completion. From @callback, call
601  * gupnp_service_proxy_end_action() to check for errors, to retrieve return
602  * values, and to free the #GUPnPServiceProxyAction.
603  *
604  * Returns: (transfer none): A #GUPnPServiceProxyAction handle. This will be freed when
605  * gupnp_service_proxy_cancel_action() or
606  * gupnp_service_proxy_end_action_valist().
607  **/
608 GUPnPServiceProxyAction *
609 gupnp_service_proxy_begin_action (GUPnPServiceProxy              *proxy,
610                                   const char                     *action,
611                                   GUPnPServiceProxyActionCallback callback,
612                                   gpointer                        user_data,
613                                   ...)
614 {
615         va_list var_args;
616         GUPnPServiceProxyAction *ret;
617
618         va_start (var_args, user_data);
619         ret = gupnp_service_proxy_begin_action_valist (proxy,
620                                                        action,
621                                                        callback,
622                                                        user_data,
623                                                        var_args);
624         va_end (var_args);
625
626         return ret;
627 }
628
629 /* Begins a basic action message */
630 static GUPnPServiceProxyAction *
631 begin_action_msg (GUPnPServiceProxy              *proxy,
632                   const char                     *action,
633                   GUPnPServiceProxyActionCallback callback,
634                   gpointer                        user_data)
635 {
636         GUPnPServiceProxyAction *ret;
637         char *control_url, *full_action;
638         const char *service_type;
639
640         /* Create action structure */
641         ret = g_slice_new (GUPnPServiceProxyAction);
642
643         ret->proxy = proxy;
644
645         ret->callback  = callback;
646         ret->user_data = user_data;
647
648         ret->msg = NULL;
649
650         ret->error = NULL;
651
652         proxy->priv->pending_actions =
653                 g_list_prepend (proxy->priv->pending_actions, ret);
654
655         /* Make sure we have a service type */
656         service_type = gupnp_service_info_get_service_type
657                                         (GUPNP_SERVICE_INFO (proxy));
658         if (service_type == NULL) {
659                 ret->error = g_error_new (GUPNP_SERVER_ERROR,
660                                           GUPNP_SERVER_ERROR_OTHER,
661                                           "No service type defined");
662
663                 return ret;
664         }
665
666         /* Create message */
667         control_url = gupnp_service_info_get_control_url
668                                         (GUPNP_SERVICE_INFO (proxy));
669
670         if (control_url != NULL) {
671                 ret->msg = soup_message_new (SOUP_METHOD_POST, control_url);
672
673                 g_free (control_url);
674         }
675
676         if (ret->msg == NULL) {
677                 ret->error = g_error_new (GUPNP_SERVER_ERROR,
678                                           GUPNP_SERVER_ERROR_INVALID_URL,
679                                           "No valid control URL defined");
680
681                 return ret;
682         }
683
684         /* Specify action */
685         full_action = g_strdup_printf ("\"%s#%s\"", service_type, action);
686         soup_message_headers_append (ret->msg->request_headers,
687                                      "SOAPAction",
688                                      full_action);
689         g_free (full_action);
690
691         /* Specify language */
692         http_request_set_accept_language (ret->msg);
693
694         /* Accept gzip encoding */
695         soup_message_headers_append (ret->msg->request_headers,
696                                      "Accept-Encoding", "gzip");
697
698         /* Set up envelope */
699         ret->msg_str = xml_util_new_string ();
700
701         g_string_append (ret->msg_str,
702                          "<?xml version=\"1.0\"?>"
703                          "<s:Envelope xmlns:s="
704                                 "\"http://schemas.xmlsoap.org/soap/envelope/\" "
705                           "s:encodingStyle="
706                                 "\"http://schemas.xmlsoap.org/soap/encoding/\">"
707                          "<s:Body>");
708
709         g_string_append (ret->msg_str, "<u:");
710         g_string_append (ret->msg_str, action);
711         g_string_append (ret->msg_str, " xmlns:u=\"");
712         g_string_append (ret->msg_str, service_type);
713         g_string_append (ret->msg_str, "\">");
714
715         return ret;
716 }
717
718 /* Received response to action message */
719 static void
720 action_got_response (SoupSession             *session,
721                      SoupMessage             *msg,
722                      GUPnPServiceProxyAction *action)
723 {
724         const char *full_action;
725
726         switch (msg->status_code) {
727         case SOUP_STATUS_CANCELLED:
728                 /* Silently return */
729                 break;
730
731         case SOUP_STATUS_METHOD_NOT_ALLOWED:
732                 /* Retry with M-POST */
733                 msg->method = "M-POST";
734
735                 soup_message_headers_append
736                         (msg->request_headers,
737                          "Man",
738                          "\"http://schemas.xmlsoap.org/soap/envelope/\"; ns=s");
739
740                 /* Rename "SOAPAction" to "s-SOAPAction" */
741                 full_action = soup_message_headers_get_one
742                         (msg->request_headers,
743                          "SOAPAction");
744                 soup_message_headers_append (msg->request_headers,
745                                              "s-SOAPAction",
746                                              full_action);
747                 soup_message_headers_remove (msg->request_headers,
748                                             "SOAPAction");
749
750                 /* And re-queue */
751                 soup_session_requeue_message (session, msg);
752
753                 break;
754
755         default:
756                 /* Success: Call callback */
757                 action->callback (action->proxy, action, action->user_data);
758
759                 break;
760         }
761 }
762
763 /* Finishes an action message and sends it off */
764 static void
765 finish_action_msg (GUPnPServiceProxyAction *action,
766                    const char              *action_name)
767 {
768         GUPnPContext *context;
769         SoupSession *session;
770
771         /* Finish message */
772         g_string_append (action->msg_str, "</u:");
773         g_string_append (action->msg_str, action_name);
774         g_string_append_c (action->msg_str, '>');
775
776         g_string_append (action->msg_str,
777                          "</s:Body>"
778                          "</s:Envelope>");
779
780         soup_message_set_request (action->msg,
781                                   "text/xml; charset=\"utf-8\"",
782                                   SOUP_MEMORY_TAKE,
783                                   action->msg_str->str,
784                                   action->msg_str->len);
785
786         g_string_free (action->msg_str, FALSE);
787
788         /* We need to keep our own reference to the message as well,
789          * in order for send_action() to work. */
790         g_object_ref (action->msg);
791
792         /* Send the message */
793         context = gupnp_service_info_get_context
794                                 (GUPNP_SERVICE_INFO (action->proxy));
795         session = gupnp_context_get_session (context);
796
797         soup_session_queue_message (session,
798                                     action->msg,
799                                     (SoupSessionCallback) action_got_response,
800                                     action);
801 }
802
803 /* Writes a parameter name and GValue pair to @msg */
804 static void
805 write_in_parameter (const char *arg_name,
806                     GValue     *value,
807                     GString    *msg_str)
808 {
809         /* Write parameter pair */
810         xml_util_start_element (msg_str, arg_name);
811         gvalue_util_value_append_to_xml_string (value, msg_str);
812         xml_util_end_element (msg_str, arg_name);
813 }
814
815 /**
816  * gupnp_service_proxy_begin_action_valist:
817  * @proxy: A #GUPnPServiceProxy
818  * @action: An action
819  * @callback: (scope async) : The callback to call when sending the action has succeeded
820  * or failed
821  * @user_data: User data for @callback
822  * @var_args: A va_list of tuples of in parameter name, in parameter type, and
823  * in parameter value
824  *
825  * See gupnp_service_proxy_begin_action(); this version takes a va_list for
826  * use by language bindings.
827  *
828  * Returns: (transfer none): A #GUPnPServiceProxyAction handle. This will
829  * be freed when calling gupnp_service_proxy_cancel_action() or
830  * gupnp_service_proxy_end_action_valist().
831  **/
832 GUPnPServiceProxyAction *
833 gupnp_service_proxy_begin_action_valist
834                                    (GUPnPServiceProxy              *proxy,
835                                     const char                     *action,
836                                     GUPnPServiceProxyActionCallback callback,
837                                     gpointer                        user_data,
838                                     va_list                         var_args)
839 {
840         const char *arg_name;
841         GUPnPServiceProxyAction *ret;
842
843         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), NULL);
844         g_return_val_if_fail (action, NULL);
845         g_return_val_if_fail (callback, NULL);
846
847         /* Create message */
848         ret = begin_action_msg (proxy, action, callback, user_data);
849
850         if (ret->error) {
851                 callback (proxy, ret, user_data);
852
853                 return ret;
854         }
855
856         /* Arguments */
857         arg_name = va_arg (var_args, const char *);
858         while (arg_name) {
859                 GType arg_type;
860                 GValue value = { 0, };
861                 char *collect_error = NULL;
862
863                 arg_type = va_arg (var_args, GType);
864                 g_value_init (&value, arg_type);
865
866                 G_VALUE_COLLECT (&value, var_args, G_VALUE_NOCOPY_CONTENTS,
867                                  &collect_error);
868                 if (!collect_error) {
869                         write_in_parameter (arg_name, &value, ret->msg_str);
870
871                         g_value_unset (&value);
872
873                 } else {
874                         g_warning ("Error collecting value: %s\n",
875                                    collect_error);
876
877                         g_free (collect_error);
878
879                         /* we purposely leak the value here, it might not be
880                          * in a sane state if an error condition occoured
881                          */
882                 }
883
884                 arg_name = va_arg (var_args, const char *);
885         }
886
887         /* Finish and send off */
888         finish_action_msg (ret, action);
889
890         /* Save the current position in the va_list for send_action_valist() */
891         G_VA_COPY (ret->var_args, var_args);
892
893         return ret;
894 }
895
896 /**
897  * gupnp_service_proxy_begin_action_list:
898  * @proxy: (transfer none): A #GUPnPServiceProxy
899  * @action: An action
900  * @in_names: (element-type utf8) (transfer none): #GList of 'in' parameter
901  * names (as strings)
902  * @in_values: (element-type GValue) (transfer none): #GList of values (as
903  * #GValue) that line up with @in_names
904  * @callback: (scope async) : The callback to call when sending the action has succeeded or
905  * failed
906  * @user_data: User data for @callback
907  *
908  * A variant of #gupnp_service_proxy_begin_action that takes lists of
909  * in-parameter names, types and values.
910  *
911  * Return value: (transfer none): A #GUPnPServiceProxyAction handle. This will
912  * be freed when calling #gupnp_service_proxy_cancel_action or
913  * #gupnp_service_proxy_end_action_list.
914  **/
915 GUPnPServiceProxyAction *
916 gupnp_service_proxy_begin_action_list
917                                    (GUPnPServiceProxy               *proxy,
918                                     const char                      *action,
919                                     GList                           *in_names,
920                                     GList                           *in_values,
921                                     GUPnPServiceProxyActionCallback  callback,
922                                     gpointer                         user_data)
923 {
924         GUPnPServiceProxyAction *ret;
925         GList *names;
926         GList *values;
927
928         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), NULL);
929         g_return_val_if_fail (action, NULL);
930         g_return_val_if_fail (callback, NULL);
931         g_return_val_if_fail (g_list_length (in_names) ==
932                               g_list_length (in_values),
933                               NULL);
934
935         /* Create message */
936         ret = begin_action_msg (proxy, action, callback, user_data);
937
938         if (ret->error) {
939                 callback (proxy, ret, user_data);
940
941                 return ret;
942         }
943
944         /* Arguments */
945         values = in_values;
946
947         for (names = in_names; names; names=names->next) {
948                 GValue* val = values->data;
949
950                 write_in_parameter (names->data,
951                                     val,
952                                     ret->msg_str);
953
954                 values = values->next;
955         }
956
957         /* Finish and send off */
958         finish_action_msg (ret, action);
959
960         return ret;
961 }
962
963 /**
964  * gupnp_service_proxy_begin_action_hash:
965  * @proxy: A #GUPnPServiceProxy
966  * @action: An action
967  * @callback: (scope async): The callback to call when sending the action has succeeded
968  * or failed
969  * @user_data: User data for @callback
970  * @hash: A #GHashTable of in parameter name and #GValue pairs
971  *
972  * See gupnp_service_proxy_begin_action(); this version takes a #GHashTable
973  * for runtime generated parameter lists.
974  *
975  * Return value: (transfer none): A #GUPnPServiceProxyAction handle. This will
976  * be freed when calling gupnp_service_proxy_cancel_action() or
977  * gupnp_service_proxy_end_action_hash().
978  **/
979 GUPnPServiceProxyAction *
980 gupnp_service_proxy_begin_action_hash
981                                    (GUPnPServiceProxy              *proxy,
982                                     const char                     *action,
983                                     GUPnPServiceProxyActionCallback callback,
984                                     gpointer                        user_data,
985                                     GHashTable                     *hash)
986 {
987         GUPnPServiceProxyAction *ret;
988
989         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), NULL);
990         g_return_val_if_fail (action, NULL);
991         g_return_val_if_fail (callback, NULL);
992
993         /* Create message */
994         ret = begin_action_msg (proxy, action, callback, user_data);
995
996         if (ret->error) {
997                 callback (proxy, ret, user_data);
998
999                 return ret;
1000         }
1001
1002         /* Arguments */
1003         g_hash_table_foreach (hash, (GHFunc) write_in_parameter, ret->msg_str);
1004
1005         /* Finish and send off */
1006         finish_action_msg (ret, action);
1007
1008         return ret;
1009 }
1010
1011 /**
1012  * gupnp_service_proxy_end_action:
1013  * @proxy: A #GUPnPServiceProxy
1014  * @action: A #GUPnPServiceProxyAction handle
1015  * @error: The location where to store any error, or %NULL
1016  * @Varargs: tuples of out parameter name, out parameter type, and out parameter
1017  * value location, terminated with %NULL. The out parameter values should be
1018  * freed after use
1019  *
1020  * Retrieves the result of @action. The out parameters in @Varargs will be
1021  * filled in, and if an error occurred, @error will be set. In case of
1022  * a UPnPError the error code will be the same in @error.
1023  *
1024  * Return value: %TRUE on success.
1025  **/
1026 gboolean
1027 gupnp_service_proxy_end_action (GUPnPServiceProxy       *proxy,
1028                                 GUPnPServiceProxyAction *action,
1029                                 GError                 **error,
1030                                 ...)
1031 {
1032         va_list var_args;
1033         gboolean ret;
1034
1035         va_start (var_args, error);
1036         ret = gupnp_service_proxy_end_action_valist (proxy,
1037                                                      action,
1038                                                      error,
1039                                                      var_args);
1040         va_end (var_args);
1041
1042         return ret;
1043 }
1044
1045 /* Checks an action response for errors and returns the parsed
1046  * xmlDoc object. */
1047 static xmlDoc *
1048 check_action_response (GUPnPServiceProxy       *proxy,
1049                        GUPnPServiceProxyAction *action,
1050                        xmlNode                **params,
1051                        GError                 **error)
1052 {
1053         xmlDoc *response;
1054         int code;
1055
1056         /* Check for errors */
1057         switch (action->msg->status_code) {
1058         case SOUP_STATUS_OK:
1059         case SOUP_STATUS_INTERNAL_SERVER_ERROR:
1060                 break;
1061         default:
1062                 _gupnp_error_set_server_error (error, action->msg);
1063
1064                 return NULL;
1065         }
1066
1067         /* Parse response */
1068         response = xmlRecoverMemory (action->msg->response_body->data,
1069                                      action->msg->response_body->length);
1070
1071         if (!response) {
1072                 if (action->msg->status_code == SOUP_STATUS_OK) {
1073                         g_set_error (error,
1074                                      GUPNP_SERVER_ERROR,
1075                                      GUPNP_SERVER_ERROR_INVALID_RESPONSE,
1076                                      "Could not parse SOAP response");
1077                 } else {
1078                         g_set_error_literal
1079                                     (error,
1080                                      GUPNP_SERVER_ERROR,
1081                                      GUPNP_SERVER_ERROR_INTERNAL_SERVER_ERROR,
1082                                      action->msg->reason_phrase);
1083                 }
1084
1085                 return NULL;
1086         }
1087
1088         /* Get parameter list */
1089         *params = xml_util_get_element ((xmlNode *) response,
1090                                         "Envelope",
1091                                         NULL);
1092         if (*params != NULL)
1093                 *params = xml_util_real_node ((*params)->children);
1094
1095         if (*params != NULL) {
1096                 if (strcmp ((const char *) (*params)->name, "Header") == 0)
1097                         *params = xml_util_real_node ((*params)->next);
1098
1099                 if (*params != NULL)
1100                         if (strcmp ((const char *) (*params)->name, "Body") != 0)
1101                                 *params = NULL;
1102         }
1103
1104         if (*params != NULL)
1105                 *params = xml_util_real_node ((*params)->children);
1106
1107         if (*params == NULL) {
1108                 g_set_error (error,
1109                              GUPNP_SERVER_ERROR,
1110                              GUPNP_SERVER_ERROR_INVALID_RESPONSE,
1111                              "Invalid Envelope");
1112
1113                 xmlFreeDoc (response);
1114
1115                 return NULL;
1116         }
1117
1118         /* Check whether we have a Fault */
1119         if (action->msg->status_code == SOUP_STATUS_INTERNAL_SERVER_ERROR) {
1120                 xmlNode *param;
1121                 char *desc;
1122
1123                 param = xml_util_get_element (*params,
1124                                               "detail",
1125                                               "UPnPError",
1126                                               NULL);
1127
1128                 if (!param) {
1129                         g_set_error (error,
1130                                      GUPNP_SERVER_ERROR,
1131                                      GUPNP_SERVER_ERROR_INVALID_RESPONSE,
1132                                      "Invalid Fault");
1133
1134                         xmlFreeDoc (response);
1135
1136                         return NULL;
1137                 }
1138
1139                 /* Code */
1140                 code = xml_util_get_child_element_content_int
1141                                         (param, "errorCode");
1142                 if (code == -1) {
1143                         g_set_error (error,
1144                                      GUPNP_SERVER_ERROR,
1145                                      GUPNP_SERVER_ERROR_INVALID_RESPONSE,
1146                                      "Invalid Fault");
1147
1148                         xmlFreeDoc (response);
1149
1150                         return NULL;
1151                 }
1152
1153                 /* Description */
1154                 desc = xml_util_get_child_element_content_glib
1155                                         (param, "errorDescription");
1156                 if (desc == NULL)
1157                         desc = g_strdup (action->msg->reason_phrase);
1158
1159                 g_set_error_literal (error,
1160                                      GUPNP_CONTROL_ERROR,
1161                                      code,
1162                                      desc);
1163
1164                 g_free (desc);
1165
1166                 xmlFreeDoc (response);
1167
1168                 return NULL;
1169         }
1170
1171         return response;
1172 }
1173
1174 /* Reads a value into the parameter name and initialised GValue pair
1175  * from @response */
1176 static void
1177 read_out_parameter (const char *arg_name,
1178                     GValue     *value,
1179                     xmlNode    *params)
1180 {
1181         xmlNode *param;
1182
1183         /* Try to find a matching parameter in the response*/
1184         param = xml_util_get_element (params,
1185                                       arg_name,
1186                                       NULL);
1187         if (!param) {
1188                 g_warning ("Could not find variable \"%s\" in response",
1189                            arg_name);
1190
1191                 return;
1192         }
1193
1194         gvalue_util_set_value_from_xml_node (value, param);
1195 }
1196
1197 /**
1198  * gupnp_service_proxy_end_action_valist:
1199  * @proxy: A #GUPnPServiceProxy
1200  * @action: A #GUPnPServiceProxyAction handle
1201  * @error: The location where to store any error, or %NULL
1202  * @var_args: A va_list of tuples of out parameter name, out parameter type,
1203  * and out parameter value location. The out parameter values should be
1204  * freed after use
1205  *
1206  * See gupnp_service_proxy_end_action(); this version takes a va_list for
1207  * use by language bindings.
1208  *
1209  * Return value: %TRUE on success.
1210  **/
1211 gboolean
1212 gupnp_service_proxy_end_action_valist (GUPnPServiceProxy       *proxy,
1213                                        GUPnPServiceProxyAction *action,
1214                                        GError                 **error,
1215                                        va_list                  var_args)
1216 {
1217         xmlDoc *response;
1218         xmlNode *params;
1219         const char *arg_name;
1220
1221         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
1222         g_return_val_if_fail (action, FALSE);
1223         g_return_val_if_fail (proxy == action->proxy, FALSE);
1224
1225         /* Check for saved error from begin_action() */
1226         if (action->error) {
1227                 if (error)
1228                         *error = action->error;
1229                 else
1230                         g_error_free (action->error);
1231
1232                 gupnp_service_proxy_action_free (action);
1233
1234                 return FALSE;
1235         }
1236
1237         /* Check response for errors and do initial parsing */
1238         response = check_action_response (proxy, action, &params, error);
1239         if (response == NULL) {
1240                 gupnp_service_proxy_action_free (action);
1241
1242                 return FALSE;
1243         }
1244
1245         /* Arguments */
1246         arg_name = va_arg (var_args, const char *);
1247         while (arg_name) {
1248                 GType arg_type;
1249                 GValue value = { 0, };
1250                 char *copy_error = NULL;
1251
1252                 arg_type = va_arg (var_args, GType);
1253
1254                 g_value_init (&value, arg_type);
1255
1256                 read_out_parameter (arg_name, &value, params);
1257
1258                 G_VALUE_LCOPY (&value, var_args, 0, &copy_error);
1259
1260                 g_value_unset (&value);
1261
1262                 if (copy_error) {
1263                         g_warning ("Error copying value: %s", copy_error);
1264
1265                         g_free (copy_error);
1266                 }
1267
1268                 arg_name = va_arg (var_args, const char *);
1269         }
1270
1271         /* Cleanup */
1272         gupnp_service_proxy_action_free (action);
1273
1274         xmlFreeDoc (response);
1275
1276         return TRUE;
1277 }
1278
1279 /**
1280  * gupnp_service_proxy_end_action_list:
1281  * @proxy: A #GUPnPServiceProxy
1282  * @action: A #GUPnPServiceProxyAction handle
1283  * @error: The location where to store any error, or %NULL
1284  * @out_names: (element-type utf8) (transfer none): #GList of 'out' parameter
1285  * names (as strings)
1286  * @out_types: (element-type GType) (transfer none): #GList of types (as #GType)
1287  * that line up with @out_names
1288  * @out_values: (element-type GValue) (transfer full) (out): #GList of values
1289  * (as #GValue) that line up with @out_names and @out_types.
1290  *
1291  * A variant of #gupnp_service_proxy_end_action that takes lists of
1292  * out-parameter names, types and place-holders for values. The returned list
1293  * in @out_values must be freed using #g_list_free and each element in it using
1294  * #g_value_unset and #g_slice_free.
1295  *
1296  * Return value : %TRUE on success.
1297  **/
1298 gboolean
1299 gupnp_service_proxy_end_action_list (GUPnPServiceProxy       *proxy,
1300                                      GUPnPServiceProxyAction *action,
1301                                      GError                 **error,
1302                                      GList                   *out_names,
1303                                      GList                   *out_types,
1304                                      GList                  **out_values)
1305 {
1306         xmlDoc *response;
1307         xmlNode *params;
1308         GList *names;
1309         GList *types;
1310         GList *out_values_list;
1311
1312         out_values_list = NULL;
1313
1314         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
1315         g_return_val_if_fail (action, FALSE);
1316         g_return_val_if_fail (proxy == action->proxy, FALSE);
1317
1318         /* Check for saved error from begin_action() */
1319         if (action->error) {
1320                 if (error)
1321                         *error = action->error;
1322                 else
1323                         g_error_free (action->error);
1324
1325                 gupnp_service_proxy_action_free (action);
1326
1327                 return FALSE;
1328         }
1329
1330         /* Check response for errors and do initial parsing */
1331         response = check_action_response (proxy, action, &params, error);
1332         if (response == NULL) {
1333                 gupnp_service_proxy_action_free (action);
1334
1335                 return FALSE;
1336         }
1337
1338         /* Read arguments */
1339         types = out_types;
1340         for (names = out_names; names; names=names->next) {
1341                 GValue *val;
1342
1343                 val = g_slice_new0 (GValue);
1344                 g_value_init (val, (GType) types->data);
1345
1346                 read_out_parameter (names->data, val, params);
1347
1348                 out_values_list = g_list_append (out_values_list, val);
1349
1350                 types = types->next;
1351         }
1352
1353         *out_values = out_values_list;
1354
1355         /* Cleanup */
1356         gupnp_service_proxy_action_free (action);
1357
1358         xmlFreeDoc (response);
1359
1360         return TRUE;
1361 }
1362
1363 /**
1364  * gupnp_service_proxy_end_action_hash:
1365  * @proxy: A #GUPnPServiceProxy
1366  * @action: A #GUPnPServiceProxyAction handle
1367  * @error: The location where to store any error, or %NULL
1368  * @hash: (element-type utf8 GValue) (inout) (transfer none): A #GHashTable of
1369  * out parameter name and initialised #GValue pairs
1370  *
1371  * See gupnp_service_proxy_end_action(); this version takes a #GHashTable for
1372  * runtime generated parameter lists.
1373  *
1374  * Return value: %TRUE on success.
1375  **/
1376 gboolean
1377 gupnp_service_proxy_end_action_hash (GUPnPServiceProxy       *proxy,
1378                                      GUPnPServiceProxyAction *action,
1379                                      GError                 **error,
1380                                      GHashTable              *hash)
1381 {
1382         xmlDoc *response;
1383         xmlNode *params;
1384
1385         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
1386         g_return_val_if_fail (action, FALSE);
1387         g_return_val_if_fail (proxy == action->proxy, FALSE);
1388
1389         /* Check for saved error from begin_action() */
1390         if (action->error) {
1391                 if (error)
1392                         *error = action->error;
1393                 else
1394                         g_error_free (action->error);
1395
1396                 gupnp_service_proxy_action_free (action);
1397
1398                 return FALSE;
1399         }
1400
1401         /* Check response for errors and do initial parsing */
1402         response = check_action_response (proxy, action, &params, error);
1403         if (response == NULL) {
1404                 gupnp_service_proxy_action_free (action);
1405
1406                 return FALSE;
1407         }
1408
1409         /* Read arguments */
1410         g_hash_table_foreach (hash, (GHFunc) read_out_parameter, params);
1411
1412         /* Cleanup */
1413         gupnp_service_proxy_action_free (action);
1414
1415         xmlFreeDoc (response);
1416
1417         return TRUE;
1418 }
1419
1420 /**
1421  * gupnp_service_proxy_cancel_action:
1422  * @proxy: A #GUPnPServiceProxy
1423  * @action: A #GUPnPServiceProxyAction handle
1424  *
1425  * Cancels @action, freeing the @action handle.
1426  **/
1427 void
1428 gupnp_service_proxy_cancel_action (GUPnPServiceProxy       *proxy,
1429                                    GUPnPServiceProxyAction *action)
1430 {
1431         GUPnPContext *context;
1432         SoupSession *session;
1433
1434         g_return_if_fail (GUPNP_IS_SERVICE_PROXY (proxy));
1435         g_return_if_fail (action);
1436         g_return_if_fail (proxy == action->proxy);
1437
1438         if (action->msg != NULL) {
1439                 context = gupnp_service_info_get_context
1440                                         (GUPNP_SERVICE_INFO (proxy));
1441                 session = gupnp_context_get_session (context);
1442
1443                 soup_session_cancel_message (session,
1444                                              action->msg,
1445                                              SOUP_STATUS_CANCELLED);
1446         }
1447
1448         if (action->error != NULL)
1449                 g_error_free (action->error);
1450
1451         gupnp_service_proxy_action_free (action);
1452 }
1453
1454 /**
1455  * gupnp_service_proxy_add_notify:
1456  * @proxy: A #GUPnPServiceProxy
1457  * @variable: The variable to add notification for
1458  * @type: The type of the variable
1459  * @callback: (scope async): The callback to call when @variable changes
1460  * @user_data: User data for @callback
1461  *
1462  * Sets up @callback to be called whenever a change notification for
1463  * @variable is recieved.
1464  *
1465  * Return value: %TRUE on success.
1466  **/
1467 gboolean
1468 gupnp_service_proxy_add_notify (GUPnPServiceProxy              *proxy,
1469                                 const char                     *variable,
1470                                 GType                           type,
1471                                 GUPnPServiceProxyNotifyCallback callback,
1472                                 gpointer                        user_data)
1473 {
1474         NotifyData *data;
1475         CallbackData *callback_data;
1476
1477         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
1478         g_return_val_if_fail (variable, FALSE);
1479         g_return_val_if_fail (type, FALSE);
1480         g_return_val_if_fail (callback, FALSE);
1481
1482         /* See if we already have notifications set up for this variable */
1483         data = g_hash_table_lookup (proxy->priv->notify_hash, variable);
1484         if (data == NULL) {
1485                 /* No, create one */
1486                 data = g_slice_new (NotifyData);
1487
1488                 data->type       = type;
1489                 data->callbacks  = NULL;
1490
1491                 g_hash_table_insert (proxy->priv->notify_hash,
1492                                      g_strdup (variable),
1493                                      data);
1494         } else {
1495                 /* Yes, check that everything is sane .. */
1496                 if (data->type != type) {
1497                         g_warning ("A notification already exists for %s, but "
1498                                    "has type %s, not %s.",
1499                                    variable,
1500                                    g_type_name (data->type),
1501                                    g_type_name (type));
1502
1503                         return FALSE;
1504                 }
1505         }
1506
1507         /* Append callback */
1508         callback_data = g_slice_new (CallbackData);
1509
1510         callback_data->callback  = callback;
1511         callback_data->user_data = user_data;
1512
1513         data->callbacks = g_list_append (data->callbacks, callback_data);
1514
1515         return TRUE;
1516 }
1517
1518 /**
1519  * gupnp_service_proxy_remove_notify:
1520  * @proxy: A #GUPnPServiceProxy
1521  * @variable: The variable to add notification for
1522  * @callback: (scope call): The callback to call when @variable changes
1523  * @user_data: User data for @callback
1524  *
1525  * Cancels the variable change notification for @callback and @user_data.
1526  *
1527  * Return value: %TRUE on success.
1528  **/
1529 gboolean
1530 gupnp_service_proxy_remove_notify (GUPnPServiceProxy              *proxy,
1531                                    const char                     *variable,
1532                                    GUPnPServiceProxyNotifyCallback callback,
1533                                    gpointer                        user_data)
1534 {
1535         NotifyData *data;
1536         gboolean found;
1537         GList *l;
1538
1539         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
1540         g_return_val_if_fail (variable, FALSE);
1541         g_return_val_if_fail (callback, FALSE);
1542
1543         /* Look up NotifyData for variable */
1544         data = g_hash_table_lookup (proxy->priv->notify_hash, variable);
1545         if (data == NULL) {
1546                 g_warning ("No notifications found for variable %s",
1547                            variable);
1548
1549                 return FALSE;
1550         }
1551
1552         /* Look up our callback-user_data pair */
1553         found = FALSE;
1554
1555         for (l = data->callbacks; l; l = l->next) {
1556                 CallbackData *callback_data;
1557
1558                 callback_data = l->data;
1559
1560                 if (callback_data->callback  == callback &&
1561                     callback_data->user_data == user_data) {
1562                         /* Gotcha! */
1563                         g_slice_free (CallbackData, callback_data);
1564
1565                         data->callbacks =
1566                                 g_list_delete_link (data->callbacks, l);
1567                         if (data->callbacks == NULL) {
1568                                 /* No callbacks left: Remove from hash */
1569                                 g_hash_table_remove (proxy->priv->notify_hash,
1570                                                      data);
1571                         }
1572
1573                         found = TRUE;
1574
1575                         break;
1576                 }
1577         }
1578
1579         if (found == FALSE)
1580                 g_warning ("No such callback-user_data pair was found");
1581
1582         return found;
1583 }
1584
1585 static void
1586 emit_notification (GUPnPServiceProxy *proxy,
1587                    xmlNode           *var_node)
1588 {
1589         NotifyData *data;
1590         GValue value = {0, };
1591         GList *l;
1592
1593         data = g_hash_table_lookup (proxy->priv->notify_hash, var_node->name);
1594         if (data == NULL)
1595                 return;
1596
1597         /* Make a GValue of the desired type */
1598         g_value_init (&value, data->type);
1599
1600         if (!gvalue_util_set_value_from_xml_node (&value, var_node)) {
1601                 g_value_unset (&value);
1602
1603                 return;
1604         }
1605
1606         /* Call callbacks */
1607         for (l = data->callbacks; l; l = l->next) {
1608                 CallbackData *callback_data;
1609
1610                 callback_data = l->data;
1611
1612                 callback_data->callback (proxy,
1613                                          (const char *) var_node->name,
1614                                          &value,
1615                                          callback_data->user_data);
1616         }
1617
1618         /* Cleanup */
1619         g_value_unset (&value);
1620 }
1621
1622 static void
1623 emit_notifications_for_doc (GUPnPServiceProxy *proxy,
1624                             xmlDoc            *doc)
1625 {
1626         xmlNode *node;
1627
1628         node = xmlDocGetRootElement (doc);
1629
1630         /* Iterate over all provided properties */
1631         for (node = node->children; node; node = node->next) {
1632                 xmlNode *var_node;
1633
1634                 /* Although according to the UPnP specs, there should be only
1635                  * one variable node inside a 'property' node, we still need to
1636                  * entertain the possibility of multiple variables inside it to
1637                  * be compatible with implementations using older GUPnP.
1638                  */
1639                 for (var_node = node->children;
1640                      var_node;
1641                      var_node = var_node->next) {
1642                         if (strcmp ((char *) node->name, "property") == 0)
1643                                 emit_notification (proxy, var_node);
1644                 }
1645         }
1646 }
1647
1648 /* Emit pending notifications. See comment below on why we do this. */
1649 static gboolean
1650 emit_notifications (gpointer user_data)
1651 {
1652         GUPnPServiceProxy *proxy = user_data;
1653         GList *pending_notify;
1654         gboolean resubscribe = FALSE;
1655
1656         g_assert (user_data);
1657
1658         if (proxy->priv->sid == NULL)
1659                 /* No SID */
1660                 if (G_LIKELY (proxy->priv->subscribed))
1661                         /* subscription in progress, delay emision! */
1662                         return TRUE;
1663
1664         for (pending_notify = proxy->priv->pending_notifies;
1665              pending_notify != NULL;
1666              pending_notify = pending_notify->next) {
1667                 EmitNotifyData *emit_notify_data;
1668
1669                 emit_notify_data = pending_notify->data;
1670
1671                 if (emit_notify_data->seq > proxy->priv->seq) {
1672                         /* Oops, we missed a notify. Resubscribe .. */
1673                         resubscribe = TRUE;
1674
1675                         break;
1676                 }
1677
1678                 /* Increment our own event sequence number */
1679                 if (proxy->priv->seq < G_MAXINT32)
1680                         proxy->priv->seq++;
1681                 else
1682                         proxy->priv->seq = 1;
1683
1684                 if (G_LIKELY (proxy->priv->sid != NULL &&
1685                               strcmp (emit_notify_data->sid,
1686                                       proxy->priv->sid) == 0))
1687                         /* Our SID, entertain! */
1688                         emit_notifications_for_doc (proxy,
1689                                                     emit_notify_data->doc);
1690         }
1691
1692         /* Cleanup */
1693         while (proxy->priv->pending_notifies != NULL) {
1694                 emit_notify_data_free (proxy->priv->pending_notifies->data);
1695
1696                 proxy->priv->pending_notifies =
1697                         g_list_delete_link (proxy->priv->pending_notifies,
1698                                             proxy->priv->pending_notifies);
1699         }
1700
1701         proxy->priv->notify_idle_src = NULL;
1702
1703         if (resubscribe) {
1704                 unsubscribe (proxy);
1705                 subscribe (proxy);
1706         }
1707
1708         return FALSE;
1709 }
1710
1711 /*
1712  * HTTP server received a message. Handle, if this was a NOTIFY
1713  * message with our SID.
1714  */
1715 static void
1716 server_handler (SoupServer        *soup_server,
1717                 SoupMessage       *msg, 
1718                 const char        *server_path,
1719                 GHashTable        *query,
1720                 SoupClientContext *soup_client,
1721                 gpointer           user_data)
1722 {
1723         GUPnPServiceProxy *proxy;
1724         const char *hdr;
1725         int seq;
1726         xmlDoc *doc;
1727         xmlNode *node;
1728         EmitNotifyData *emit_notify_data;
1729
1730         proxy = GUPNP_SERVICE_PROXY (user_data);
1731
1732         if (strcmp (msg->method, GENA_METHOD_NOTIFY) != 0) {
1733                 /* We don't implement this method */
1734                 soup_message_set_status (msg, SOUP_STATUS_NOT_IMPLEMENTED);
1735
1736                 return;
1737         }
1738
1739         hdr = soup_message_headers_get_one (msg->request_headers, "NT");
1740         if (hdr == NULL || strcmp (hdr, "upnp:event") != 0) {
1741                 /* Proper NT header lacking */
1742                 soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
1743
1744                 return;
1745         }
1746
1747         hdr = soup_message_headers_get_one (msg->request_headers, "NTS");
1748         if (hdr == NULL || strcmp (hdr, "upnp:propchange") != 0) {
1749                 /* Proper NTS header lacking */
1750                 soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
1751
1752                 return;
1753         }
1754
1755         hdr = soup_message_headers_get_one (msg->request_headers, "SEQ");
1756         if (hdr == NULL) {
1757                 /* No SEQ header */
1758                 soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
1759
1760                 return;
1761         }
1762
1763         seq = atoi (hdr);
1764
1765         hdr = soup_message_headers_get_one (msg->request_headers, "SID");
1766         if (hdr == NULL) {
1767                 /* No SID */
1768                 soup_message_set_status (msg, SOUP_STATUS_PRECONDITION_FAILED);
1769
1770                 return;
1771         }
1772
1773         /* Parse the actual XML message content */
1774         doc = xmlRecoverMemory (msg->request_body->data,
1775                                 msg->request_body->length);
1776         if (doc == NULL) {
1777                 /* Failed */
1778                 g_warning ("Failed to parse NOTIFY message body");
1779
1780                 soup_message_set_status (msg,
1781                                          SOUP_STATUS_INTERNAL_SERVER_ERROR);
1782
1783                 return;
1784         }
1785
1786         /* Get root propertyset element */
1787         node = xmlDocGetRootElement (doc);
1788         if (node == NULL || strcmp ((char *) node->name, "propertyset")) {
1789                 /* Empty or unsupported */
1790                 xmlFreeDoc (doc);
1791
1792                 soup_message_set_status (msg, SOUP_STATUS_OK);
1793
1794                 return;
1795         }
1796
1797         /*
1798          * Some UPnP stacks (hello, myigd/1.0) block when sending a NOTIFY, so
1799          * call the callbacks in an idle handler so that if the client calls the
1800          * device in the notify callback the server can actually respond.
1801          */
1802         emit_notify_data = emit_notify_data_new (hdr, seq, doc);
1803
1804         proxy->priv->pending_notifies =
1805                 g_list_append (proxy->priv->pending_notifies, emit_notify_data);
1806         if (!proxy->priv->notify_idle_src) {
1807                 proxy->priv->notify_idle_src = g_idle_source_new();
1808                 g_source_set_callback (proxy->priv->notify_idle_src,
1809                                        emit_notifications,
1810                                        proxy, NULL);
1811                 g_source_attach (proxy->priv->notify_idle_src,
1812                                  g_main_context_get_thread_default ());
1813
1814                 g_source_unref (proxy->priv->notify_idle_src);
1815         }
1816         
1817         /* Everything went OK */
1818         soup_message_set_status (msg, SOUP_STATUS_OK);
1819 }
1820
1821 /*
1822  * Generates a timeout header for the subscription timeout specified
1823  * in our GUPnPContext.
1824  */
1825 static char *
1826 make_timeout_header (GUPnPContext *context)
1827 {
1828         guint timeout;
1829
1830         timeout = gupnp_context_get_subscription_timeout (context);
1831         if (timeout > 0)
1832                 return g_strdup_printf ("Second-%d", timeout);
1833         else
1834                 return g_strdup ("infinite");
1835 }
1836
1837 /*
1838  * Subscription expired.
1839  */
1840 static gboolean
1841 subscription_expire (gpointer user_data)
1842 {
1843         GUPnPServiceProxy *proxy;
1844         GUPnPContext *context;
1845         SoupMessage *msg;
1846         SoupSession *session;
1847         char *sub_url, *timeout;
1848
1849         proxy = GUPNP_SERVICE_PROXY (user_data);
1850
1851         /* Reset timeout ID */
1852         proxy->priv->subscription_timeout_src = NULL;
1853
1854         /* Send renewal message */
1855         context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy));
1856
1857         /* Create subscription message */
1858         sub_url = gupnp_service_info_get_event_subscription_url
1859                                                 (GUPNP_SERVICE_INFO (proxy));
1860
1861         msg = soup_message_new (GENA_METHOD_SUBSCRIBE, sub_url);
1862
1863         g_free (sub_url);
1864
1865         g_return_val_if_fail (msg != NULL, FALSE);
1866
1867         /* Add headers */
1868         soup_message_headers_append (msg->request_headers,
1869                                     "SID",
1870                                     proxy->priv->sid);
1871
1872         timeout = make_timeout_header (context);
1873         soup_message_headers_append (msg->request_headers,
1874                                      "Timeout",
1875                                      timeout);
1876         g_free (timeout);
1877
1878         /* And send it off */
1879         proxy->priv->pending_messages =
1880                 g_list_prepend (proxy->priv->pending_messages, msg);
1881
1882         session = gupnp_context_get_session (context);
1883
1884         soup_session_queue_message (session,
1885                                     msg,
1886                                     (SoupSessionCallback)
1887                                         subscribe_got_response,
1888                                     proxy);
1889
1890         return FALSE;
1891 }
1892
1893 /*
1894  * Received subscription response.
1895  */
1896 static void
1897 subscribe_got_response (SoupSession       *session,
1898                         SoupMessage       *msg,
1899                         GUPnPServiceProxy *proxy)
1900 {
1901         GError *error;
1902
1903         /* Cancelled? */
1904         if (msg->status_code == SOUP_STATUS_CANCELLED)
1905                 return;
1906
1907         /* Remove from pending messages list */
1908         proxy->priv->pending_messages =
1909                 g_list_remove (proxy->priv->pending_messages, msg);
1910
1911         /* Check whether the subscription is still wanted */
1912         if (!proxy->priv->subscribed)
1913                 return;
1914
1915         /* Reset SID */
1916         g_free (proxy->priv->sid);
1917         proxy->priv->sid = NULL;
1918
1919         /* Check message status */
1920         if (SOUP_STATUS_IS_SUCCESSFUL (msg->status_code)) {
1921                 /* Success. */
1922                 const char *hdr;
1923                 int timeout;
1924
1925                 /* Save SID. */
1926                 hdr = soup_message_headers_get_one (msg->response_headers,
1927                                                     "SID");
1928                 if (hdr == NULL) {
1929                         error = g_error_new
1930                                         (GUPNP_EVENTING_ERROR,
1931                                          GUPNP_EVENTING_ERROR_SUBSCRIPTION_LOST,
1932                                          "No SID in SUBSCRIBE response");
1933
1934                         goto hdr_err;
1935                 }
1936
1937                 proxy->priv->sid = g_strdup (hdr);
1938
1939                 /* Figure out when the subscription times out */
1940                 hdr = soup_message_headers_get_one (msg->response_headers,
1941                                                     "Timeout");
1942                 if (hdr == NULL) {
1943                         g_warning ("No Timeout in SUBSCRIBE response.");
1944
1945                         return;
1946                 }
1947
1948                 if (strncmp (hdr, "Second-", strlen ("Second-")) == 0) {
1949                         /* We have a finite timeout */
1950                         timeout = atoi (hdr + strlen ("Second-"));
1951
1952                         if (timeout < 0) {
1953                                 g_warning ("Invalid time-out specified. "
1954                                            "Assuming default value of %d.",
1955                                            GENA_DEFAULT_TIMEOUT);
1956
1957                                 timeout = GENA_DEFAULT_TIMEOUT;
1958                         }
1959
1960                         /* We want to resubscribe before the subscription
1961                          * expires. */
1962                         timeout = g_random_int_range (1, timeout / 2);
1963
1964                         /* Add actual timeout */
1965                         proxy->priv->subscription_timeout_src =
1966                                 g_timeout_source_new_seconds (timeout);
1967                         g_source_set_callback
1968                                 (proxy->priv->subscription_timeout_src,
1969                                  subscription_expire,
1970                                  proxy, NULL);
1971                         g_source_attach (proxy->priv->subscription_timeout_src,
1972                                          g_main_context_get_thread_default ());
1973
1974                         g_source_unref (proxy->priv->subscription_timeout_src);
1975                 }
1976         } else {
1977                 GUPnPContext *context;
1978                 SoupServer *server;
1979
1980                 /* Subscription failed. */
1981                 error = g_error_new_literal
1982                                 (GUPNP_EVENTING_ERROR,
1983                                  GUPNP_EVENTING_ERROR_SUBSCRIPTION_FAILED,
1984                                  msg->reason_phrase);
1985
1986 hdr_err:
1987                 /* Remove listener */
1988                 context = gupnp_service_info_get_context
1989                                         (GUPNP_SERVICE_INFO (proxy));
1990
1991                 server = gupnp_context_get_server (context);
1992                 soup_server_remove_handler (server, proxy->priv->path);
1993
1994                 proxy->priv->subscribed = FALSE;
1995
1996                 g_object_notify (G_OBJECT (proxy), "subscribed");
1997
1998                 /* Emit subscription-lost */
1999                 g_signal_emit (proxy,
2000                                signals[SUBSCRIPTION_LOST],
2001                                0,
2002                                error);
2003
2004                 g_error_free (error);
2005         }
2006 }
2007
2008 /*
2009  * Subscribe to this service.
2010  */
2011 static void
2012 subscribe (GUPnPServiceProxy *proxy)
2013 {
2014         GUPnPContext *context;
2015         SoupMessage *msg;
2016         SoupSession *session;
2017         SoupServer *server;
2018         const char *server_url;
2019         char *sub_url, *delivery_url, *timeout;
2020
2021         context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy));
2022
2023         /* Create subscription message */
2024         sub_url = gupnp_service_info_get_event_subscription_url
2025                                                 (GUPNP_SERVICE_INFO (proxy));
2026
2027         msg = NULL;
2028         if (sub_url != NULL) {
2029                 msg = soup_message_new (GENA_METHOD_SUBSCRIBE, sub_url);
2030
2031                 g_free (sub_url);
2032         }
2033
2034         if (msg == NULL) {
2035                 GError *error;
2036
2037                 /* Subscription failed. */
2038                 proxy->priv->subscribed = FALSE;
2039
2040                 g_object_notify (G_OBJECT (proxy), "subscribed");
2041
2042                 /* Emit subscription-lost */
2043                 error = g_error_new (GUPNP_SERVER_ERROR,
2044                                      GUPNP_SERVER_ERROR_INVALID_URL,
2045                                      "No valid subscription URL defined");
2046
2047                 g_signal_emit (proxy,
2048                                signals[SUBSCRIPTION_LOST],
2049                                0,
2050                                error);
2051
2052                 g_error_free (error);
2053
2054                 return;
2055         }
2056
2057         /* Add headers */
2058         server_url = _gupnp_context_get_server_url (context);
2059         delivery_url = g_strdup_printf ("<%s%s>",
2060                                         server_url,
2061                                         proxy->priv->path);
2062         soup_message_headers_append (msg->request_headers,
2063                                      "Callback",
2064                                      delivery_url);
2065         g_free (delivery_url);
2066
2067         soup_message_headers_append (msg->request_headers,
2068                                      "NT",
2069                                      "upnp:event");
2070
2071         timeout = make_timeout_header (context);
2072         soup_message_headers_append (msg->request_headers,
2073                                      "Timeout",
2074                                      timeout);
2075         g_free (timeout);
2076
2077         /* Listen for events */
2078         server = gupnp_context_get_server (context);
2079
2080         soup_server_add_handler (server,
2081                                  proxy->priv->path,
2082                                  server_handler,
2083                                  proxy,
2084                                  NULL);
2085
2086         /* And send our subscription message off */
2087         proxy->priv->pending_messages =
2088                 g_list_prepend (proxy->priv->pending_messages, msg);
2089
2090         session = gupnp_context_get_session (context);
2091
2092         soup_session_queue_message (session,
2093                                     msg,
2094                                     (SoupSessionCallback)
2095                                         subscribe_got_response,
2096                                     proxy);
2097 }
2098
2099 /*
2100  * Unsubscribe from this service.
2101  */
2102 static void
2103 unsubscribe (GUPnPServiceProxy *proxy)
2104 {
2105         GUPnPContext *context;
2106         SoupSession *session;
2107         SoupServer *server;
2108
2109         context = gupnp_service_info_get_context (GUPNP_SERVICE_INFO (proxy));
2110
2111         /* Remove server handler */
2112         server = gupnp_context_get_server (context);
2113         soup_server_remove_handler (server, proxy->priv->path);
2114
2115         if (proxy->priv->sid != NULL) {
2116                 SoupMessage *msg;
2117                 char *sub_url;
2118
2119                 /* Create unsubscription message */
2120                 sub_url = gupnp_service_info_get_event_subscription_url
2121                                                    (GUPNP_SERVICE_INFO (proxy));
2122
2123                 msg = soup_message_new (GENA_METHOD_UNSUBSCRIBE, sub_url);
2124
2125                 g_free (sub_url);
2126
2127                 if (msg != NULL) {
2128                         /* Add headers */
2129                         soup_message_headers_append (msg->request_headers,
2130                                                      "SID",
2131                                                      proxy->priv->sid);
2132
2133                         /* And queue it */
2134                         session = gupnp_context_get_session (context);
2135
2136                         soup_session_queue_message (session, msg, NULL, NULL);
2137                 }
2138
2139                 /* Reset SID */
2140                 g_free (proxy->priv->sid);
2141                 proxy->priv->sid = NULL;
2142
2143                 /* Reset sequence number */
2144                 proxy->priv->seq = 0;
2145         }
2146
2147         /* Remove subscription timeout */
2148         if (proxy->priv->subscription_timeout_src) {
2149                 g_source_destroy (proxy->priv->subscription_timeout_src);
2150                 proxy->priv->subscription_timeout_src = NULL;
2151         }
2152 }
2153
2154 /**
2155  * gupnp_service_proxy_set_subscribed:
2156  * @proxy: A #GUPnPServiceProxy
2157  * @subscribed: %TRUE to subscribe to this service
2158  *
2159  * (Un)subscribes to this service.
2160  *
2161  * Note that the relevant messages are not immediately sent but queued.
2162  * If you want to unsubcribe from this service because the application
2163  * is quitting, rely on automatic synchronised unsubscription on object
2164  * destruction instead.
2165  **/
2166 void
2167 gupnp_service_proxy_set_subscribed (GUPnPServiceProxy *proxy,
2168                                     gboolean           subscribed)
2169 {
2170         g_return_if_fail (GUPNP_IS_SERVICE_PROXY (proxy));
2171
2172         if (proxy->priv->subscribed == subscribed)
2173                 return;
2174
2175         proxy->priv->subscribed = subscribed;
2176
2177         if (subscribed)
2178                 subscribe (proxy);
2179         else
2180                 unsubscribe (proxy);
2181
2182         g_object_notify (G_OBJECT (proxy), "subscribed");
2183 }
2184
2185 /**
2186  * gupnp_service_proxy_get_subscribed:
2187  * @proxy: A #GUPnPServiceProxy
2188  *
2189  * Returns if we are subscribed to this service.
2190  *
2191  * Return value: %TRUE if we are subscribed to this service, otherwise %FALSE.
2192  **/
2193 gboolean
2194 gupnp_service_proxy_get_subscribed (GUPnPServiceProxy *proxy)
2195 {
2196         g_return_val_if_fail (GUPNP_IS_SERVICE_PROXY (proxy), FALSE);
2197
2198         return proxy->priv->subscribed;
2199 }