Don't crash when setting cookie for about:blank
[platform/upstream/libsoup.git] / libsoup / soup-cookie-jar.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  * soup-cookie-jar.c
4  *
5  * Copyright (C) 2008 Red Hat, Inc.
6  */
7
8 #ifdef HAVE_CONFIG_H
9 #include <config.h>
10 #endif
11
12 #include <stdio.h>
13 #include <string.h>
14
15 #include "soup-cookie.h"
16 #include "soup-cookie-jar.h"
17 #include "soup-date.h"
18 #include "soup-marshal.h"
19 #include "soup-message.h"
20 #include "soup-session-feature.h"
21 #include "soup-uri.h"
22
23 /**
24  * SECTION:soup-cookie-jar
25  * @short_description: Automatic cookie handling for #SoupSession
26  *
27  * A #SoupCookieJar stores #SoupCookie<!-- -->s and arrange for them
28  * to be sent with the appropriate #SoupMessage<!-- -->s.
29  * #SoupCookieJar implements #SoupSessionFeature, so you can add a
30  * cookie jar to a session with soup_session_add_feature() or
31  * soup_session_add_feature_by_type().
32  *
33  * Note that the base #SoupCookieJar class does not support any form
34  * of long-term cookie persistence.
35  **/
36
37 static void soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
38 static void request_queued (SoupSessionFeature *feature, SoupSession *session,
39                             SoupMessage *msg);
40 static void request_started (SoupSessionFeature *feature, SoupSession *session,
41                              SoupMessage *msg, SoupSocket *socket);
42 static void request_unqueued (SoupSessionFeature *feature, SoupSession *session,
43                               SoupMessage *msg);
44
45 G_DEFINE_TYPE_WITH_CODE (SoupCookieJar, soup_cookie_jar, G_TYPE_OBJECT,
46                          G_IMPLEMENT_INTERFACE (SOUP_TYPE_SESSION_FEATURE,
47                                                 soup_cookie_jar_session_feature_init))
48
49 enum {
50         CHANGED,
51         LAST_SIGNAL
52 };
53
54 static guint signals[LAST_SIGNAL] = { 0 };
55
56 enum {
57         PROP_0,
58
59         PROP_READ_ONLY,
60
61         LAST_PROP
62 };
63
64 typedef struct {
65         gboolean constructed, read_only;
66         GHashTable *domains;
67 } SoupCookieJarPrivate;
68 #define SOUP_COOKIE_JAR_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_COOKIE_JAR, SoupCookieJarPrivate))
69
70 static void set_property (GObject *object, guint prop_id,
71                           const GValue *value, GParamSpec *pspec);
72 static void get_property (GObject *object, guint prop_id,
73                           GValue *value, GParamSpec *pspec);
74
75 static void
76 soup_cookie_jar_init (SoupCookieJar *jar)
77 {
78         SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
79
80         priv->domains = g_hash_table_new_full (g_str_hash, g_str_equal,
81                                                g_free, NULL);
82 }
83
84 static void
85 constructed (GObject *object)
86 {
87         SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
88
89         priv->constructed = TRUE;
90 }
91
92 static void
93 finalize (GObject *object)
94 {
95         SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
96         GHashTableIter iter;
97         gpointer key, value;
98
99         g_hash_table_iter_init (&iter, priv->domains);
100         while (g_hash_table_iter_next (&iter, &key, &value))
101                 soup_cookies_free (value);
102         g_hash_table_destroy (priv->domains);
103
104         G_OBJECT_CLASS (soup_cookie_jar_parent_class)->finalize (object);
105 }
106
107 static void
108 soup_cookie_jar_class_init (SoupCookieJarClass *jar_class)
109 {
110         GObjectClass *object_class = G_OBJECT_CLASS (jar_class);
111
112         g_type_class_add_private (jar_class, sizeof (SoupCookieJarPrivate));
113
114         object_class->constructed = constructed;
115         object_class->finalize = finalize;
116         object_class->set_property = set_property;
117         object_class->get_property = get_property;
118
119         /**
120          * SoupCookieJar::changed
121          * @jar: the #SoupCookieJar
122          * @old_cookie: the old #SoupCookie value
123          * @new_cookie: the new #SoupCookie value
124          *
125          * Emitted when @jar changes. If a cookie has been added,
126          * @new_cookie will contain the newly-added cookie and
127          * @old_cookie will be %NULL. If a cookie has been deleted,
128          * @old_cookie will contain the to-be-deleted cookie and
129          * @new_cookie will be %NULL. If a cookie has been changed,
130          * @old_cookie will contain its old value, and @new_cookie its
131          * new value.
132          **/
133         signals[CHANGED] =
134                 g_signal_new ("changed",
135                               G_OBJECT_CLASS_TYPE (object_class),
136                               G_SIGNAL_RUN_FIRST,
137                               G_STRUCT_OFFSET (SoupCookieJarClass, changed),
138                               NULL, NULL,
139                               soup_marshal_NONE__BOXED_BOXED,
140                               G_TYPE_NONE, 2, 
141                               SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE,
142                               SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE);
143
144         /**
145          * SOUP_COOKIE_JAR_READ_ONLY:
146          *
147          * Alias for the #SoupCookieJar:read-only property. (Whether
148          * or not the cookie jar is read-only.)
149          **/
150         g_object_class_install_property (
151                 object_class, PROP_READ_ONLY,
152                 g_param_spec_boolean (SOUP_COOKIE_JAR_READ_ONLY,
153                                       "Read-only",
154                                       "Whether or not the cookie jar is read-only",
155                                       FALSE,
156                                       G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
157 }
158
159 static void
160 soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface,
161                                       gpointer interface_data)
162 {
163         feature_interface->request_queued = request_queued;
164         feature_interface->request_started = request_started;
165         feature_interface->request_unqueued = request_unqueued;
166 }
167
168 static void
169 set_property (GObject *object, guint prop_id,
170               const GValue *value, GParamSpec *pspec)
171 {
172         SoupCookieJarPrivate *priv =
173                 SOUP_COOKIE_JAR_GET_PRIVATE (object);
174
175         switch (prop_id) {
176         case PROP_READ_ONLY:
177                 priv->read_only = g_value_get_boolean (value);
178                 break;
179         default:
180                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
181                 break;
182         }
183 }
184
185 static void
186 get_property (GObject *object, guint prop_id,
187               GValue *value, GParamSpec *pspec)
188 {
189         SoupCookieJarPrivate *priv =
190                 SOUP_COOKIE_JAR_GET_PRIVATE (object);
191
192         switch (prop_id) {
193         case PROP_READ_ONLY:
194                 g_value_set_boolean (value, priv->read_only);
195                 break;
196         default:
197                 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
198                 break;
199         }
200 }
201
202 /**
203  * soup_cookie_jar_new:
204  *
205  * Creates a new #SoupCookieJar. The base #SoupCookieJar class does
206  * not support persistent storage of cookies; use a subclass for that.
207  *
208  * Returns: a new #SoupCookieJar
209  *
210  * Since: 2.24
211  **/
212 SoupCookieJar *
213 soup_cookie_jar_new (void) 
214 {
215         return g_object_new (SOUP_TYPE_COOKIE_JAR, NULL);
216 }
217
218 void
219 soup_cookie_jar_save (SoupCookieJar *jar)
220 {
221         /* Does nothing, obsolete */
222 }
223
224 static void
225 soup_cookie_jar_changed (SoupCookieJar *jar,
226                          SoupCookie *old, SoupCookie *new)
227 {
228         SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
229
230         if (priv->read_only || !priv->constructed)
231                 return;
232
233         g_signal_emit (jar, signals[CHANGED], 0, old, new);
234 }
235
236 /**
237  * soup_cookie_jar_get_cookies:
238  * @jar: a #SoupCookieJar
239  * @uri: a #SoupURI
240  * @for_http: whether or not the return value is being passed directly
241  * to an HTTP operation
242  *
243  * Retrieves (in Cookie-header form) the list of cookies that would
244  * be sent with a request to @uri.
245  *
246  * If @for_http is %TRUE, the return value will include cookies marked
247  * "HttpOnly" (that is, cookies that the server wishes to keep hidden
248  * from client-side scripting operations such as the JavaScript
249  * document.cookies property). Since #SoupCookieJar sets the Cookie
250  * header itself when making the actual HTTP request, you should
251  * almost certainly be setting @for_http to %FALSE if you are calling
252  * this.
253  *
254  * Return value: the cookies, in string form, or %NULL if there are no
255  * cookies for @uri.
256  *
257  * Since: 2.24
258  **/
259 char *
260 soup_cookie_jar_get_cookies (SoupCookieJar *jar, SoupURI *uri,
261                              gboolean for_http)
262 {
263         SoupCookieJarPrivate *priv;
264         GSList *cookies, *domain_cookies;
265         char *domain, *cur, *next_domain, *result;
266         GSList *new_head, *cookies_to_remove = NULL, *p;
267
268         g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
269         priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
270         g_return_val_if_fail (uri != NULL, NULL);
271
272         if (!SOUP_URI_VALID_FOR_HTTP (uri))
273                 return NULL;
274
275         /* The logic here is a little weird, but the plan is that if
276          * uri->host is "www.foo.com", we will end up looking up
277          * cookies for ".www.foo.com", "www.foo.com", ".foo.com", and
278          * ".com", in that order. (Logic stolen from Mozilla.)
279          */
280         cookies = NULL;
281         domain = cur = g_strdup_printf (".%s", uri->host);
282         next_domain = domain + 1;
283         do {
284                 new_head = domain_cookies = g_hash_table_lookup (priv->domains, cur);
285                 while (domain_cookies) {
286                         GSList *next = domain_cookies->next;
287                         SoupCookie *cookie = domain_cookies->data;
288
289                         if (cookie->expires && soup_date_is_past (cookie->expires)) {
290                                 cookies_to_remove = g_slist_append (cookies_to_remove,
291                                                                     cookie);
292                                 new_head = g_slist_delete_link (new_head, domain_cookies);
293                                 g_hash_table_insert (priv->domains,
294                                                      g_strdup (cur),
295                                                      new_head);
296                         } else if (soup_cookie_applies_to_uri (cookie, uri) &&
297                                    (for_http || !cookie->http_only))
298                                 cookies = g_slist_append (cookies, cookie);
299
300                         domain_cookies = next;
301                 }
302                 cur = next_domain;
303                 if (cur)
304                         next_domain = strchr (cur + 1, '.');
305         } while (cur);
306         g_free (domain);
307
308         for (p = cookies_to_remove; p; p = p->next) {
309                 SoupCookie *cookie = p->data;
310
311                 soup_cookie_jar_changed (jar, cookie, NULL);
312                 soup_cookie_free (cookie);
313         }
314         g_slist_free (cookies_to_remove);
315
316         if (cookies) {
317                 /* FIXME: sort? */
318                 result = soup_cookies_to_cookie_header (cookies);
319                 g_slist_free (cookies);
320                 return result;
321         } else
322                 return NULL;
323 }
324
325 /**
326  * soup_cookie_jar_add_cookie:
327  * @jar: a #SoupCookieJar
328  * @cookie: a #SoupCookie
329  *
330  * Adds @cookie to @jar, emitting the 'changed' signal if we are modifying
331  * an existing cookie or adding a valid new cookie ('valid' means
332  * that the cookie's expire date is not in the past).
333  *
334  * @cookie will be 'stolen' by the jar, so don't free it afterwards.
335  *
336  * Since: 2.26
337  **/
338 void
339 soup_cookie_jar_add_cookie (SoupCookieJar *jar, SoupCookie *cookie)
340 {
341         SoupCookieJarPrivate *priv;
342         GSList *old_cookies, *oc, *prev = NULL;
343         SoupCookie *old_cookie;
344
345         g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
346         g_return_if_fail (cookie != NULL);
347
348         priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
349         old_cookies = g_hash_table_lookup (priv->domains, cookie->domain);
350         for (oc = old_cookies; oc; oc = oc->next) {
351                 old_cookie = oc->data;
352                 if (!strcmp (cookie->name, old_cookie->name) &&
353                     !g_strcmp0 (cookie->path, old_cookie->path)) {
354                         if (cookie->expires && soup_date_is_past (cookie->expires)) {
355                                 /* The new cookie has an expired date,
356                                  * this is the way the the server has
357                                  * of telling us that we have to
358                                  * remove the cookie.
359                                  */
360                                 old_cookies = g_slist_delete_link (old_cookies, oc);
361                                 g_hash_table_insert (priv->domains,
362                                                      g_strdup (cookie->domain),
363                                                      old_cookies);
364                                 soup_cookie_jar_changed (jar, old_cookie, NULL);
365                                 soup_cookie_free (old_cookie);
366                                 soup_cookie_free (cookie);
367                         } else {
368                                 oc->data = cookie;
369                                 soup_cookie_jar_changed (jar, old_cookie, cookie);
370                                 soup_cookie_free (old_cookie);
371                         }
372
373                         return;
374                 }
375                 prev = oc;
376         }
377
378         /* The new cookie is... a new cookie */
379         if (cookie->expires && soup_date_is_past (cookie->expires)) {
380                 soup_cookie_free (cookie);
381                 return;
382         }
383
384         if (prev)
385                 prev = g_slist_append (prev, cookie);
386         else {
387                 old_cookies = g_slist_append (NULL, cookie);
388                 g_hash_table_insert (priv->domains, g_strdup (cookie->domain),
389                                      old_cookies);
390         }
391
392         soup_cookie_jar_changed (jar, NULL, cookie);
393 }
394
395 /**
396  * soup_cookie_jar_set_cookie:
397  * @jar: a #SoupCookieJar
398  * @uri: the URI setting the cookie
399  * @cookie: the stringified cookie to set
400  *
401  * Adds @cookie to @jar, exactly as though it had appeared in a
402  * Set-Cookie header returned from a request to @uri.
403  *
404  * Since: 2.24
405  **/
406 void
407 soup_cookie_jar_set_cookie (SoupCookieJar *jar, SoupURI *uri,
408                             const char *cookie)
409 {
410         SoupCookie *soup_cookie;
411
412         g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
413         g_return_if_fail (uri != NULL);
414         g_return_if_fail (cookie != NULL);
415
416         if (!SOUP_URI_VALID_FOR_HTTP (uri))
417                 return;
418
419         soup_cookie = soup_cookie_parse (cookie, uri);
420         if (soup_cookie) {
421                 /* will steal or free soup_cookie */
422                 soup_cookie_jar_add_cookie (jar, soup_cookie);
423         }
424 }
425
426 static void
427 process_set_cookie_header (SoupMessage *msg, gpointer user_data)
428 {
429         SoupCookieJar *jar = user_data;
430         GSList *new_cookies, *nc;
431
432         new_cookies = soup_cookies_from_response (msg);
433         for (nc = new_cookies; nc; nc = nc->next)
434                 soup_cookie_jar_add_cookie (jar, nc->data);
435         g_slist_free (new_cookies);
436 }
437
438 static void
439 request_queued (SoupSessionFeature *feature, SoupSession *session,
440                 SoupMessage *msg)
441 {
442         soup_message_add_header_handler (msg, "got-headers",
443                                          "Set-Cookie",
444                                          G_CALLBACK (process_set_cookie_header),
445                                          feature);
446 }
447
448 static void
449 request_started (SoupSessionFeature *feature, SoupSession *session,
450                  SoupMessage *msg, SoupSocket *socket)
451 {
452         SoupCookieJar *jar = SOUP_COOKIE_JAR (feature);
453         char *cookies;
454
455         cookies = soup_cookie_jar_get_cookies (jar, soup_message_get_uri (msg), TRUE);
456         if (cookies) {
457                 soup_message_headers_replace (msg->request_headers,
458                                               "Cookie", cookies);
459                 g_free (cookies);
460         } else
461                 soup_message_headers_remove (msg->request_headers, "Cookie");
462 }
463
464 static void
465 request_unqueued (SoupSessionFeature *feature, SoupSession *session,
466                   SoupMessage *msg)
467 {
468         g_signal_handlers_disconnect_by_func (msg, process_set_cookie_header, feature);
469 }
470
471 /**
472  * soup_cookie_jar_all_cookies:
473  * @jar: a #SoupCookieJar
474  *
475  * Constructs a #GSList with every cookie inside the @jar.
476  * The cookies in the list are a copy of the original, so
477  * you have to free them when you are done with them.
478  *
479  * Return value: a #GSList with all the cookies in the @jar.
480  *
481  * Since: 2.26
482  **/
483 GSList *
484 soup_cookie_jar_all_cookies (SoupCookieJar *jar)
485 {
486         SoupCookieJarPrivate *priv;
487         GHashTableIter iter;
488         GSList *l = NULL;
489         gpointer key, value;
490
491         g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
492
493         priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
494
495         g_hash_table_iter_init (&iter, priv->domains);
496
497         while (g_hash_table_iter_next (&iter, &key, &value)) {
498                 GSList *p, *cookies = value;
499                 for (p = cookies; p; p = p->next)
500                         l = g_slist_prepend (l, soup_cookie_copy (p->data));
501         }
502
503         return l;
504 }
505
506 /**
507  * soup_cookie_jar_delete_cookie:
508  * @jar: a #SoupCookieJar
509  * @cookie: a #SoupCookie
510  *
511  * Deletes @cookie from @jar, emitting the 'changed' signal.
512  *
513  * Since: 2.26
514  **/
515 void
516 soup_cookie_jar_delete_cookie (SoupCookieJar *jar,
517                                SoupCookie    *cookie)
518 {
519         SoupCookieJarPrivate *priv;
520         GSList *cookies, *p;
521         char *domain;
522
523         g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
524         g_return_if_fail (cookie != NULL);
525
526         priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
527
528         domain = g_strdup (cookie->domain);
529
530         cookies = g_hash_table_lookup (priv->domains, domain);
531         if (cookies == NULL)
532                 return;
533
534         for (p = cookies; p; p = p->next ) {
535                 SoupCookie *c = (SoupCookie*)p->data;
536                 if (soup_cookie_equal (cookie, c)) {
537                         cookies = g_slist_delete_link (cookies, p);
538                         g_hash_table_insert (priv->domains,
539                                              domain,
540                                              cookies);
541                         soup_cookie_jar_changed (jar, c, NULL);
542                         soup_cookie_free (c);
543                         return;
544                 }
545         }
546 }