1// Copyright 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.android_webview;
6
7import android.content.ContentValues;
8import android.content.Context;
9import android.database.Cursor;
10import android.database.sqlite.SQLiteDatabase;
11import android.database.sqlite.SQLiteException;
12import android.util.Log;
13
14/**
15 * This database is used to support WebView's setHttpAuthUsernamePassword and
16 * getHttpAuthUsernamePassword methods, and WebViewDatabase's clearHttpAuthUsernamePassword and
17 * hasHttpAuthUsernamePassword methods.
18 *
19 * While this class is intended to be used as a singleton, this property is not enforced in this
20 * layer, primarily for ease of testing. To line up with the classic implementation and behavior,
21 * there is no specific handling and reporting when SQL errors occur.
22 *
23 * Note on thread-safety: As per the classic implementation, most API functions have thread safety
24 * provided by the underlying SQLiteDatabase instance. The exception is database opening: this
25 * is handled in the dedicated background thread, which also provides a performance gain
26 * if triggered early on (e.g. as a side effect of CookieSyncManager.createInstance() call),
27 * sufficiently in advance of the first blocking usage of the API.
28 */
29public class HttpAuthDatabase {
30
31    private static final String LOGTAG = "HttpAuthDatabase";
32
33    private static final int DATABASE_VERSION = 1;
34
35    private SQLiteDatabase mDatabase = null;
36
37    private static final String ID_COL = "_id";
38
39    private static final String[] ID_PROJECTION = new String[] {
40        ID_COL
41    };
42
43    // column id strings for "httpauth" table
44    private static final String HTTPAUTH_TABLE_NAME = "httpauth";
45    private static final String HTTPAUTH_HOST_COL = "host";
46    private static final String HTTPAUTH_REALM_COL = "realm";
47    private static final String HTTPAUTH_USERNAME_COL = "username";
48    private static final String HTTPAUTH_PASSWORD_COL = "password";
49
50    /**
51     * Initially false until the background thread completes.
52     */
53    private boolean mInitialized = false;
54
55    private final Object mInitializedLock = new Object();
56
57    /**
58     * Creates and returns an instance of HttpAuthDatabase for the named file, and kicks-off
59     * background initialization of that database.
60     *
61     * @param context the Context to use for opening the database
62     * @param databaseFile Name of the file to be initialized.
63     */
64    public static HttpAuthDatabase newInstance(final Context context, final String databaseFile) {
65        final HttpAuthDatabase httpAuthDatabase = new HttpAuthDatabase();
66        new Thread() {
67            @Override
68            public void run() {
69                httpAuthDatabase.initOnBackgroundThread(context, databaseFile);
70            }
71        }.start();
72        return httpAuthDatabase;
73    }
74
75    // Prevent instantiation. Callers should use newInstance().
76    private HttpAuthDatabase() {}
77
78    /**
79     * Initializes the databases and notifies any callers waiting on waitForInit.
80     *
81     * @param context the Context to use for opening the database
82     * @param databaseFile Name of the file to be initialized.
83     */
84    private void initOnBackgroundThread(Context context, String databaseFile) {
85        synchronized (mInitializedLock) {
86            if (mInitialized) {
87                return;
88            }
89
90            initDatabase(context, databaseFile);
91
92            // Thread done, notify.
93            mInitialized = true;
94            mInitializedLock.notifyAll();
95        }
96    }
97
98    /**
99     * Opens the database, and upgrades it if necessary.
100     *
101     * @param context the Context to use for opening the database
102     * @param databaseFile Name of the file to be initialized.
103     */
104    private void initDatabase(Context context, String databaseFile) {
105        try {
106            mDatabase = context.openOrCreateDatabase(databaseFile, 0, null);
107        } catch (SQLiteException e) {
108            // try again by deleting the old db and create a new one
109            if (context.deleteDatabase(databaseFile)) {
110                mDatabase = context.openOrCreateDatabase(databaseFile, 0, null);
111            }
112        }
113
114        if (mDatabase == null) {
115            // Not much we can do to recover at this point
116            Log.e(LOGTAG, "Unable to open or create " + databaseFile);
117            return;
118        }
119
120        if (mDatabase.getVersion() != DATABASE_VERSION) {
121            mDatabase.beginTransactionNonExclusive();
122            try {
123                createTable();
124                mDatabase.setTransactionSuccessful();
125            } finally {
126                mDatabase.endTransaction();
127            }
128        }
129    }
130
131    private void createTable() {
132        mDatabase.execSQL("CREATE TABLE " + HTTPAUTH_TABLE_NAME
133                + " (" + ID_COL + " INTEGER PRIMARY KEY, "
134                + HTTPAUTH_HOST_COL + " TEXT, " + HTTPAUTH_REALM_COL
135                + " TEXT, " + HTTPAUTH_USERNAME_COL + " TEXT, "
136                + HTTPAUTH_PASSWORD_COL + " TEXT," + " UNIQUE ("
137                + HTTPAUTH_HOST_COL + ", " + HTTPAUTH_REALM_COL
138                + ") ON CONFLICT REPLACE);");
139
140        mDatabase.setVersion(DATABASE_VERSION);
141    }
142
143    /**
144     * Waits for the background initialization thread to complete and check the database creation
145     * status.
146     *
147     * @return true if the database was initialized, false otherwise
148     */
149    private boolean waitForInit() {
150        synchronized (mInitializedLock) {
151            while (!mInitialized) {
152                try {
153                    mInitializedLock.wait();
154                } catch (InterruptedException e) {
155                    Log.e(LOGTAG, "Caught exception while checking initialization", e);
156                }
157            }
158        }
159        return mDatabase != null;
160    }
161
162    /**
163     * Sets the HTTP authentication password. Tuple (HTTPAUTH_HOST_COL, HTTPAUTH_REALM_COL,
164     * HTTPAUTH_USERNAME_COL) is unique.
165     *
166     * @param host the host for the password
167     * @param realm the realm for the password
168     * @param username the username for the password.
169     * @param password the password
170     */
171    public void setHttpAuthUsernamePassword(String host, String realm, String username,
172            String password) {
173        if (host == null || realm == null || !waitForInit()) {
174            return;
175        }
176
177        final ContentValues c = new ContentValues();
178        c.put(HTTPAUTH_HOST_COL, host);
179        c.put(HTTPAUTH_REALM_COL, realm);
180        c.put(HTTPAUTH_USERNAME_COL, username);
181        c.put(HTTPAUTH_PASSWORD_COL, password);
182        mDatabase.insert(HTTPAUTH_TABLE_NAME, HTTPAUTH_HOST_COL, c);
183    }
184
185    /**
186     * Retrieves the HTTP authentication username and password for a given host and realm pair. If
187     * there are multiple username/password combinations for a host/realm, only the first one will
188     * be returned.
189     *
190     * @param host the host the password applies to
191     * @param realm the realm the password applies to
192     * @return a String[] if found where String[0] is username (which can be null) and
193     *         String[1] is password.  Null is returned if it can't find anything.
194     */
195    public String[] getHttpAuthUsernamePassword(String host, String realm) {
196        if (host == null || realm == null || !waitForInit()) {
197            return null;
198        }
199
200        final String[] columns = new String[] {
201            HTTPAUTH_USERNAME_COL, HTTPAUTH_PASSWORD_COL
202        };
203        final String selection = "(" + HTTPAUTH_HOST_COL + " == ?) AND " +
204                "(" + HTTPAUTH_REALM_COL + " == ?)";
205
206        String[] ret = null;
207        Cursor cursor = null;
208        try {
209            cursor = mDatabase.query(HTTPAUTH_TABLE_NAME, columns, selection,
210                    new String[] { host, realm }, null, null, null);
211            if (cursor.moveToFirst()) {
212                ret = new String[] {
213                        cursor.getString(cursor.getColumnIndex(HTTPAUTH_USERNAME_COL)),
214                        cursor.getString(cursor.getColumnIndex(HTTPAUTH_PASSWORD_COL)),
215                };
216            }
217        } catch (IllegalStateException e) {
218            Log.e(LOGTAG, "getHttpAuthUsernamePassword", e);
219        } finally {
220            if (cursor != null) cursor.close();
221        }
222        return ret;
223    }
224
225    /**
226     * Determines if there are any HTTP authentication passwords saved.
227     *
228     * @return true if there are passwords saved
229     */
230    public boolean hasHttpAuthUsernamePassword() {
231        if (!waitForInit()) {
232            return false;
233        }
234
235        Cursor cursor = null;
236        boolean ret = false;
237        try {
238            cursor = mDatabase.query(HTTPAUTH_TABLE_NAME, ID_PROJECTION, null, null, null, null,
239                    null);
240            ret = cursor.moveToFirst();
241        } catch (IllegalStateException e) {
242            Log.e(LOGTAG, "hasEntries", e);
243        } finally {
244            if (cursor != null) cursor.close();
245        }
246        return ret;
247    }
248
249    /**
250     * Clears the HTTP authentication password database.
251     */
252    public void clearHttpAuthUsernamePassword() {
253        if (!waitForInit()) {
254            return;
255        }
256        mDatabase.delete(HTTPAUTH_TABLE_NAME, null, null);
257    }
258}
259