1/*
2 * Copyright (C) 2012 Igalia S.L.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2,1 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB.  If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20#include "config.h"
21#include "WebKitSoupCookieJarSqlite.h"
22
23#include <WebCore/SQLiteDatabase.h>
24#include <WebCore/SQLiteStatement.h>
25#include <WebCore/SQLiteTransaction.h>
26#include <libsoup/soup.h>
27#include <wtf/CurrentTime.h>
28#include <wtf/MathExtras.h>
29
30using namespace WebCore;
31
32struct _WebKitSoupCookieJarSqlitePrivate {
33    String databasePath;
34    SQLiteDatabase database;
35    bool isLoading;
36};
37
38G_DEFINE_TYPE(WebKitSoupCookieJarSqlite, webkit_soup_cookie_jar_sqlite, SOUP_TYPE_COOKIE_JAR)
39
40enum {
41    ColumnID,
42    ColumnName,
43    ColumnValue,
44    ColumnHost,
45    ColumnPath,
46    ColumnExpiry,
47    ColumnLastAccess,
48    ColumnSecure,
49    ColumnHTTPOnly
50};
51
52static bool webkitSoupCookieJarSqliteOpenDatabase(WebKitSoupCookieJarSqlite* sqliteJar)
53{
54    WebKitSoupCookieJarSqlitePrivate* priv = sqliteJar->priv;
55    if (priv->database.isOpen())
56        return true;
57
58    ASSERT(!priv->databasePath.isEmpty());
59    if (!priv->database.open(priv->databasePath)) {
60        g_warning("Can't open database %s", priv->databasePath.utf8().data());
61        return false;
62    }
63
64    priv->database.setSynchronous(SQLiteDatabase::SyncOff);
65    priv->database.executeCommand("PRAGMA secure_delete = 1;");
66
67    return true;
68}
69
70static bool webkitSoupCookieJarSqliteCreateTable(WebKitSoupCookieJarSqlite* sqliteJar)
71{
72    WebKitSoupCookieJarSqlitePrivate* priv = sqliteJar->priv;
73    if (priv->database.tableExists("moz_cookies"))
74        return true;
75
76    if (!priv->database.executeCommand("CREATE TABLE moz_cookies (id INTEGER PRIMARY KEY, name TEXT, value TEXT, host TEXT, path TEXT, expiry INTEGER, lastAccessed INTEGER, isSecure INTEGER, isHttpOnly INTEGER)")) {
77        g_warning("Failed to create table moz_cookies: (%i) - %s", priv->database.lastError(), priv->database.lastErrorMsg());
78        priv->database.close();
79
80        return false;
81    }
82
83    return true;
84}
85
86static void webkitSoupCookieJarSqliteLoad(WebKitSoupCookieJarSqlite* sqliteJar)
87{
88    if (!webkitSoupCookieJarSqliteOpenDatabase(sqliteJar))
89        return;
90    if (!webkitSoupCookieJarSqliteCreateTable(sqliteJar))
91        return;
92
93    WebKitSoupCookieJarSqlitePrivate* priv = sqliteJar->priv;
94    priv->isLoading = true;
95    SQLiteStatement query(priv->database, "SELECT id, name, value, host, path, expiry, lastAccessed, isSecure, isHttpOnly FROM moz_cookies;");
96    if (query.prepare() != SQLResultOk) {
97        g_warning("Failed to prepare all cookies query");
98        priv->isLoading = false;
99        return;
100    }
101
102    SoupCookieJar* jar = SOUP_COOKIE_JAR(sqliteJar);
103    time_t now = floorf(currentTime());
104    int result;
105    while ((result = query.step()) == SQLResultRow) {
106        int expireTime = query.getColumnInt(ColumnExpiry);
107        if (now >= expireTime)
108            continue;
109
110        SoupCookie* cookie = soup_cookie_new(query.getColumnText(ColumnName).utf8().data(), query.getColumnText(ColumnValue).utf8().data(),
111                                             query.getColumnText(ColumnHost).utf8().data(), query.getColumnText(ColumnPath).utf8().data(),
112                                             expireTime - now <= G_MAXINT ? expireTime - now : G_MAXINT);
113        if (query.getColumnInt(ColumnSecure))
114            soup_cookie_set_secure(cookie, TRUE);
115        if (query.getColumnInt(ColumnHTTPOnly))
116            soup_cookie_set_http_only(cookie, TRUE);
117
118        soup_cookie_jar_add_cookie(jar, cookie);
119    }
120
121    if (result != SQLResultDone)
122        g_warning("Error reading cookies from database");
123    priv->isLoading = false;
124}
125
126static bool webkitSoupCookieJarSqliteInsertCookie(WebKitSoupCookieJarSqlite* sqliteJar, SoupCookie* cookie)
127{
128    WebKitSoupCookieJarSqlitePrivate* priv = sqliteJar->priv;
129    SQLiteStatement query(priv->database, "INSERT INTO moz_cookies VALUES(NULL, ?, ?, ?, ?, ?, NULL, ?, ?);");
130    if (query.prepare() != SQLResultOk) {
131        g_warning("Failed to prepare insert cookies query");
132        return false;
133    }
134
135    query.bindText(1, String::fromUTF8(cookie->name));
136    query.bindText(2, String::fromUTF8(cookie->value));
137    query.bindText(3, String::fromUTF8(cookie->domain));
138    query.bindText(4, String::fromUTF8(cookie->path));
139    query.bindInt(5, static_cast<int64_t>(soup_date_to_time_t(cookie->expires)));
140    query.bindInt(6, cookie->secure);
141    query.bindInt(7, cookie->http_only);
142    if (query.step() != SQLResultDone) {
143        g_warning("Error adding cookie (name=%s, domain=%s) to database", cookie->name, cookie->name);
144        return false;
145    }
146
147    return true;
148}
149
150static bool webkitSoupCookieJarSqliteDeleteCookie(WebKitSoupCookieJarSqlite* sqliteJar, SoupCookie* cookie)
151{
152    WebKitSoupCookieJarSqlitePrivate* priv = sqliteJar->priv;
153    SQLiteStatement query(priv->database, "DELETE FROM moz_cookies WHERE name = (?) AND host = (?);");
154    if (query.prepare() != SQLResultOk) {
155        g_warning("Failed to prepare delete cookies query");
156        return false;
157    }
158
159    query.bindText(1, String::fromUTF8(cookie->name));
160    query.bindText(2, String::fromUTF8(cookie->domain));
161    if (query.step() != SQLResultDone) {
162        g_warning("Error deleting cookie (name=%s, domain=%s) from database", cookie->name, cookie->name);
163        return false;
164    }
165
166    return true;
167}
168
169static void webkitSoupCookieJarSqliteChanged(SoupCookieJar* jar, SoupCookie* oldCookie, SoupCookie* newCookie)
170{
171    WebKitSoupCookieJarSqlite* sqliteJar = WEBKIT_SOUP_COOKIE_JAR_SQLITE(jar);
172    if (sqliteJar->priv->isLoading)
173        return;
174    if (!webkitSoupCookieJarSqliteOpenDatabase(sqliteJar))
175        return;
176    if (!oldCookie && (!newCookie || !newCookie->expires))
177        return;
178    if (!webkitSoupCookieJarSqliteCreateTable(sqliteJar))
179        return;
180
181    SQLiteTransaction updateTransaction(sqliteJar->priv->database);
182    updateTransaction.begin();
183
184    if (oldCookie && !webkitSoupCookieJarSqliteDeleteCookie(sqliteJar, oldCookie))
185        return;
186
187    if (newCookie && newCookie->expires && !webkitSoupCookieJarSqliteInsertCookie(sqliteJar, newCookie))
188        return;
189
190    updateTransaction.commit();
191}
192
193static void webkitSoupCookieJarSqliteFinalize(GObject* object)
194{
195    WEBKIT_SOUP_COOKIE_JAR_SQLITE(object)->priv->~WebKitSoupCookieJarSqlitePrivate();
196    G_OBJECT_CLASS(webkit_soup_cookie_jar_sqlite_parent_class)->finalize(object);
197}
198
199static void webkit_soup_cookie_jar_sqlite_init(WebKitSoupCookieJarSqlite* sqliteJar)
200{
201    WebKitSoupCookieJarSqlitePrivate* priv = G_TYPE_INSTANCE_GET_PRIVATE(sqliteJar, WEBKIT_TYPE_SOUP_COOKIE_JAR_SQLITE, WebKitSoupCookieJarSqlitePrivate);
202    sqliteJar->priv = priv;
203    new (priv) WebKitSoupCookieJarSqlitePrivate();
204}
205
206static void webkit_soup_cookie_jar_sqlite_class_init(WebKitSoupCookieJarSqliteClass* sqliteJarClass)
207{
208    SoupCookieJarClass* cookieJarClass = SOUP_COOKIE_JAR_CLASS(sqliteJarClass);
209    cookieJarClass->changed = webkitSoupCookieJarSqliteChanged;
210
211    GObjectClass* gObjectClass = G_OBJECT_CLASS(sqliteJarClass);
212    gObjectClass->finalize = webkitSoupCookieJarSqliteFinalize;
213
214    g_type_class_add_private(sqliteJarClass, sizeof(WebKitSoupCookieJarSqlitePrivate));
215}
216
217SoupCookieJar* webkitSoupCookieJarSqliteNew(const String& databasePath)
218{
219    ASSERT(!databasePath.isEmpty());
220    WebKitSoupCookieJarSqlite* sqliteJar = WEBKIT_SOUP_COOKIE_JAR_SQLITE(g_object_new(WEBKIT_TYPE_SOUP_COOKIE_JAR_SQLITE, NULL));
221    sqliteJar->priv->databasePath = databasePath;
222    webkitSoupCookieJarSqliteLoad(sqliteJar);
223    return SOUP_COOKIE_JAR(sqliteJar);
224}
225