/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ #filter substitution package @ANDROID_PACKAGE_NAME@.db; import java.io.File; import java.io.IOException; import java.lang.IllegalArgumentException; import java.util.HashMap; import java.util.ArrayList; import java.util.Random; import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoDirProvider; import org.mozilla.gecko.GeckoEvent; import org.mozilla.gecko.GeckoEventListener; import org.mozilla.gecko.db.BrowserContract.CommonColumns; import org.mozilla.gecko.db.DBUtils; import org.mozilla.gecko.db.BrowserContract.Passwords; import org.mozilla.gecko.db.BrowserContract.DeletedPasswords; import org.mozilla.gecko.db.BrowserContract.SyncColumns; import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.sqlite.SQLiteBridge; import org.mozilla.gecko.sqlite.SQLiteBridgeException; import org.mozilla.gecko.sync.Utils; import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.os.Build; import android.text.TextUtils; import android.util.Log; public class PasswordsProvider extends ContentProvider { private Context mContext = null; private static final String LOGTAG = "GeckoPasswordsProvider"; static final String DATABASE_NAME = "signons.sqlite"; static final int DATABASE_VERSION = 5; static final String TABLE_PASSWORDS = "moz_logins"; static final String TABLE_DELETED_PASSWORDS = "deleted_logins"; private static final int PASSWORDS = 500; private static final int DELETED_PASSWORDS = 502; static final String DEFAULT_PASSWORDS_SORT_ORDER = Passwords.HOSTNAME + " ASC"; static final String DEFAULT_DELETED_PASSWORDS_SORT_ORDER = DeletedPasswords.TIME_DELETED + " ASC"; private static final UriMatcher URI_MATCHER; private static HashMap PASSWORDS_PROJECTION_MAP; private static HashMap DELETED_PASSWORDS_PROJECTION_MAP; private HashMap mDatabasePerProfile; private static ArrayList mSyncColumns; static { URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH); // content://org.mozilla.gecko.providers.browser/passwords/# URI_MATCHER.addURI(BrowserContract.PASSWORDS_AUTHORITY, "passwords", PASSWORDS); PASSWORDS_PROJECTION_MAP = new HashMap(); PASSWORDS_PROJECTION_MAP.put(Passwords.ID, Passwords.ID); PASSWORDS_PROJECTION_MAP.put(Passwords.HOSTNAME, Passwords.HOSTNAME); PASSWORDS_PROJECTION_MAP.put(Passwords.HTTP_REALM, Passwords.HTTP_REALM); PASSWORDS_PROJECTION_MAP.put(Passwords.FORM_SUBMIT_URL, Passwords.FORM_SUBMIT_URL); PASSWORDS_PROJECTION_MAP.put(Passwords.USERNAME_FIELD, Passwords.USERNAME_FIELD); PASSWORDS_PROJECTION_MAP.put(Passwords.PASSWORD_FIELD, Passwords.PASSWORD_FIELD); PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_USERNAME, Passwords.ENCRYPTED_USERNAME); PASSWORDS_PROJECTION_MAP.put(Passwords.ENCRYPTED_PASSWORD, Passwords.ENCRYPTED_PASSWORD); PASSWORDS_PROJECTION_MAP.put(Passwords.GUID, Passwords.GUID); PASSWORDS_PROJECTION_MAP.put(Passwords.ENC_TYPE, Passwords.ENC_TYPE); PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_CREATED, Passwords.TIME_CREATED); PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_LAST_USED, Passwords.TIME_LAST_USED); PASSWORDS_PROJECTION_MAP.put(Passwords.TIME_PASSWORD_CHANGED, Passwords.TIME_PASSWORD_CHANGED); PASSWORDS_PROJECTION_MAP.put(Passwords.TIMES_USED, Passwords.TIMES_USED); URI_MATCHER.addURI(BrowserContract.DELETED_PASSWORDS_AUTHORITY, "deleted-passwords", DELETED_PASSWORDS); mSyncColumns = new ArrayList(); mSyncColumns.add(Passwords._ID); mSyncColumns.add(Passwords.DATE_CREATED); mSyncColumns.add(Passwords.DATE_MODIFIED); DELETED_PASSWORDS_PROJECTION_MAP = new HashMap(); DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.ID, DeletedPasswords.ID); DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.GUID, DeletedPasswords.GUID); DELETED_PASSWORDS_PROJECTION_MAP.put(DeletedPasswords.TIME_DELETED, DeletedPasswords.TIME_DELETED); } private SQLiteBridge getDB(Context context, final String databasePath) { SQLiteBridge mBridge = null; try { mBridge = new SQLiteBridge(databasePath); boolean dbNeedsSetup = true; try { int version = mBridge.getVersion(); Log.i(LOGTAG, version + " == " + DATABASE_VERSION); dbNeedsSetup = version != DATABASE_VERSION; } catch(Exception ex) { Log.e(LOGTAG, "Error getting version ", ex); // if Gecko is not running, we should bail out. Otherwise we try to // let Gecko build the database for us if (!GeckoApp.checkLaunchState(GeckoApp.LaunchState.GeckoRunning)) { mBridge = null; throw new UnsupportedOperationException("Need to launch Gecko to set password database up"); } } if (dbNeedsSetup) { Log.i(LOGTAG, "Sending init to gecko"); mBridge = null; GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Passwords:Init", databasePath)); } } catch(SQLiteBridgeException ex) { Log.e(LOGTAG, "Error getting database", ex); } return mBridge; } private SQLiteBridge getDatabaseForProfile(String profile) { // Each profile has a separate signons.sqlite database. The target // profile is provided using a URI query argument in each request // to our content provider. if (TextUtils.isEmpty(profile)) { Log.d(LOGTAG, "No profile provided, using default"); profile = BrowserContract.DEFAULT_PROFILE; } SQLiteBridge db = mDatabasePerProfile.get(profile); if (db == null) { synchronized (this) { try { db = getDB(getContext(), getDatabasePath(profile)); } catch(UnsupportedOperationException ex) { Log.i(LOGTAG, "Gecko has not set the database up yet"); return db; } mDatabasePerProfile.put(profile, db); } } Log.d(LOGTAG, "Successfully created database helper for profile: " + profile); return db; } private String getDatabasePath(String profile) { File profileDir = null; try { profileDir = GeckoDirProvider.getProfileDir(mContext, profile); } catch (IOException ex) { Log.e(LOGTAG, "Error getting profile dir", ex); } if (profileDir == null) { Log.d(LOGTAG, "Couldn't find directory for profile: " + profile); return null; } String databasePath = new File(profileDir, DATABASE_NAME).getAbsolutePath(); return databasePath; } private SQLiteBridge getDatabase(Uri uri) { String profile = null; if (uri != null) profile = uri.getQueryParameter(BrowserContract.PARAM_PROFILE); return getDatabaseForProfile(profile); } @Override public boolean onCreate() { mContext = getContext(); mDatabasePerProfile = new HashMap(); return true; } @Override public String getType(Uri uri) { final int match = URI_MATCHER.match(uri); switch (match) { case PASSWORDS: return Passwords.CONTENT_TYPE; case DELETED_PASSWORDS: return DeletedPasswords.CONTENT_TYPE; } Log.d(LOGTAG, "URI has unrecognized type: " + uri); return null; } private String getTable(Uri uri) { final int match = URI_MATCHER.match(uri); switch (match) { case DELETED_PASSWORDS: return TABLE_DELETED_PASSWORDS; case PASSWORDS: { return TABLE_PASSWORDS; } default: throw new UnsupportedOperationException("Unknown table " + uri); } } private String getSortOrder(Uri uri, String aRequested) { if (!TextUtils.isEmpty(aRequested)) return aRequested; final int match = URI_MATCHER.match(uri); switch (match) { case DELETED_PASSWORDS: return DEFAULT_DELETED_PASSWORDS_SORT_ORDER; case PASSWORDS: { return DEFAULT_PASSWORDS_SORT_ORDER; } default: throw new UnsupportedOperationException("Unknown delete URI " + uri); } } private Uri getAuthUri(Uri uri) { final int match = URI_MATCHER.match(uri); switch (match) { case DELETED_PASSWORDS: return BrowserContract.DELETED_PASSWORDS_AUTHORITY_URI; case PASSWORDS: { return BrowserContract.PASSWORDS_AUTHORITY_URI; } default: throw new UnsupportedOperationException("Unknown delete URI " + uri); } } private void setupDefaults(Uri uri, ContentValues values) throws IllegalArgumentException { int match = URI_MATCHER.match(uri); long now = System.currentTimeMillis(); switch (match) { case DELETED_PASSWORDS: values.put(DeletedPasswords.TIME_DELETED, now); // Deleted passwords must contain a guid if (!values.containsKey(Passwords.GUID)) { throw new IllegalArgumentException("Must provide a GUID for a deleted password"); } break; case PASSWORDS: { values.put(Passwords.TIME_CREATED, now); // Generate GUID for new password. Don't override specified GUIDs. if (!values.containsKey(Passwords.GUID)) { String guid = Utils.generateGuid(); values.put(Passwords.GUID, guid); } String nowString = new Long(now).toString(); DBUtils.replaceKey(values, CommonColumns._ID, Passwords.ID, ""); DBUtils.replaceKey(values, SyncColumns.DATE_CREATED, Passwords.TIME_CREATED, nowString); DBUtils.replaceKey(values, SyncColumns.DATE_MODIFIED, Passwords.TIME_PASSWORD_CHANGED, nowString); DBUtils.replaceKey(values, null, Passwords.HOSTNAME, ""); DBUtils.replaceKey(values, null, Passwords.HTTP_REALM, ""); DBUtils.replaceKey(values, null, Passwords.FORM_SUBMIT_URL, ""); DBUtils.replaceKey(values, null, Passwords.USERNAME_FIELD, ""); DBUtils.replaceKey(values, null, Passwords.PASSWORD_FIELD, ""); DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_USERNAME, ""); DBUtils.replaceKey(values, null, Passwords.ENCRYPTED_PASSWORD, ""); DBUtils.replaceKey(values, null, Passwords.ENC_TYPE, "0"); DBUtils.replaceKey(values, null, Passwords.TIME_LAST_USED, nowString); DBUtils.replaceKey(values, null, Passwords.TIME_PASSWORD_CHANGED, nowString); DBUtils.replaceKey(values, null, Passwords.TIMES_USED, "0"); break; } default: throw new UnsupportedOperationException("Unknown insert URI " + uri); } } @Override public int delete(Uri uri, String selection, String[] selectionArgs) { int deleted = 0; final SQLiteBridge db = getDatabase(uri); if (db == null) return deleted; String table = getTable(uri); if (table.equals("")) return deleted; if (selection != null) { for (String item : mSyncColumns) selection = selection.replace(item, translateColumn(item)); } try { deleted = db.delete(table, selection, selectionArgs); } catch (SQLiteBridgeException ex) { Log.e(LOGTAG, "Error deleting record", ex); } return deleted; } @Override public Uri insert(Uri uri, ContentValues values) { long id = -1; final SQLiteBridge db = getDatabase(uri); if (db == null) return null; String table = getTable(uri); if (table.equals("")) return null; try { setupDefaults(uri, values); } catch(Exception ex) { Log.e(LOGTAG, "Error setting up defaults", ex); return null; } try { id = db.insert(table, "", values); } catch(SQLiteBridgeException ex) { Log.e(LOGTAG, "Error inserting in db", ex); } return ContentUris.withAppendedId(uri, id); } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { int updated = 0; final SQLiteBridge db = getDatabase(uri); if (db == null) return updated; String table = getTable(uri); if (TextUtils.isEmpty(table)) return updated; if (selection != null) { for (String item : mSyncColumns) selection = selection.replace(item, translateColumn(item)); } try { return db.update(table, values, selection, selectionArgs); } catch(SQLiteBridgeException ex) { Log.e(LOGTAG, "Error updating table", ex); } return 0; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { Cursor cursor = null; final SQLiteBridge db = getDatabase(uri); if (db == null) return cursor; String table = getTable(uri); if (table.equals("")) return cursor; sortOrder = getSortOrder(uri, sortOrder); if (TextUtils.isEmpty(sortOrder)) return cursor; try { cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder, null); cursor.setNotificationUri(getContext().getContentResolver(), getAuthUri(uri)); } catch (SQLiteBridgeException ex) { Log.e(LOGTAG, "Error querying database", ex); } return cursor; } private String translateColumn(String column) { if (column.equals(SyncColumns.DATE_CREATED)) return Passwords.TIME_CREATED; else if (column.equals(SyncColumns.DATE_MODIFIED)) return Passwords.TIME_PASSWORD_CHANGED; else if (column.equals(CommonColumns._ID)) return Passwords.ID; else return column; } }