1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
5 * Copyright (C) 2008 Red Hat, Inc.
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"
24 * SECTION:soup-cookie-jar
25 * @short_description: Automatic cookie handling for #SoupSession
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().
33 * Note that the base #SoupCookieJar class does not support any form
34 * of long-term cookie persistence.
37 static void soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface, gpointer interface_data);
38 static void request_queued (SoupSessionFeature *feature, SoupSession *session,
40 static void request_started (SoupSessionFeature *feature, SoupSession *session,
41 SoupMessage *msg, SoupSocket *socket);
42 static void request_unqueued (SoupSessionFeature *feature, SoupSession *session,
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))
54 static guint signals[LAST_SIGNAL] = { 0 };
65 gboolean constructed, read_only;
66 GHashTable *domains, *serials;
68 } SoupCookieJarPrivate;
69 #define SOUP_COOKIE_JAR_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), SOUP_TYPE_COOKIE_JAR, SoupCookieJarPrivate))
71 static void set_property (GObject *object, guint prop_id,
72 const GValue *value, GParamSpec *pspec);
73 static void get_property (GObject *object, guint prop_id,
74 GValue *value, GParamSpec *pspec);
77 soup_cookie_jar_init (SoupCookieJar *jar)
79 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
81 priv->domains = g_hash_table_new_full (soup_str_case_hash,
84 priv->serials = g_hash_table_new (NULL, NULL);
88 constructed (GObject *object)
90 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
92 priv->constructed = TRUE;
96 finalize (GObject *object)
98 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (object);
102 g_hash_table_iter_init (&iter, priv->domains);
103 while (g_hash_table_iter_next (&iter, &key, &value))
104 soup_cookies_free (value);
105 g_hash_table_destroy (priv->domains);
106 g_hash_table_destroy (priv->serials);
108 G_OBJECT_CLASS (soup_cookie_jar_parent_class)->finalize (object);
112 soup_cookie_jar_class_init (SoupCookieJarClass *jar_class)
114 GObjectClass *object_class = G_OBJECT_CLASS (jar_class);
116 g_type_class_add_private (jar_class, sizeof (SoupCookieJarPrivate));
118 object_class->constructed = constructed;
119 object_class->finalize = finalize;
120 object_class->set_property = set_property;
121 object_class->get_property = get_property;
124 * SoupCookieJar::changed
125 * @jar: the #SoupCookieJar
126 * @old_cookie: the old #SoupCookie value
127 * @new_cookie: the new #SoupCookie value
129 * Emitted when @jar changes. If a cookie has been added,
130 * @new_cookie will contain the newly-added cookie and
131 * @old_cookie will be %NULL. If a cookie has been deleted,
132 * @old_cookie will contain the to-be-deleted cookie and
133 * @new_cookie will be %NULL. If a cookie has been changed,
134 * @old_cookie will contain its old value, and @new_cookie its
138 g_signal_new ("changed",
139 G_OBJECT_CLASS_TYPE (object_class),
141 G_STRUCT_OFFSET (SoupCookieJarClass, changed),
143 soup_marshal_NONE__BOXED_BOXED,
145 SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE,
146 SOUP_TYPE_COOKIE | G_SIGNAL_TYPE_STATIC_SCOPE);
149 * SOUP_COOKIE_JAR_READ_ONLY:
151 * Alias for the #SoupCookieJar:read-only property. (Whether
152 * or not the cookie jar is read-only.)
154 g_object_class_install_property (
155 object_class, PROP_READ_ONLY,
156 g_param_spec_boolean (SOUP_COOKIE_JAR_READ_ONLY,
158 "Whether or not the cookie jar is read-only",
160 G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
164 soup_cookie_jar_session_feature_init (SoupSessionFeatureInterface *feature_interface,
165 gpointer interface_data)
167 feature_interface->request_queued = request_queued;
168 feature_interface->request_started = request_started;
169 feature_interface->request_unqueued = request_unqueued;
173 set_property (GObject *object, guint prop_id,
174 const GValue *value, GParamSpec *pspec)
176 SoupCookieJarPrivate *priv =
177 SOUP_COOKIE_JAR_GET_PRIVATE (object);
181 priv->read_only = g_value_get_boolean (value);
184 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
190 get_property (GObject *object, guint prop_id,
191 GValue *value, GParamSpec *pspec)
193 SoupCookieJarPrivate *priv =
194 SOUP_COOKIE_JAR_GET_PRIVATE (object);
198 g_value_set_boolean (value, priv->read_only);
201 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
207 * soup_cookie_jar_new:
209 * Creates a new #SoupCookieJar. The base #SoupCookieJar class does
210 * not support persistent storage of cookies; use a subclass for that.
212 * Returns: a new #SoupCookieJar
217 soup_cookie_jar_new (void)
219 return g_object_new (SOUP_TYPE_COOKIE_JAR, NULL);
223 soup_cookie_jar_save (SoupCookieJar *jar)
225 /* Does nothing, obsolete */
229 soup_cookie_jar_changed (SoupCookieJar *jar,
230 SoupCookie *old, SoupCookie *new)
232 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
234 if (old && old != new)
235 g_hash_table_remove (priv->serials, old);
238 g_hash_table_insert (priv->serials, new, GUINT_TO_POINTER (priv->serial));
241 if (priv->read_only || !priv->constructed)
244 g_signal_emit (jar, signals[CHANGED], 0, old, new);
248 compare_cookies (gconstpointer a, gconstpointer b, gpointer jar)
250 SoupCookie *ca = (SoupCookie *)a;
251 SoupCookie *cb = (SoupCookie *)b;
252 SoupCookieJarPrivate *priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
254 guint aserial, bserial;
256 /* "Cookies with longer path fields are listed before cookies
257 * with shorter path field."
259 alen = ca->path ? strlen (ca->path) : 0;
260 blen = cb->path ? strlen (cb->path) : 0;
264 /* "Among cookies that have equal length path fields, cookies
265 * with earlier creation dates are listed before cookies with
266 * later creation dates."
268 aserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, ca));
269 bserial = GPOINTER_TO_UINT (g_hash_table_lookup (priv->serials, cb));
270 return aserial - bserial;
274 * soup_cookie_jar_get_cookies:
275 * @jar: a #SoupCookieJar
277 * @for_http: whether or not the return value is being passed directly
278 * to an HTTP operation
280 * Retrieves (in Cookie-header form) the list of cookies that would
281 * be sent with a request to @uri.
283 * If @for_http is %TRUE, the return value will include cookies marked
284 * "HttpOnly" (that is, cookies that the server wishes to keep hidden
285 * from client-side scripting operations such as the JavaScript
286 * document.cookies property). Since #SoupCookieJar sets the Cookie
287 * header itself when making the actual HTTP request, you should
288 * almost certainly be setting @for_http to %FALSE if you are calling
291 * Return value: the cookies, in string form, or %NULL if there are no
297 soup_cookie_jar_get_cookies (SoupCookieJar *jar, SoupURI *uri,
300 SoupCookieJarPrivate *priv;
301 GSList *cookies, *domain_cookies;
302 char *domain, *cur, *next_domain, *result;
303 GSList *new_head, *cookies_to_remove = NULL, *p;
305 g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
306 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
307 g_return_val_if_fail (uri != NULL, NULL);
309 if (!SOUP_URI_VALID_FOR_HTTP (uri))
312 /* The logic here is a little weird, but the plan is that if
313 * uri->host is "www.foo.com", we will end up looking up
314 * cookies for ".www.foo.com", "www.foo.com", ".foo.com", and
315 * ".com", in that order. (Logic stolen from Mozilla.)
318 domain = cur = g_strdup_printf (".%s", uri->host);
319 next_domain = domain + 1;
321 new_head = domain_cookies = g_hash_table_lookup (priv->domains, cur);
322 while (domain_cookies) {
323 GSList *next = domain_cookies->next;
324 SoupCookie *cookie = domain_cookies->data;
326 if (cookie->expires && soup_date_is_past (cookie->expires)) {
327 cookies_to_remove = g_slist_append (cookies_to_remove,
329 new_head = g_slist_delete_link (new_head, domain_cookies);
330 g_hash_table_insert (priv->domains,
333 } else if (soup_cookie_applies_to_uri (cookie, uri) &&
334 (for_http || !cookie->http_only))
335 cookies = g_slist_append (cookies, cookie);
337 domain_cookies = next;
341 next_domain = strchr (cur + 1, '.');
345 for (p = cookies_to_remove; p; p = p->next) {
346 SoupCookie *cookie = p->data;
348 soup_cookie_jar_changed (jar, cookie, NULL);
349 soup_cookie_free (cookie);
351 g_slist_free (cookies_to_remove);
354 cookies = g_slist_sort_with_data (cookies, compare_cookies, jar);
355 result = soup_cookies_to_cookie_header (cookies);
356 g_slist_free (cookies);
368 * soup_cookie_jar_add_cookie:
369 * @jar: a #SoupCookieJar
370 * @cookie: a #SoupCookie
372 * Adds @cookie to @jar, emitting the 'changed' signal if we are modifying
373 * an existing cookie or adding a valid new cookie ('valid' means
374 * that the cookie's expire date is not in the past).
376 * @cookie will be 'stolen' by the jar, so don't free it afterwards.
381 soup_cookie_jar_add_cookie (SoupCookieJar *jar, SoupCookie *cookie)
383 SoupCookieJarPrivate *priv;
384 GSList *old_cookies, *oc, *prev = NULL;
385 SoupCookie *old_cookie;
387 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
388 g_return_if_fail (cookie != NULL);
390 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
391 old_cookies = g_hash_table_lookup (priv->domains, cookie->domain);
392 for (oc = old_cookies; oc; oc = oc->next) {
393 old_cookie = oc->data;
394 if (!strcmp (cookie->name, old_cookie->name) &&
395 !g_strcmp0 (cookie->path, old_cookie->path)) {
396 if (cookie->expires && soup_date_is_past (cookie->expires)) {
397 /* The new cookie has an expired date,
398 * this is the way the the server has
399 * of telling us that we have to
402 old_cookies = g_slist_delete_link (old_cookies, oc);
403 g_hash_table_insert (priv->domains,
404 g_strdup (cookie->domain),
406 soup_cookie_jar_changed (jar, old_cookie, NULL);
407 soup_cookie_free (old_cookie);
408 soup_cookie_free (cookie);
411 soup_cookie_jar_changed (jar, old_cookie, cookie);
412 soup_cookie_free (old_cookie);
420 /* The new cookie is... a new cookie */
421 if (cookie->expires && soup_date_is_past (cookie->expires)) {
422 soup_cookie_free (cookie);
427 prev = g_slist_append (prev, cookie);
429 old_cookies = g_slist_append (NULL, cookie);
430 g_hash_table_insert (priv->domains, g_strdup (cookie->domain),
434 soup_cookie_jar_changed (jar, NULL, cookie);
438 * soup_cookie_jar_set_cookie:
439 * @jar: a #SoupCookieJar
440 * @uri: the URI setting the cookie
441 * @cookie: the stringified cookie to set
443 * Adds @cookie to @jar, exactly as though it had appeared in a
444 * Set-Cookie header returned from a request to @uri.
449 soup_cookie_jar_set_cookie (SoupCookieJar *jar, SoupURI *uri,
452 SoupCookie *soup_cookie;
454 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
455 g_return_if_fail (uri != NULL);
456 g_return_if_fail (cookie != NULL);
458 if (!SOUP_URI_VALID_FOR_HTTP (uri))
461 soup_cookie = soup_cookie_parse (cookie, uri);
463 /* will steal or free soup_cookie */
464 soup_cookie_jar_add_cookie (jar, soup_cookie);
469 process_set_cookie_header (SoupMessage *msg, gpointer user_data)
471 SoupCookieJar *jar = user_data;
472 GSList *new_cookies, *nc;
474 new_cookies = soup_cookies_from_response (msg);
475 for (nc = new_cookies; nc; nc = nc->next)
476 soup_cookie_jar_add_cookie (jar, nc->data);
477 g_slist_free (new_cookies);
481 request_queued (SoupSessionFeature *feature, SoupSession *session,
484 soup_message_add_header_handler (msg, "got-headers",
486 G_CALLBACK (process_set_cookie_header),
491 request_started (SoupSessionFeature *feature, SoupSession *session,
492 SoupMessage *msg, SoupSocket *socket)
494 SoupCookieJar *jar = SOUP_COOKIE_JAR (feature);
497 cookies = soup_cookie_jar_get_cookies (jar, soup_message_get_uri (msg), TRUE);
499 soup_message_headers_replace (msg->request_headers,
503 soup_message_headers_remove (msg->request_headers, "Cookie");
507 request_unqueued (SoupSessionFeature *feature, SoupSession *session,
510 g_signal_handlers_disconnect_by_func (msg, process_set_cookie_header, feature);
514 * soup_cookie_jar_all_cookies:
515 * @jar: a #SoupCookieJar
517 * Constructs a #GSList with every cookie inside the @jar.
518 * The cookies in the list are a copy of the original, so
519 * you have to free them when you are done with them.
521 * Return value: a #GSList with all the cookies in the @jar.
526 soup_cookie_jar_all_cookies (SoupCookieJar *jar)
528 SoupCookieJarPrivate *priv;
533 g_return_val_if_fail (SOUP_IS_COOKIE_JAR (jar), NULL);
535 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
537 g_hash_table_iter_init (&iter, priv->domains);
539 while (g_hash_table_iter_next (&iter, &key, &value)) {
540 GSList *p, *cookies = value;
541 for (p = cookies; p; p = p->next)
542 l = g_slist_prepend (l, soup_cookie_copy (p->data));
549 * soup_cookie_jar_delete_cookie:
550 * @jar: a #SoupCookieJar
551 * @cookie: a #SoupCookie
553 * Deletes @cookie from @jar, emitting the 'changed' signal.
558 soup_cookie_jar_delete_cookie (SoupCookieJar *jar,
561 SoupCookieJarPrivate *priv;
565 g_return_if_fail (SOUP_IS_COOKIE_JAR (jar));
566 g_return_if_fail (cookie != NULL);
568 priv = SOUP_COOKIE_JAR_GET_PRIVATE (jar);
570 domain = g_strdup (cookie->domain);
572 cookies = g_hash_table_lookup (priv->domains, domain);
576 for (p = cookies; p; p = p->next ) {
577 SoupCookie *c = (SoupCookie*)p->data;
578 if (soup_cookie_equal (cookie, c)) {
579 cookies = g_slist_delete_link (cookies, p);
580 g_hash_table_insert (priv->domains,
583 soup_cookie_jar_changed (jar, c, NULL);
584 soup_cookie_free (c);