/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Mozilla Communicator client code. * * The Initial Developer of the Original Code is * Netscape Communications Corporation. * Portions created by the Initial Developer are Copyright (C) 1998 * the Initial Developer. All Rights Reserved. * * Contributor(s): * Joe Hewitt (Original Author) * Brett Wilson * Justin Dolske * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ #include "nsStorageFormHistory.h" #include "nsIServiceManager.h" #include "nsIObserverService.h" #include "nsICategoryManager.h" #include "nsIDirectoryService.h" #include "nsAppDirectoryServiceDefs.h" #include "nsCRT.h" #include "nsString.h" #include "nsUnicharUtils.h" #include "nsReadableUtils.h" #include "nsIDOMNode.h" #include "nsIDOMHTMLFormElement.h" #include "nsIDOMHTMLInputElement.h" #include "nsIDOMHTMLCollection.h" #include "nsIPrefService.h" #include "nsIPrefBranch.h" #include "nsIPrefBranch2.h" #include "nsCOMArray.h" #include "mozStorageHelper.h" #include "mozStorageCID.h" #include "nsIAutoCompleteSimpleResult.h" #include "nsTArray.h" // The size of the database cache. This is the number of database PAGES that // can be cached in memory. Normally, pages are 1K unless the size has been // explicitly changed. // // 4MB should be much larger than normal form histories. Normal form histories // will be several hundered KB at most. If the form history is smaller, the // extra memory will never be allocated, so there is no penalty for larger // numbers. See StartCache #define DATABASE_CACHE_PAGES 4000 #define DB_SCHEMA_VERSION 1 #define DB_FILENAME NS_LITERAL_STRING("formhistory.sqlite") #define DB_CORRUPT_FILENAME NS_LITERAL_STRING("formhistory.sqlite.corrupt") #define PR_HOURS ((PRInt64)60 * 60 * 1000000) // nsFormHistoryResult is a specialized autocomplete result class that knows // how to remove entries from the form history table. class nsFormHistoryResult : public nsIAutoCompleteSimpleResult { public: nsFormHistoryResult(const nsAString &aFieldName) : mFieldName(aFieldName) {} nsresult Init(); NS_DECL_ISUPPORTS // Forward everything except RemoveValueAt to the internal result NS_IMETHOD GetSearchString(nsAString &_result) { return mResult->GetSearchString(_result); } NS_IMETHOD GetSearchResult(PRUint16 *_result) { return mResult->GetSearchResult(_result); } NS_IMETHOD GetDefaultIndex(PRInt32 *_result) { return mResult->GetDefaultIndex(_result); } NS_IMETHOD GetErrorDescription(nsAString &_result) { return mResult->GetErrorDescription(_result); } NS_IMETHOD GetMatchCount(PRUint32 *_result) { return mResult->GetMatchCount(_result); } NS_IMETHOD GetValueAt(PRInt32 aIndex, nsAString &_result) { return mResult->GetValueAt(aIndex, _result); } NS_IMETHOD GetCommentAt(PRInt32 aIndex, nsAString &_result) { return mResult->GetCommentAt(aIndex, _result); } NS_IMETHOD GetStyleAt(PRInt32 aIndex, nsAString &_result) { return mResult->GetStyleAt(aIndex, _result); } NS_IMETHOD GetImageAt(PRInt32 aIndex, nsAString &_result) { return mResult->GetImageAt(aIndex, _result); } NS_IMETHOD RemoveValueAt(PRInt32 aRowIndex, PRBool aRemoveFromDB); NS_FORWARD_NSIAUTOCOMPLETESIMPLERESULT(mResult->) protected: nsCOMPtr mResult; nsString mFieldName; }; NS_IMPL_ISUPPORTS2(nsFormHistoryResult, nsIAutoCompleteResult, nsIAutoCompleteSimpleResult) nsresult nsFormHistoryResult::Init() { nsresult rv; mResult = do_CreateInstance(NS_AUTOCOMPLETESIMPLERESULT_CONTRACTID, &rv); return rv; } NS_IMETHODIMP nsFormHistoryResult::RemoveValueAt(PRInt32 aRowIndex, PRBool aRemoveFromDB) { if (!aRemoveFromDB) { return mResult->RemoveValueAt(aRowIndex, aRemoveFromDB); } nsAutoString value; nsresult rv = mResult->GetValueAt(aRowIndex, value); NS_ENSURE_SUCCESS(rv, rv); rv = mResult->RemoveValueAt(aRowIndex, aRemoveFromDB); NS_ENSURE_SUCCESS(rv, rv); nsFormHistory* fh = nsFormHistory::GetInstance(); NS_ENSURE_TRUE(fh, NS_ERROR_OUT_OF_MEMORY); return fh->RemoveEntry(mFieldName, value); } #define PREF_FORMFILL_BRANCH "browser.formfill." #define PREF_FORMFILL_ENABLE "enable" NS_INTERFACE_MAP_BEGIN(nsFormHistory) NS_INTERFACE_MAP_ENTRY(nsIFormHistory2) NS_INTERFACE_MAP_ENTRY(nsIFormHistoryPrivate) NS_INTERFACE_MAP_ENTRY(nsIObserver) NS_INTERFACE_MAP_ENTRY(nsIFormSubmitObserver) NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) NS_INTERFACE_MAP_END NS_IMPL_ADDREF(nsFormHistory) NS_IMPL_RELEASE(nsFormHistory) PRBool nsFormHistory::gFormHistoryEnabled = PR_FALSE; PRBool nsFormHistory::gPrefsInitialized = PR_FALSE; nsFormHistory* nsFormHistory::gFormHistory = nsnull; nsFormHistory::nsFormHistory() { NS_ASSERTION(!gFormHistory, "nsFormHistory must be used as a service"); gFormHistory = this; } nsFormHistory::~nsFormHistory() { NS_ASSERTION(gFormHistory == this, "nsFormHistory must be used as a service"); gFormHistory = nsnull; } nsresult nsFormHistory::Init() { PRBool doImport; nsresult rv = OpenDatabase(&doImport); if (rv == NS_ERROR_FILE_CORRUPTED) { /* If the DB is corrupt, nuke it and try again with a new DB. */ rv = dbCleanup(); NS_ENSURE_SUCCESS(rv, rv); rv = OpenDatabase(&doImport); doImport = PR_FALSE; } NS_ENSURE_SUCCESS(rv, rv); #ifdef MOZ_MORKREADER if (doImport) { // Locate the old formhistory.dat file and import it. nsCOMPtr historyFile; rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(historyFile)); if (NS_SUCCEEDED(rv)) { historyFile->Append(NS_LITERAL_STRING("formhistory.dat")); nsCOMPtr importer = new nsFormHistoryImporter(); NS_ENSURE_TRUE(importer, NS_ERROR_OUT_OF_MEMORY); importer->ImportFormHistory(historyFile, this); } } #endif nsCOMPtr service = do_GetService("@mozilla.org/observer-service;1"); if (service) service->AddObserver(this, NS_EARLYFORMSUBMIT_SUBJECT, PR_TRUE); return NS_OK; } /* static */ PRBool nsFormHistory::FormHistoryEnabled() { if (!gPrefsInitialized) { nsCOMPtr prefService = do_GetService(NS_PREFSERVICE_CONTRACTID); prefService->GetBranch(PREF_FORMFILL_BRANCH, getter_AddRefs(gFormHistory->mPrefBranch)); gFormHistory->mPrefBranch->GetBoolPref(PREF_FORMFILL_ENABLE, &gFormHistoryEnabled); nsCOMPtr branchInternal = do_QueryInterface(gFormHistory->mPrefBranch); branchInternal->AddObserver(PREF_FORMFILL_ENABLE, gFormHistory, PR_TRUE); gPrefsInitialized = PR_TRUE; } return gFormHistoryEnabled; } //////////////////////////////////////////////////////////////////////// //// nsIFormHistory2 NS_IMETHODIMP nsFormHistory::GetHasEntries(PRBool *aHasEntries) { mozStorageStatementScoper scope(mDBSelectEntries); PRBool hasMore; *aHasEntries = NS_SUCCEEDED(mDBSelectEntries->ExecuteStep(&hasMore)) && hasMore; return NS_OK; } NS_IMETHODIMP nsFormHistory::AddEntry(const nsAString &aName, const nsAString &aValue) { nsresult rv; if (!FormHistoryEnabled()) return NS_OK; PRInt64 existingID = GetExistingEntryID(aName, aValue); if (existingID != -1) { mozStorageStatementScoper scope(mDBUpdateEntry); // lastUsed rv = mDBUpdateEntry->BindInt64Parameter(0, PR_Now()); NS_ENSURE_SUCCESS(rv, rv); // WHERE id rv = mDBUpdateEntry->BindInt64Parameter(1, existingID); NS_ENSURE_SUCCESS(rv, rv); rv = mDBUpdateEntry->Execute(); NS_ENSURE_SUCCESS(rv, rv); } else { PRInt64 now = PR_Now(); mozStorageStatementScoper scope(mDBInsertNameValue); // fieldname rv = mDBInsertNameValue->BindStringParameter(0, aName); NS_ENSURE_SUCCESS(rv, rv); // value rv = mDBInsertNameValue->BindStringParameter(1, aValue); NS_ENSURE_SUCCESS(rv, rv); // timesUsed rv = mDBInsertNameValue->BindInt32Parameter(2, 1); NS_ENSURE_SUCCESS(rv, rv); // firstUsed rv = mDBInsertNameValue->BindInt64Parameter(3, now); NS_ENSURE_SUCCESS(rv, rv); // lastUsed rv = mDBInsertNameValue->BindInt64Parameter(4, now); NS_ENSURE_SUCCESS(rv, rv); rv = mDBInsertNameValue->Execute(); NS_ENSURE_SUCCESS(rv, rv); } return NS_OK; } /* Returns -1 if entry not found, or the ID if it was. */ PRInt64 nsFormHistory::GetExistingEntryID (const nsAString &aName, const nsAString &aValue) { mozStorageStatementScoper scope(mDBFindEntry); nsresult rv = mDBFindEntry->BindStringParameter(0, aName); NS_ENSURE_SUCCESS(rv, -1); rv = mDBFindEntry->BindStringParameter(1, aValue); NS_ENSURE_SUCCESS(rv, -1); PRBool hasMore; rv = mDBFindEntry->ExecuteStep(&hasMore); NS_ENSURE_SUCCESS(rv, -1); PRInt64 ID = -1; if (hasMore) { mDBFindEntry->GetInt64(0, &ID); NS_ENSURE_SUCCESS(rv, -1); } return ID; } NS_IMETHODIMP nsFormHistory::EntryExists(const nsAString &aName, const nsAString &aValue, PRBool *_retval) { PRInt64 ID = GetExistingEntryID(aName, aValue); *_retval = ID != -1; return NS_OK; } NS_IMETHODIMP nsFormHistory::NameExists(const nsAString &aName, PRBool *_retval) { mozStorageStatementScoper scope(mDBFindEntryByName); nsresult rv = mDBFindEntryByName->BindStringParameter(0, aName); NS_ENSURE_SUCCESS(rv, rv); PRBool hasMore; *_retval = (NS_SUCCEEDED(mDBFindEntryByName->ExecuteStep(&hasMore)) && hasMore); return NS_OK; } NS_IMETHODIMP nsFormHistory::RemoveEntry(const nsAString &aName, const nsAString &aValue) { nsCOMPtr dbDelete; nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_formhistory WHERE fieldname=?1 AND value=?2"), getter_AddRefs(dbDelete)); NS_ENSURE_SUCCESS(rv,rv); rv = dbDelete->BindStringParameter(0, aName); NS_ENSURE_SUCCESS(rv,rv); rv = dbDelete->BindStringParameter(1, aValue); NS_ENSURE_SUCCESS(rv, rv); return dbDelete->Execute(); } NS_IMETHODIMP nsFormHistory::RemoveEntriesForName(const nsAString &aName) { nsCOMPtr dbDelete; nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_formhistory WHERE fieldname=?1"), getter_AddRefs(dbDelete)); NS_ENSURE_SUCCESS(rv,rv); rv = dbDelete->BindStringParameter(0, aName); NS_ENSURE_SUCCESS(rv,rv); return dbDelete->Execute(); } NS_IMETHODIMP nsFormHistory::RemoveAllEntries() { nsCOMPtr dbDeleteAll; nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_formhistory"), getter_AddRefs(dbDeleteAll)); NS_ENSURE_SUCCESS(rv,rv); // privacy cleanup, if there's an old mork formhistory around, just delete it nsCOMPtr oldFormHistoryFile; rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(oldFormHistoryFile)); if (NS_FAILED(rv)) return rv; rv = oldFormHistoryFile->Append(NS_LITERAL_STRING("formhistory.dat")); NS_ENSURE_SUCCESS(rv, rv); PRBool fileExists; if (NS_SUCCEEDED(oldFormHistoryFile->Exists(&fileExists)) && fileExists) { rv = oldFormHistoryFile->Remove(PR_FALSE); NS_ENSURE_SUCCESS(rv, rv); } return dbDeleteAll->Execute(); } //////////////////////////////////////////////////////////////////////// //// nsIObserver NS_IMETHODIMP nsFormHistory::Observe(nsISupports *aSubject, const char *aTopic, const PRUnichar *aData) { if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) { mPrefBranch->GetBoolPref(PREF_FORMFILL_ENABLE, &gFormHistoryEnabled); } return NS_OK; } //////////////////////////////////////////////////////////////////////// //// nsIFormSubmitObserver NS_IMETHODIMP nsFormHistory::Notify(nsIDOMHTMLFormElement* formElt, nsIDOMWindowInternal* aWindow, nsIURI* aActionURL, PRBool* aCancelSubmit) { if (!FormHistoryEnabled()) return NS_OK; NS_NAMED_LITERAL_STRING(kAutoComplete, "autocomplete"); nsAutoString autocomplete; formElt->GetAttribute(kAutoComplete, autocomplete); if (autocomplete.LowerCaseEqualsLiteral("off")) return NS_OK; nsCOMPtr elts; formElt->GetElements(getter_AddRefs(elts)); PRUint32 length; elts->GetLength(&length); if (length == 0) return NS_OK; mozStorageTransaction transaction(mDBConn, PR_FALSE); for (PRUint32 i = 0; i < length; ++i) { nsCOMPtr node; elts->Item(i, getter_AddRefs(node)); nsCOMPtr inputElt = do_QueryInterface(node); if (inputElt) { // Filter only inputs that are of type "text" without autocomplete="off" nsAutoString type; inputElt->GetType(type); if (!type.LowerCaseEqualsLiteral("text")) continue; // TODO: If Login Manager marked this input, don't save it. The login // manager will deal with remembering it. nsAutoString autocomplete; inputElt->GetAttribute(kAutoComplete, autocomplete); if (!autocomplete.LowerCaseEqualsLiteral("off")) { // If this input has a name/id and value, add it to the database nsAutoString value; inputElt->GetValue(value); if (!value.IsEmpty()) { nsAutoString name; inputElt->GetName(name); if (name.IsEmpty()) inputElt->GetId(name); if (!name.IsEmpty()) AddEntry(name, value); } } } } return transaction.Commit(); } nsresult nsFormHistory::CreateTable() { nsresult rv; // When adding new columns, also update dbAreExpectedColumnsPresent()! rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE moz_formhistory (" "id INTEGER PRIMARY KEY, fieldname TEXT NOT NULL, " "value TEXT NOT NULL, timesUsed INTEGER, " "firstUsed INTEGER, lastUsed INTEGER)")); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("CREATE INDEX moz_formhistory_index ON moz_formhistory (fieldname)")); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->SetSchemaVersion(DB_SCHEMA_VERSION); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult nsFormHistory::CreateStatements() { nsresult rv; rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT * FROM moz_formhistory"), getter_AddRefs(mDBSelectEntries)); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT id FROM moz_formhistory WHERE fieldname=?1 AND value=?2"), getter_AddRefs(mDBFindEntry)); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT * FROM moz_formhistory WHERE fieldname=?1"), getter_AddRefs(mDBFindEntryByName)); NS_ENSURE_SUCCESS(rv,rv); rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT value FROM moz_formhistory WHERE fieldname=?1 ORDER BY UPPER(value) ASC"), getter_AddRefs(mDBGetMatchingField)); NS_ENSURE_SUCCESS(rv,rv); rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "INSERT INTO moz_formhistory (fieldname, value, timesUsed, " "firstUsed, lastUsed) VALUES (?1, ?2, ?3, ?4, ?5)"), getter_AddRefs(mDBInsertNameValue)); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "UPDATE moz_formhistory " "SET timesUsed=timesUsed + 1, lastUsed=?1 " "WHERE id = ?2"), getter_AddRefs(mDBUpdateEntry)); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult nsFormHistory::OpenDatabase(PRBool *aDoImport) { // init DB service and obtain a connection nsresult rv; mStorageService = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr formHistoryFile; rv = GetDatabaseFile(getter_AddRefs(formHistoryFile)); NS_ENSURE_SUCCESS(rv, rv); rv = mStorageService->OpenDatabase(formHistoryFile, getter_AddRefs(mDBConn)); NS_ENSURE_SUCCESS(rv, rv); // We execute many statements before the database cache is started to create // the tables (which can not be done which the cache is locked in memory by // the dummy statement--see StartCache). This transaction will keep the cache // between these statements, which should improve startup performance because // we won't have to keep requesting pages from the OS. // We also want the transaction to rollback any failed schema upgrade. mozStorageTransaction transaction(mDBConn, PR_FALSE); PRBool exists; mDBConn->TableExists(NS_LITERAL_CSTRING("moz_formhistory"), &exists); if (!exists) { *aDoImport = PR_TRUE; rv = CreateTable(); NS_ENSURE_SUCCESS(rv, rv); } else { *aDoImport = PR_FALSE; } // Ensure DB is at the current schema. rv = dbMigrate(); NS_ENSURE_SUCCESS(rv, rv); // should commit before starting cache transaction.Commit(); // ignore errors since the cache is not critical for operation StartCache(); rv = CreateStatements(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /* * dbMigrate */ nsresult nsFormHistory::dbMigrate() { PRInt32 schemaVersion; nsresult rv = mDBConn->GetSchemaVersion(&schemaVersion); NS_ENSURE_SUCCESS(rv, rv); // Upgrade from the version in the DB to the version we expect, step by // step. Note that the formhistory.sqlite in Firefox 3 didn't have a // schema version set, so we start at 0. switch (schemaVersion) { case 0: rv = MigrateToVersion1(); NS_ENSURE_SUCCESS(rv, rv); // (fallthrough to the next upgrade) case DB_SCHEMA_VERSION: // (current version, nothing more to do) break; // Unknown schema version, it's probably a DB modified by some future // version of this code. Try to use the DB anyway. default: // Sanity check: make sure the columns we expect are present. If not, // treat it as corrupt (back it up, nuke it, start from scratch). if(!dbAreExpectedColumnsPresent()) return NS_ERROR_FILE_CORRUPTED; // If it's ok, downgrade the schema version so the future version will // know to re-upgrade the DB. rv = mDBConn->SetSchemaVersion(DB_SCHEMA_VERSION); NS_ENSURE_SUCCESS(rv, rv); break; } return NS_OK; } /* * MigrateToVersion1 * * Updates the DB schema to v1 (bug 463154). * Adds firstUsed, lastUsed, timesUsed columns. */ nsresult nsFormHistory::MigrateToVersion1() { // Check to see if the new columns already exist (could be a v1 DB that // was downgraded to v0). If they exist, we don't need to add them. nsCOMPtr stmt; nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT timesUsed, firstUsed, lastUsed FROM moz_formhistory"), getter_AddRefs(stmt)); PRBool columnsExist = !!NS_SUCCEEDED(rv); if (!columnsExist) { rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_formhistory ADD COLUMN timesUsed INTEGER")); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_formhistory ADD COLUMN firstUsed INTEGER")); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "ALTER TABLE moz_formhistory ADD COLUMN lastUsed INTEGER")); NS_ENSURE_SUCCESS(rv, rv); } // Set the default values for the new columns. // // Note that we set the timestamps to 24 hours in the past. We want a // timestamp that's recent (so that "keep form history for 90 days" // doesn't expire things surprisingly soon), but not so recent that // "forget the last hour of stuff" deletes all freshly migrated data. nsCOMPtr addDefaultValues; rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "UPDATE moz_formhistory " "SET timesUsed=1, firstUsed=?1, lastUsed=?1 " "WHERE timesUsed isnull OR firstUsed isnull OR lastUsed isnull"), getter_AddRefs(addDefaultValues)); rv = addDefaultValues->BindInt64Parameter(0, PR_Now() - 24 * PR_HOURS); NS_ENSURE_SUCCESS(rv, rv); rv = addDefaultValues->Execute(); NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->SetSchemaVersion(1); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } nsresult nsFormHistory::GetDatabaseFile(nsIFile** aFile) { nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, aFile); NS_ENSURE_SUCCESS(rv, rv); return (*aFile)->Append(DB_FILENAME); } /* * dbCleanup * * Called when a DB is corrupt. We back it up to a .corrupt file, and then * nuke it to start from scratch. */ nsresult nsFormHistory::dbCleanup() { nsCOMPtr dbFile; nsresult rv = GetDatabaseFile(getter_AddRefs(dbFile)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr backupFile; NS_ENSURE_TRUE(mStorageService, NS_ERROR_NOT_AVAILABLE); rv = mStorageService->BackupDatabaseFile(dbFile, DB_CORRUPT_FILENAME, nsnull, getter_AddRefs(backupFile)); NS_ENSURE_SUCCESS(rv, rv); if (mDBConn) mDBConn->Close(); rv = dbFile->Remove(PR_FALSE); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } /* * dbAreExpectedColumnsPresent */ PRBool nsFormHistory::dbAreExpectedColumnsPresent() { // If the statement succeeds, all the columns are there. nsCOMPtr stmt; nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "SELECT fieldname, value, timesUsed, firstUsed, lastUsed " "FROM moz_formhistory"), getter_AddRefs(stmt)); return NS_SUCCEEDED(rv) ? PR_TRUE : PR_FALSE; } // nsFormHistory::StartCache // // This function starts the dummy statement that locks the cache in memory. // As long as there is an open connection sharing the same cache, the cache // won't be expired. Therefore, we create a dummy table with some data in // it, and open a statement over the data. As long as this statement is // open, we can go fast. // // This dummy statement prevents the schema from being modified. If you // want to add or change a table or index schema, you must stop the dummy // statement first. See nsNavHistory::StartDummyStatement for a slightly // more detailed discussion. // // Note that we should not use a transaction in this function since that // will commit the dummy statement and everything will break. // // This function also initializes the cache. nsresult nsFormHistory::StartCache() { // do nothing if the dummy statement is already running if (mDummyStatement) return NS_OK; // dummy database connection nsCOMPtr formHistoryFile; nsresult rv = GetDatabaseFile(getter_AddRefs(formHistoryFile)); NS_ENSURE_SUCCESS(rv, rv); rv = mStorageService->OpenDatabase(formHistoryFile, getter_AddRefs(mDummyConnection)); NS_ENSURE_SUCCESS(rv, rv); // Make sure the dummy table exists PRBool tableExists; rv = mDummyConnection->TableExists(NS_LITERAL_CSTRING("moz_dummy_table"), &tableExists); NS_ENSURE_SUCCESS(rv, rv); if (! tableExists) { rv = mDummyConnection->ExecuteSimpleSQL( NS_LITERAL_CSTRING("CREATE TABLE moz_dummy_table (id INTEGER PRIMARY KEY)")); NS_ENSURE_SUCCESS(rv, rv); } // This table is guaranteed to have something in it and will keep the dummy // statement open. If the table is empty, it won't hold the statement open. // the PRIMARY KEY value on ID means that it is unique. The OR IGNORE means // that if there is already a value of 1 there, this insert will be ignored, // which is what we want so as to avoid growing the table infinitely. rv = mDummyConnection->ExecuteSimpleSQL( NS_LITERAL_CSTRING("INSERT OR IGNORE INTO moz_dummy_table VALUES (1)")); NS_ENSURE_SUCCESS(rv, rv); rv = mDummyConnection->CreateStatement(NS_LITERAL_CSTRING( "SELECT id FROM moz_dummy_table LIMIT 1"), getter_AddRefs(mDummyStatement)); NS_ENSURE_SUCCESS(rv, rv); // we have to step the dummy statement so that it will hold a lock on the DB PRBool dummyHasResults; rv = mDummyStatement->ExecuteStep(&dummyHasResults); NS_ENSURE_SUCCESS(rv, rv); // Set the cache size nsCAutoString cacheSizePragma("PRAGMA cache_size="); cacheSizePragma.AppendInt(DATABASE_CACHE_PAGES); rv = mDummyConnection->ExecuteSimpleSQL(cacheSizePragma); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } // nsFormHistory::StopCache // // Call this before doing any schema modifying operations. You should // start the dummy statement again to give good performance. // See StartCache. nsresult nsFormHistory::StopCache() { // do nothing if the dummy statement isn't running if (! mDummyStatement) return NS_OK; nsresult rv = mDummyStatement->Reset(); NS_ENSURE_SUCCESS(rv, rv); mDummyStatement = nsnull; return NS_OK; } nsresult nsFormHistory::AutoCompleteSearch(const nsAString &aInputName, const nsAString &aInputValue, nsIAutoCompleteSimpleResult *aPrevResult, nsIAutoCompleteResult **aResult) { if (!FormHistoryEnabled()) return NS_OK; nsCOMPtr result; if (aPrevResult) { result = aPrevResult; PRUint32 matchCount; result->GetMatchCount(&matchCount); for (PRInt32 i = matchCount - 1; i >= 0; --i) { nsAutoString match; result->GetValueAt(i, match); if (!StringBeginsWith(match, aInputValue, nsCaseInsensitiveStringComparator())) { result->RemoveValueAt(i, PR_FALSE); } } } else { nsCOMPtr fhResult = new nsFormHistoryResult(aInputName); NS_ENSURE_TRUE(fhResult, NS_ERROR_OUT_OF_MEMORY); nsresult rv = fhResult->Init(); NS_ENSURE_SUCCESS(rv, rv); reinterpret_cast*>(&fhResult)->swap(result); result->SetSearchString(aInputValue); // generates query string mozStorageStatementScoper scope(mDBGetMatchingField); rv = mDBGetMatchingField->BindStringParameter(0, aInputName); NS_ENSURE_SUCCESS(rv,rv); PRBool hasMore = PR_FALSE; PRUint32 count = 0; while (NS_SUCCEEDED(mDBGetMatchingField->ExecuteStep(&hasMore)) && hasMore) { nsAutoString entryString; mDBGetMatchingField->GetString(0, entryString); // filters out irrelevant results if(StringBeginsWith(entryString, aInputValue, nsCaseInsensitiveStringComparator())) { result->AppendMatch(entryString, EmptyString(), EmptyString(), EmptyString()); ++count; } } if (count > 0) { result->SetSearchResult(nsIAutoCompleteResult::RESULT_SUCCESS); result->SetDefaultIndex(0); } else { result->SetSearchResult(nsIAutoCompleteResult::RESULT_NOMATCH); result->SetDefaultIndex(-1); } } *aResult = result; NS_IF_ADDREF(*aResult); return NS_OK; } #ifdef MOZ_MORKREADER // Columns for form history rows enum { kNameColumn, kValueColumn, kColumnCount // keep me last }; static const char * const gColumnNames[] = { "Name", "Value" }; struct FormHistoryImportClosure { FormHistoryImportClosure(nsMorkReader *aReader, nsIFormHistory2 *aFormHistory) : reader(aReader), formHistory(aFormHistory), byteOrderColumn(-1), swapBytes(PR_FALSE) { for (PRUint32 i = 0; i < kColumnCount; ++i) { columnIndexes[i] = -1; } } // Back pointers to the reader and history we're operating on const nsMorkReader *reader; nsIFormHistory2 *formHistory; // Indexes of the columns that we care about PRInt32 columnIndexes[kColumnCount]; PRInt32 byteOrderColumn; // Whether we need to swap bytes (file format is other-endian) PRPackedBool swapBytes; }; // Reverses the high and low bytes in a PRUnichar buffer. // This is used if the file format has a different endianness from the // current architecture. static void SwapBytes(PRUnichar* aBuffer) { for (PRUnichar *b = aBuffer; *b; b++) { PRUnichar c = *b; *b = (0xff & (c >> 8)) | (c << 8); } } // Enumerator callback to add an entry to the FormHistory /* static */ PLDHashOperator PR_CALLBACK nsFormHistoryImporter::AddToFormHistoryCB(const nsCSubstring &aRowID, const nsTArray *aValues, void *aData) { FormHistoryImportClosure *data = static_cast (aData); const nsMorkReader *reader = data->reader; nsCString values[kColumnCount]; const PRUnichar* valueStrings[kColumnCount]; PRUint32 valueLengths[kColumnCount]; const PRInt32 *columnIndexes = data->columnIndexes; PRInt32 i; // Values are in UTF16. for (i = 0; i < kColumnCount; ++i) { if (columnIndexes[i] == -1) { // We didn't find this column in the map continue; } values[i] = (*aValues)[columnIndexes[i]]; reader->NormalizeValue(values[i]); PRUint32 length; const char *bytes; if (values[i].IsEmpty()) { bytes = "\0"; length = 0; } else { length = values[i].Length() / 2; // add an extra null byte onto the end, so that the buffer ends // with a complete unicode null character. values[i].Append('\0'); // Swap the bytes in the unicode characters if necessary. if (data->swapBytes) { SwapBytes(reinterpret_cast(values[i].BeginWriting())); } bytes = values[i].get(); } valueStrings[i] = reinterpret_cast(bytes); valueLengths[i] = length; } data->formHistory->AddEntry(nsDependentString(valueStrings[kNameColumn], valueLengths[kNameColumn]), nsDependentString(valueStrings[kValueColumn], valueLengths[kValueColumn])); return PL_DHASH_NEXT; } NS_IMPL_ISUPPORTS1(nsFormHistoryImporter, nsIFormHistoryImporter) NS_IMETHODIMP nsFormHistoryImporter::ImportFormHistory(nsIFile *aFile, nsIFormHistory2 *aFormHistory) { // Check that the file exists before we try to open it PRBool exists; aFile->Exists(&exists); if (!exists) { return NS_OK; } nsMorkReader reader; nsresult rv = reader.Init(); NS_ENSURE_SUCCESS(rv, rv); rv = reader.Read(aFile); NS_ENSURE_SUCCESS(rv, rv); // Gather up the column ids so we don't need to find them on each row FormHistoryImportClosure data(&reader, aFormHistory); const nsTArray columns = reader.GetColumns(); for (PRUint32 i = 0; i < columns.Length(); ++i) { const nsCSubstring &name = columns[i].name; for (PRUint32 j = 0; j < kColumnCount; ++j) { if (name.Equals(gColumnNames[j])) { data.columnIndexes[j] = i; break; } } if (name.EqualsLiteral("ByteOrder")) { data.byteOrderColumn = i; } } // Determine the byte order from the table's meta-row. const nsTArray *metaRow = reader.GetMetaRow(); if (metaRow && data.byteOrderColumn != -1) { const nsCString &byteOrder = (*metaRow)[data.byteOrderColumn]; // Note whether the file uses a non-native byte ordering. // If it does, we'll have to swap bytes for PRUnichar values. // "BBBB" and "llll" are the only recognized values, anything // else is garbage and the file will be treated as native-endian // (no swapping). nsCAutoString byteOrderValue(byteOrder); reader.NormalizeValue(byteOrderValue); #ifdef IS_LITTLE_ENDIAN data.swapBytes = byteOrderValue.EqualsLiteral("BBBB"); #else data.swapBytes = byteOrderValue.EqualsLiteral("llll"); #endif } #if defined(XP_MACOSX) && defined(IS_LITTLE_ENDIAN) // The meta row and its ByteOrder field was introduced in 1.8.0.2. // If it's not present, treat the formhistory db as using native byte // ordering (as was done prior to 1.8.0.2). // Exception: the ByteOrder field was always present since the initial // x86 Mac release, so if we're on one of those, and the file doesn't // have a ByteOrder field, it most likely came from a ppc Mac and needs // its bytes swapped. nsFormHistory in 1.8.0.2 swapped the bytes, this // importer should behave the same way. else { data.swapBytes = PR_TRUE; } #endif // Add the rows to form history nsCOMPtr fhPrivate = do_QueryInterface(aFormHistory); NS_ENSURE_TRUE(fhPrivate, NS_ERROR_FAILURE); mozIStorageConnection *conn = fhPrivate->GetStorageConnection(); NS_ENSURE_TRUE(conn, NS_ERROR_NOT_INITIALIZED); mozStorageTransaction transaction(conn, PR_FALSE); reader.EnumerateRows(AddToFormHistoryCB, &data); return transaction.Commit(); } #endif