diff --git a/toolkit/components/satchel/src/nsStorageFormHistory.cpp b/toolkit/components/satchel/src/nsStorageFormHistory.cpp index 1b0cd21c1087..e9f6dca18e29 100644 --- a/toolkit/components/satchel/src/nsStorageFormHistory.cpp +++ b/toolkit/components/satchel/src/nsStorageFormHistory.cpp @@ -54,6 +54,7 @@ #include "nsString.h" #include "nsUnicharUtils.h" #include "nsReadableUtils.h" +#include "nsISupportsPrimitives.h" #include "nsIDOMNode.h" #include "nsIDOMHTMLFormElement.h" #include "nsIDOMHTMLInputElement.h" @@ -68,6 +69,7 @@ #include "mozStorageHelper.h" #include "mozStorageCID.h" #include "nsTArray.h" +#include "nsIMutableArray.h" #include "nsIPrivateBrowsingService.h" #include "nsNetCID.h" @@ -139,11 +141,11 @@ nsFormHistory::Init() } NS_ENSURE_SUCCESS(rv, rv); - nsCOMPtr service = do_GetService("@mozilla.org/observer-service;1"); - if (service) { - service->AddObserver(this, NS_EARLYFORMSUBMIT_SUBJECT, PR_TRUE); - service->AddObserver(this, "idle-daily", PR_TRUE); - service->AddObserver(this, "formhistory-expire-now", PR_TRUE); + mObserverService = do_GetService("@mozilla.org/observer-service;1"); + if (mObserverService) { + mObserverService->AddObserver(this, NS_EARLYFORMSUBMIT_SUBJECT, PR_TRUE); + mObserverService->AddObserver(this, "idle-daily", PR_TRUE); + mObserverService->AddObserver(this, "formhistory-expire-now", PR_TRUE); } return NS_OK; @@ -236,7 +238,8 @@ nsFormHistory::AddEntry(const nsAString &aName, const nsAString &aValue) if (!FormHistoryEnabled()) return NS_OK; - PRInt64 existingID = GetExistingEntryID(aName, aValue); + nsAutoString existingGUID; + PRInt64 existingID = GetExistingEntryID(aName, aValue, existingGUID); if (existingID != -1) { mozStorageStatementScoper scope(mDBUpdateEntry); @@ -249,6 +252,8 @@ nsFormHistory::AddEntry(const nsAString &aName, const nsAString &aValue) rv = mDBUpdateEntry->Execute(); NS_ENSURE_SUCCESS(rv, rv); + + SendNotification(NS_LITERAL_STRING("modifyEntry"), aName, aValue, existingGUID); } else { nsCAutoString guid; rv = GenerateGUID(guid); @@ -278,6 +283,8 @@ nsFormHistory::AddEntry(const nsAString &aName, const nsAString &aValue) rv = mDBInsertNameValue->Execute(); NS_ENSURE_SUCCESS(rv, rv); + + SendNotification(NS_LITERAL_STRING("addEntry"), aName, aValue, NS_ConvertUTF8toUTF16(guid)); } return NS_OK; } @@ -285,7 +292,8 @@ nsFormHistory::AddEntry(const nsAString &aName, const nsAString &aValue) /* Returns -1 if entry not found, or the ID if it was. */ PRInt64 nsFormHistory::GetExistingEntryID (const nsAString &aName, - const nsAString &aValue) + const nsAString &aValue, + nsAString &aGuid) { mozStorageStatementScoper scope(mDBFindEntry); @@ -299,15 +307,28 @@ nsFormHistory::GetExistingEntryID (const nsAString &aName, rv = mDBFindEntry->ExecuteStep(&hasMore); NS_ENSURE_SUCCESS(rv, -1); + nsCAutoString guid; PRInt64 ID = -1; if (hasMore) { - mDBFindEntry->GetInt64(0, &ID); + rv = mDBFindEntry->GetInt64(0, &ID); NS_ENSURE_SUCCESS(rv, -1); + rv = mDBFindEntry->GetUTF8String(1, guid); + NS_ENSURE_SUCCESS(rv, -1); + CopyUTF8toUTF16(guid, aGuid); } return ID; } +/* Returns -1 if entry not found, or the ID if it was. */ +PRInt64 +nsFormHistory::GetExistingEntryID (const nsAString &aName, + const nsAString &aValue) +{ + nsString guid; + return GetExistingEntryID(aName, aValue, guid); +} + NS_IMETHODIMP nsFormHistory::EntryExists(const nsAString &aName, const nsAString &aValue, PRBool *_retval) @@ -334,23 +355,32 @@ nsFormHistory::NameExists(const nsAString &aName, PRBool *_retval) NS_IMETHODIMP nsFormHistory::RemoveEntry(const nsAString &aName, const nsAString &aValue) { + nsAutoString existingGUID; + PRInt64 existingID = GetExistingEntryID(aName, aValue, existingGUID); + + SendNotification(NS_LITERAL_STRING("before-removeEntry"), aName, aValue, existingGUID); + nsCOMPtr dbDelete; - nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_formhistory WHERE fieldname=?1 AND value=?2"), + nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_formhistory WHERE id=?1"), getter_AddRefs(dbDelete)); NS_ENSURE_SUCCESS(rv,rv); - rv = dbDelete->BindStringParameter(0, aName); + rv = dbDelete->BindInt64Parameter(0, existingID); NS_ENSURE_SUCCESS(rv,rv); - rv = dbDelete->BindStringParameter(1, aValue); + rv = dbDelete->Execute(); NS_ENSURE_SUCCESS(rv, rv); - return dbDelete->Execute(); + SendNotification(NS_LITERAL_STRING("removeEntry"), aName, aValue, existingGUID); + + return NS_OK; } NS_IMETHODIMP nsFormHistory::RemoveEntriesForName(const nsAString &aName) { + SendNotification(NS_LITERAL_STRING("before-removeEntriesForName"), aName); + nsCOMPtr dbDelete; nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_formhistory WHERE fieldname=?1"), getter_AddRefs(dbDelete)); @@ -359,12 +389,19 @@ nsFormHistory::RemoveEntriesForName(const nsAString &aName) rv = dbDelete->BindStringParameter(0, aName); NS_ENSURE_SUCCESS(rv,rv); - return dbDelete->Execute(); + rv = dbDelete->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + SendNotification(NS_LITERAL_STRING("removeEntriesForName"), aName); + + return NS_OK; } NS_IMETHODIMP nsFormHistory::RemoveAllEntries() { + SendNotification(NS_LITERAL_STRING("before-removeAllEntries"), (nsISupports*)nsnull); + nsCOMPtr dbDeleteAll; nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING("DELETE FROM moz_formhistory"), getter_AddRefs(dbDeleteAll)); @@ -385,13 +422,20 @@ nsFormHistory::RemoveAllEntries() NS_ENSURE_SUCCESS(rv, rv); } - return dbDeleteAll->Execute(); + rv = dbDeleteAll->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + SendNotification(NS_LITERAL_STRING("removeAllEntries"), (nsISupports*)nsnull); + + return NS_OK; } NS_IMETHODIMP nsFormHistory::RemoveEntriesByTimeframe(PRInt64 aStartTime, PRInt64 aEndTime) { + SendNotification(NS_LITERAL_STRING("before-removeEntriesByTimeframe"), aStartTime, aEndTime); + nsCOMPtr stmt; nsresult rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( "DELETE FROM moz_formhistory " @@ -407,6 +451,8 @@ nsFormHistory::RemoveEntriesByTimeframe(PRInt64 aStartTime, PRInt64 aEndTime) rv = stmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); + SendNotification(NS_LITERAL_STRING("removeEntriesByTimeframe"), aStartTime, aEndTime); + return NS_OK; } @@ -573,6 +619,7 @@ nsFormHistory::ExpireOldEntries() expireDays = DEFAULT_EXPIRE_DAYS; PRInt64 expireTime = PR_Now() - expireDays * 24 * PR_HOURS; + SendNotification(NS_LITERAL_STRING("before-expireOldEntries"), expireTime); PRInt32 beginningCount = CountAllEntries(); @@ -598,6 +645,8 @@ nsFormHistory::ExpireOldEntries() NS_ENSURE_SUCCESS(rv, rv); } + SendNotification(NS_LITERAL_STRING("expireOldEntries"), expireTime); + return NS_OK; } @@ -654,7 +703,7 @@ nsFormHistory::CreateStatements() NS_ENSURE_SUCCESS(rv, rv); rv = mDBConn->CreateStatement(NS_LITERAL_CSTRING( - "SELECT id FROM moz_formhistory WHERE fieldname=?1 AND value=?2"), + "SELECT id, guid FROM moz_formhistory WHERE fieldname=?1 AND value=?2"), getter_AddRefs(mDBFindEntry)); NS_ENSURE_SUCCESS(rv, rv); @@ -964,3 +1013,140 @@ nsFormHistory::dbAreExpectedColumnsPresent() "FROM moz_formhistory"), getter_AddRefs(stmt)); return NS_SUCCEEDED(rv) ? PR_TRUE : PR_FALSE; } + +/* + * Send a notification when stored data is changed + */ +nsresult +nsFormHistory::SendNotification(const nsAString &aChangeType, nsISupports *aData) +{ + return mObserverService->NotifyObservers(aData, + "satchel-storage-changed", + PromiseFlatString(aChangeType).get()); +} + +/* + * Send a notification with a field name + */ +nsresult +nsFormHistory::SendNotification(const nsAString &aChangeType, + const nsAString &aName) +{ + nsresult rv; + + nsCOMPtr fieldName = do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); + if (!fieldName) + return NS_ERROR_OUT_OF_MEMORY; + + fieldName->SetData(aName); + + rv = SendNotification(aChangeType, fieldName); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +/* + * Send a notification with a name and value entry + */ +nsresult +nsFormHistory::SendNotification(const nsAString &aChangeType, + const nsAString &aName, + const nsAString &aValue, + const nsAutoString &aGuid) +{ + nsresult rv; + + nsCOMPtr fieldName = do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); + if (!fieldName) + return NS_ERROR_OUT_OF_MEMORY; + + fieldName->SetData(aName); + + nsCOMPtr fieldValue = do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); + if (!fieldValue) + return NS_ERROR_OUT_OF_MEMORY; + + fieldValue->SetData(aValue); + + nsCOMPtr guid = do_CreateInstance(NS_SUPPORTS_STRING_CONTRACTID); + if (!guid) + return NS_ERROR_OUT_OF_MEMORY; + guid->SetData(aGuid); + + + nsCOMPtr notifyData = do_CreateInstance(NS_ARRAY_CONTRACTID); + if (!notifyData) + return rv; + rv = notifyData->AppendElement(fieldName, PR_FALSE); + NS_ENSURE_SUCCESS(rv, rv); + + rv = notifyData->AppendElement(fieldValue, PR_FALSE); + NS_ENSURE_SUCCESS(rv, rv); + + rv = notifyData->AppendElement(guid, PR_FALSE); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SendNotification(aChangeType, notifyData); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +/* + * Send a notification with a PRInt64 + */ +nsresult +nsFormHistory::SendNotification(const nsAString &aChangeType, + const PRInt64 &aNumber) +{ + nsresult rv; + + nsCOMPtr valOne = do_CreateInstance(NS_SUPPORTS_PRINT64_CONTRACTID); + if (!valOne) + return NS_ERROR_OUT_OF_MEMORY; + + valOne->SetData(aNumber); + + rv = SendNotification(aChangeType, valOne); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +/* + * Send a notification with an array of 2 PRInt64 entries + */ +nsresult +nsFormHistory::SendNotification(const nsAString &aChangeType, + const PRInt64 &aOne, + const PRInt64 &aTwo) +{ + nsresult rv; + + nsCOMPtr valOne = do_CreateInstance(NS_SUPPORTS_PRINT64_CONTRACTID); + if (!valOne) + return NS_ERROR_OUT_OF_MEMORY; + + valOne->SetData(aOne); + + nsCOMPtr valTwo = do_CreateInstance(NS_SUPPORTS_PRINT64_CONTRACTID); + if (!valTwo) + return NS_ERROR_OUT_OF_MEMORY; + + valTwo->SetData(aTwo); + + nsCOMPtr notifyData = do_CreateInstance(NS_ARRAY_CONTRACTID); + if (!notifyData) + return rv; + rv = notifyData->AppendElement(valOne, PR_FALSE); + NS_ENSURE_SUCCESS(rv, rv); + + rv = notifyData->AppendElement(valTwo, PR_FALSE); + NS_ENSURE_SUCCESS(rv, rv); + + rv = SendNotification(aChangeType, notifyData); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} diff --git a/toolkit/components/satchel/src/nsStorageFormHistory.h b/toolkit/components/satchel/src/nsStorageFormHistory.h index 2ca5cd83676a..c3f487701baf 100644 --- a/toolkit/components/satchel/src/nsStorageFormHistory.h +++ b/toolkit/components/satchel/src/nsStorageFormHistory.h @@ -47,6 +47,7 @@ #include "nsIPrefBranch.h" #include "nsIUUIDGenerator.h" #include "nsWeakReference.h" +#include "nsIMutableArray.h" #include "mozIStorageService.h" #include "mozIStorageConnection.h" @@ -58,6 +59,7 @@ class nsIAutoCompleteSimpleResult; class nsIAutoCompleteResult; class nsFormHistory; +class nsIObserverService; template class nsTArray; #define NS_IFORMHISTORYPRIVATE_IID \ @@ -124,10 +126,18 @@ public: nsresult GenerateGUID(nsACString &guid); nsresult ExpireOldEntries(); PRInt32 CountAllEntries(); - PRInt64 GetExistingEntryID(const nsAString &aName, const nsAString &aValue); + PRInt64 GetExistingEntryID (const nsAString &aName, const nsAString &aValue); + PRInt64 GetExistingEntryID (const nsAString &aName, const nsAString &aValue, nsAString &aGuid); + + nsresult SendNotification(const nsAString &aChangeType, nsISupports *aData); + nsresult SendNotification(const nsAString &aChangeType, const nsAString &aName); + nsresult SendNotification(const nsAString &aChangeType, const nsAString &aName, const nsAString &aValue, const nsAutoString &aGuid); + nsresult SendNotification(const nsAString &aChangeType, const PRInt64 &aNumber); + nsresult SendNotification(const nsAString &aChangeType, const PRInt64 &aOne, const PRInt64 &aTwo); nsCOMPtr mUUIDService; nsCOMPtr mPrefBranch; + nsCOMPtr mObserverService; nsCOMPtr mStorageService; nsCOMPtr mDBFindEntry; nsCOMPtr mDBFindEntryByName; diff --git a/toolkit/components/satchel/test/unit/test_notify.js b/toolkit/components/satchel/test/unit/test_notify.js new file mode 100644 index 000000000000..4e93eee2cbe9 --- /dev/null +++ b/toolkit/components/satchel/test/unit/test_notify.js @@ -0,0 +1,214 @@ +/* + * Test suite for satchel notifications + * + * Tests notifications dispatched when modifying form history. + * + */ + +Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + +var expectedNotification; +var expectedBeforeNotification = null; +var expectedData; + +var TestObserver = { + QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), + + observe : function (subject, topic, data) { + do_check_eq(topic, "satchel-storage-changed"); + + // ensure that the "before-" notification comes before the other + dump(expectedBeforeNotification + " : " + expectedNotification + "\n"); + if (!expectedBeforeNotification) + do_check_eq(data, expectedNotification); + else + do_check_eq(data, expectedBeforeNotification); + + switch (data) { + case "addEntry": + do_check_true(subject instanceof Ci.nsIMutableArray); + do_check_eq(expectedData[0], subject.queryElementAt(0, Ci.nsISupportsString)); + do_check_eq(expectedData[1], subject.queryElementAt(1, Ci.nsISupportsString)); + do_check_true(isGUID.test(subject.queryElementAt(2, Ci.nsISupportsString).toString())); + break; + case "modifyEntry": + do_check_true(subject instanceof Ci.nsIMutableArray); + do_check_eq(expectedData[0], subject.queryElementAt(0, Ci.nsISupportsString)); + do_check_eq(expectedData[1], subject.queryElementAt(1, Ci.nsISupportsString)); + do_check_true(isGUID.test(subject.queryElementAt(2, Ci.nsISupportsString).toString())); + break; + case "before-removeEntry": + case "removeEntry": + do_check_true(subject instanceof Ci.nsIMutableArray); + do_check_eq(expectedData[0], subject.queryElementAt(0, Ci.nsISupportsString)); + do_check_eq(expectedData[1], subject.queryElementAt(1, Ci.nsISupportsString)); + do_check_true(isGUID.test(subject.queryElementAt(2, Ci.nsISupportsString).toString())); + break; + case "before-removeAllEntries": + case "removeAllEntries": + do_check_eq(subject, expectedData); + break; + case "before-removeEntriesForName": + case "removeEntriesForName": + do_check_true(subject instanceof Ci.nsISupportsString); + do_check_eq(subject, expectedData); + break; + case "before-removeEntriesByTimeframe": + case "removeEntriesByTimeframe": + do_check_true(subject instanceof Ci.nsIMutableArray); + do_check_eq(expectedData[0], subject.queryElementAt(0, Ci.nsISupportsPRInt64)); + do_check_eq(expectedData[1], subject.queryElementAt(1, Ci.nsISupportsPRInt64)); + break; + case "before-expireOldEntries": + case "expireOldEntries": + do_check_true(subject instanceof Ci.nsISupportsPRInt64); + do_check_true(subject.data > 0); + break; + default: + do_throw("Unhandled notification: " + data + " / " + topic); + } + // ensure a duplicate is flagged as unexpected + if (expectedBeforeNotification) { + expectedBeforeNotification = null; + } else { + expectedNotification = null; + expectedData = null; + } + } +}; + +function countAllEntries() { + let stmt = fh.DBConnection.createStatement("SELECT COUNT(*) as numEntries FROM moz_formhistory"); + do_check_true(stmt.step()); + let numEntries = stmt.row.numEntries; + stmt.finalize(); + return numEntries; +} + +function triggerExpiration() { + // We can't easily fake a "daily idle" event, so for testing purposes form + // history listens for another notification to trigger an immediate + // expiration. + var os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); + os.notifyObservers(null, "formhistory-expire-now", null); +} + +function run_test() { + +try { + +var testnum = 0; +var testdesc = "Setup of test form history entries"; +fh = Cc["@mozilla.org/satchel/form-history;1"]. + getService(Ci.nsIFormHistory2); + +do_check_true(fh != null); + +var entry1 = ["entry1", "value1"]; +var entry2 = ["entry2", "value2"]; + + +// Add the observer +var os = Cc["@mozilla.org/observer-service;1"]. + getService(Ci.nsIObserverService); +os.addObserver(TestObserver, "satchel-storage-changed", false); + + +/* ========== 1 ========== */ +var testnum = 1; +var testdesc = "Initial connection to storage module" + +fh.DBConnection.executeSimpleSQL("DELETE FROM moz_formhistory"); +do_check_eq(countAllEntries(), 0, "Checking initial DB is empty"); + +/* ========== 2 ========== */ +testnum++; +testdesc = "addEntry"; + +expectedNotification = "addEntry"; +expectedData = entry1; +fh.addEntry(entry1[0], entry1[1]); +do_check_true(fh.entryExists(entry1[0], entry1[1])); +do_check_eq(expectedNotification, null); // check that observer got a notification + +/* ========== 3 ========== */ +testnum++; +testdesc = "modifyEntry"; + +expectedNotification = "modifyEntry"; +expectedData = entry1; +fh.addEntry(entry1[0], entry1[1]); // will update previous entry +do_check_eq(expectedNotification, null); + +/* ========== 4 ========== */ +testnum++; +testdesc = "removeEntry"; + +expectedNotification = "removeEntry"; +expectedBeforeNotification = "before-" + expectedNotification; +expectedData = entry1; +fh.removeEntry(entry1[0], entry1[1]); +do_check_eq(expectedNotification, null); +do_check_eq(expectedBeforeNotification, null); +do_check_true(!fh.entryExists(entry1[0], entry1[1])); + +/* ========== 5 ========== */ +testnum++; +testdesc = "removeAllEntries"; + +expectedNotification = "removeAllEntries"; +expectedBeforeNotification = "before-" + expectedNotification; +expectedData = null; // no data expected +fh.removeAllEntries(); +do_check_eq(expectedNotification, null); +do_check_eq(expectedBeforeNotification, null); + +/* ========== 6 ========== */ +testnum++; +testdesc = "removeAllEntries (again)"; + +expectedNotification = "removeAllEntries"; +expectedBeforeNotification = "before-" + expectedNotification; +expectedData = null; +fh.removeAllEntries(); +do_check_eq(expectedNotification, null); +do_check_eq(expectedBeforeNotification, null); + +/* ========== 7 ========== */ +testnum++; +testdesc = "removeEntriesForName"; + +expectedNotification = "removeEntriesForName"; +expectedBeforeNotification = "before-" + expectedNotification; +expectedData = "field2"; +fh.removeEntriesForName("field2"); +do_check_eq(expectedNotification, null); +do_check_eq(expectedBeforeNotification, null); + +/* ========== 8 ========== */ +testnum++; +testdesc = "removeEntriesByTimeframe"; + +expectedNotification = "removeEntriesByTimeframe"; +expectedBeforeNotification = "before-" + expectedNotification; +expectedData = [10, 99999999999]; +fh.removeEntriesByTimeframe(expectedData[0], expectedData[1]); +do_check_eq(expectedNotification, null); +do_check_eq(expectedBeforeNotification, null); + +/* ========== 9 ========== */ +testnum++; +testdesc = "expireOldEntries"; + +expectedNotification = "expireOldEntries"; +expectedBeforeNotification = "before-" + expectedNotification; +expectedData = null; // TestObserver checks expiryDate > 0 +triggerExpiration(); +do_check_eq(expectedNotification, null); +do_check_eq(expectedBeforeNotification, null); + +} catch (e) { + throw "FAILED in test #" + testnum + " -- " + testdesc + ": " + e; +} +};