From 1ab598f388fa3df455f6326103af937cf1e1feca Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Mon, 20 Dec 2010 18:21:49 +0100 Subject: [PATCH 01/23] Bug 615992 - IsBookmarked can lock for each onVisit call. r=sdwilsh a=blocking --- .../components/places/src/nsNavBookmarks.cpp | 149 ++++++++++-------- .../components/places/src/nsNavBookmarks.h | 43 +++++ .../tests/bookmarks/test_async_observers.js | 110 +++++++++++++ .../components/places/tests/head_common.js | 34 ++++ 4 files changed, 274 insertions(+), 62 deletions(-) create mode 100644 toolkit/components/places/tests/bookmarks/test_async_observers.js diff --git a/toolkit/components/places/src/nsNavBookmarks.cpp b/toolkit/components/places/src/nsNavBookmarks.cpp index 35bb50e87b28..ba3d81662719 100644 --- a/toolkit/components/places/src/nsNavBookmarks.cpp +++ b/toolkit/components/places/src/nsNavBookmarks.cpp @@ -118,6 +118,45 @@ SearchBookmarkForKeyword(nsTrimInt64HashKey::KeyType aKey, return PL_DHASH_NEXT; } +template +class AsyncGetBookmarksForURI : public AsyncStatementCallback +{ +public: + AsyncGetBookmarksForURI(nsNavBookmarks* aBookmarksSvc, + Method aCallback, + DataType aData) + : mBookmarksSvc(aBookmarksSvc) + , mCallback(aCallback) + , mData(aData) + { + nsCOMPtr stmt = + aBookmarksSvc->GetStatementById(DB_GET_BOOKMARKS_FOR_URI); + if (stmt) { + (void)URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aData.uri); + nsCOMPtr pendingStmt; + (void)stmt->ExecuteAsync(this, getter_AddRefs(pendingStmt)); + } + } + + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) + { + nsCOMPtr row; + while (NS_SUCCEEDED(aResultSet->GetNextRow(getter_AddRefs(row))) && row) { + nsresult rv = row->GetInt64(0, &mData.itemId); + NS_ENSURE_SUCCESS(rv, rv); + if (mCallback) { + ((*mBookmarksSvc).*mCallback)(mData); + } + } + return NS_OK; + } + +private: + nsRefPtr mBookmarksSvc; + Method mCallback; + DataType mData; +}; + } // Anonymous namespace. @@ -2868,8 +2907,24 @@ nsNavBookmarks::RemoveObserver(nsINavBookmarkObserver* aObserver) return mObservers.RemoveWeakElement(aObserver); } +void +nsNavBookmarks::NotifyItemVisited(ItemVisitData aData) +{ + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, nsINavBookmarkObserver, + OnItemVisited(aData.itemId, aData.visitId, aData.time)); +} -// nsNavBookmarks::nsINavHistoryObserver +void +nsNavBookmarks::NotifyItemChanged(ItemChangeData aData) +{ + NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, nsINavBookmarkObserver, + OnItemChanged(aData.itemId, aData.property, + aData.isAnnotation, aData.newValue, + aData.lastModified, aData.itemType)); +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsNavBookmarks::nsINavHistoryObserver NS_IMETHODIMP nsNavBookmarks::OnBeginUpdateBatch() @@ -2895,27 +2950,18 @@ nsNavBookmarks::OnEndUpdateBatch() NS_IMETHODIMP -nsNavBookmarks::OnVisit(nsIURI* aURI, PRInt64 aVisitID, PRTime aTime, +nsNavBookmarks::OnVisit(nsIURI* aURI, PRInt64 aVisitId, PRTime aTime, PRInt64 aSessionID, PRInt64 aReferringID, PRUint32 aTransitionType, PRUint32* aAdded) { - // If the page is bookmarked, we need to notify observers - PRBool bookmarked = PR_FALSE; - IsBookmarked(aURI, &bookmarked); - if (bookmarked) { - // query for all bookmarks for that URI, notify for each - nsTArray bookmarks; + // If the page is bookmarked, notify observers for each associated bookmark. + ItemVisitData visitData; + visitData.uri = aURI; + visitData.visitId = aVisitId; + visitData.time = aTime; - nsresult rv = GetBookmarkIdsForURITArray(aURI, bookmarks); - NS_ENSURE_SUCCESS(rv, rv); - - if (bookmarks.Length()) { - for (PRUint32 i = 0; i < bookmarks.Length(); i++) - NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, - nsINavBookmarkObserver, - OnItemVisited(bookmarks[i], aVisitID, aTime)); - } - } + nsRefPtr< AsyncGetBookmarksForURI > notifier = + new AsyncGetBookmarksForURI(this, &nsNavBookmarks::NotifyItemVisited, visitData); return NS_OK; } @@ -2930,26 +2976,16 @@ nsNavBookmarks::OnBeforeDeleteURI(nsIURI* aURI) NS_IMETHODIMP nsNavBookmarks::OnDeleteURI(nsIURI* aURI) { - // If the page is bookmarked, we need to notify observers - PRBool bookmarked = PR_FALSE; - IsBookmarked(aURI, &bookmarked); - if (bookmarked) { - // query for all bookmarks for that URI, notify for each - nsTArray bookmarks; + // If the page is bookmarked, notify observers for each associated bookmark. + ItemChangeData changeData; + changeData.uri = aURI; + changeData.property = NS_LITERAL_CSTRING("cleartime"); + changeData.isAnnotation = PR_FALSE; + changeData.lastModified = 0; + changeData.itemType = TYPE_BOOKMARK; - nsresult rv = GetBookmarkIdsForURITArray(aURI, bookmarks); - NS_ENSURE_SUCCESS(rv, rv); - - if (bookmarks.Length()) { - for (PRUint32 i = 0; i < bookmarks.Length(); i ++) - NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, - nsINavBookmarkObserver, - OnItemChanged(bookmarks[i], - NS_LITERAL_CSTRING("cleartime"), - PR_FALSE, EmptyCString(), 0, - TYPE_BOOKMARK)); - } - } + nsRefPtr< AsyncGetBookmarksForURI > notifier = + new AsyncGetBookmarksForURI(this, &nsNavBookmarks::NotifyItemChanged, changeData); return NS_OK; } @@ -2977,6 +3013,14 @@ nsNavBookmarks::OnPageChanged(nsIURI* aURI, PRUint32 aWhat, { nsresult rv; if (aWhat == nsINavHistoryObserver::ATTRIBUTE_FAVICON) { + ItemChangeData changeData; + changeData.uri = aURI; + changeData.property = NS_LITERAL_CSTRING("favicon"); + changeData.isAnnotation = PR_FALSE; + changeData.newValue = NS_ConvertUTF16toUTF8(aValue); + changeData.lastModified = 0; + changeData.itemType = TYPE_BOOKMARK; + // Favicons may be set to either pure URIs or to folder URIs PRBool isPlaceURI; rv = aURI->SchemeIs("place", &isPlaceURI); @@ -2994,33 +3038,14 @@ nsNavBookmarks::OnPageChanged(nsIURI* aURI, PRUint32 aWhat, rv = history->QueryStringToQueryArray(spec, &queries, getter_AddRefs(options)); NS_ENSURE_SUCCESS(rv, rv); - NS_ENSURE_STATE(queries.Count() == 1); - NS_ENSURE_STATE(queries[0]->Folders().Length() == 1); - - NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, - nsINavBookmarkObserver, - OnItemChanged(queries[0]->Folders()[0], - NS_LITERAL_CSTRING("favicon"), - PR_FALSE, - NS_ConvertUTF16toUTF8(aValue), - 0, TYPE_BOOKMARK)); + if (queries.Count() == 1 && queries[0]->Folders().Length() == 1) { + changeData.itemId = queries[0]->Folders()[0]; + NotifyItemChanged(changeData); + } } else { - // query for all bookmarks for that URI, notify for each - nsTArray bookmarks; - rv = GetBookmarkIdsForURITArray(aURI, bookmarks); - NS_ENSURE_SUCCESS(rv, rv); - - if (bookmarks.Length()) { - for (PRUint32 i = 0; i < bookmarks.Length(); i ++) - NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, - nsINavBookmarkObserver, - OnItemChanged(bookmarks[i], - NS_LITERAL_CSTRING("favicon"), - PR_FALSE, - NS_ConvertUTF16toUTF8(aValue), - 0, TYPE_BOOKMARK)); - } + nsRefPtr< AsyncGetBookmarksForURI > notifier = + new AsyncGetBookmarksForURI(this, &nsNavBookmarks::NotifyItemChanged, changeData); } } return NS_OK; diff --git a/toolkit/components/places/src/nsNavBookmarks.h b/toolkit/components/places/src/nsNavBookmarks.h index 1a44a4c64582..3616a7f480a3 100644 --- a/toolkit/components/places/src/nsNavBookmarks.h +++ b/toolkit/components/places/src/nsNavBookmarks.h @@ -54,8 +54,29 @@ namespace places { enum BookmarkStatementId { DB_FIND_REDIRECTED_BOOKMARK = 0 + , DB_GET_BOOKMARKS_FOR_URI }; + struct ItemVisitData { + PRInt64 itemId; + nsCOMPtr uri; + PRInt64 visitId; + PRTime time; + }; + + struct ItemChangeData { + PRInt64 itemId; + nsCOMPtr uri; + nsCString property; + PRBool isAnnotation; + nsCString newValue; + PRTime lastModified; + PRUint16 itemType; + }; + + typedef void (nsNavBookmarks::*ItemVisitMethod)(ItemVisitData); + typedef void (nsNavBookmarks::*ItemChangeMethod)(ItemChangeData); + } // namespace places } // namespace mozilla @@ -176,10 +197,32 @@ public: switch(aStatementId) { case DB_FIND_REDIRECTED_BOOKMARK: return GetStatement(mDBFindRedirectedBookmark); + case DB_GET_BOOKMARKS_FOR_URI: + return GetStatement(mDBFindURIBookmarks); } return nsnull; } + /** + * Notifies that a bookmark has been visited. + * + * @param aItemId + * The visited item id. + * @param aData + * Details about the new visit. + */ + void NotifyItemVisited(mozilla::places::ItemVisitData aData); + + /** + * Notifies that a bookmark has changed. + * + * @param aItemId + * The changed item id. + * @param aData + * Details about the change. + */ + void NotifyItemChanged(mozilla::places::ItemChangeData aData); + private: static nsNavBookmarks* gBookmarksService; diff --git a/toolkit/components/places/tests/bookmarks/test_async_observers.js b/toolkit/components/places/tests/bookmarks/test_async_observers.js new file mode 100644 index 000000000000..c12617b53327 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_async_observers.js @@ -0,0 +1,110 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* This test checks that bookmarks service is correctly forwarding async + * events like visit or favicon additions. */ + +const NOW = Date.now() * 1000; +const ICON_URI = NetUtil.newURI(do_get_file("../unit/favicon-normal32.png")); + +let observer = { + bookmarks: [], + observedBookmarks: 0, + visitId: 0, + reset: function () + { + this.observedBookmarks = 0; + }, + onBeginUpdateBatch: function () {}, + onEndUpdateBatch: function () {}, + onItemAdded: function () {}, + onBeforeItemRemoved: function () {}, + onItemRemoved: function () {}, + onItemMoved: function () {}, + onItemChanged: function(aItemId, aProperty, aIsAnnotation, aNewValue, + aLastModified, aItemType) + { + do_log_info("Check that we got the correct change information."); + do_check_neq(this.bookmarks.indexOf(aItemId), -1); + if (aProperty == "favicon") { + do_check_false(aIsAnnotation); + do_check_eq(aNewValue, ICON_URI.spec); + do_check_eq(aLastModified, 0); + do_check_eq(aItemType, PlacesUtils.bookmarks.TYPE_BOOKMARK); + } + else if (aProperty == "cleartime") { + do_check_false(aIsAnnotation); + do_check_eq(aNewValue, ""); + do_check_eq(aLastModified, 0); + do_check_eq(aItemType, PlacesUtils.bookmarks.TYPE_BOOKMARK); + } + else { + do_throw("Unexpected property change " + aProperty); + } + + if (++this.observedBookmarks == this.bookmarks.length) { + run_next_test(); + } + }, + onItemVisited: function(aItemId, aVisitId, aTime) + { + do_log_info("Check that we got the correct visit information."); + do_check_neq(this.bookmarks.indexOf(aItemId), -1); + do_check_eq(aVisitId, this.visitId); + do_check_eq(aTime, NOW); + if (++this.observedBookmarks == this.bookmarks.length) { + run_next_test(); + } + }, + + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsINavBookmarkObserver, + ]) +}; +PlacesUtils.bookmarks.addObserver(observer, false); + +let gTests = [ + function add_visit_test() + { + observer.reset(); + // Add a visit to the bookmark and wait for the observer. + observer.visitId = + PlacesUtils.history.addVisit(NetUtil.newURI("http://book.ma.rk/"), NOW, null, + PlacesUtils.history.TRANSITION_TYPED, false, 0); + }, + function add_icon_test() + { + observer.reset(); + PlacesUtils.favicons.setAndLoadFaviconForPage(NetUtil.newURI("http://book.ma.rk/"), + ICON_URI, true); + }, + function remove_page_test() + { + observer.reset(); + PlacesUtils.history.removePage(NetUtil.newURI("http://book.ma.rk/")); + }, + function cleanup() + { + PlacesUtils.bookmarks.removeObserver(observer, false); + run_next_test(); + }, +]; + +function run_test() +{ + // Add multiple bookmarks to the same uri. + observer.bookmarks.push( + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, + NetUtil.newURI("http://book.ma.rk/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "Bookmark") + ); + observer.bookmarks.push( + PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId, + NetUtil.newURI("http://book.ma.rk/"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "Bookmark") + ); + + run_next_test(); +} diff --git a/toolkit/components/places/tests/head_common.js b/toolkit/components/places/tests/head_common.js index ee5a06209b1e..a118ea473449 100644 --- a/toolkit/components/places/tests/head_common.js +++ b/toolkit/components/places/tests/head_common.js @@ -607,3 +607,37 @@ function do_log_info(aMessage) { print("TEST-INFO | " + _TEST_FILE + " | " + aMessage); } + +/** + * Runs the next test in the gTests array. gTests should be a array defined in + * each test file. + */ +let gRunningTest = null; +let gTestIndex = 0; // The index of the currently running test. +function run_next_test() +{ + if (gRunningTest !== null) { + // Close the previous test do_test_pending call. + do_test_finished(); + } + + function _run_next_test() + { + if (gTestIndex < gTests.length) { + do_test_pending(); + gRunningTest = gTests[gTestIndex++]; + print("TEST-INFO | " + _TEST_FILE + " | Starting " + + gRunningTest.name); + // Exceptions do not kill asynchronous tests, so they'll time out. + try { + gRunningTest(); + } + catch (e) { + do_throw(e); + } + } + } + + // For sane stacks during failures, we execute this code soon, but not now. + do_execute_soon(_run_next_test); +} From eff14e19f8e09531b7ef8f65dcee6ff02563c350 Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Mon, 10 Jan 2011 19:54:49 +0100 Subject: [PATCH 02/23] Bug 609286 (Part 1) - Detect corrupt places.sqlite and replace it on next startup. r=dietrich a=blocker --- .../places/src/PlacesCategoriesStarter.js | 16 +- .../components/places/src/PlacesDBUtils.jsm | 593 ++++++++++++------ .../tests/unit/test_preventive_maintenance.js | 45 +- ...ventive_maintenance_checkAndFixDatabase.js | 41 +- 4 files changed, 476 insertions(+), 219 deletions(-) diff --git a/toolkit/components/places/src/PlacesCategoriesStarter.js b/toolkit/components/places/src/PlacesCategoriesStarter.js index 8880874ee88c..ba714e6016cf 100644 --- a/toolkit/components/places/src/PlacesCategoriesStarter.js +++ b/toolkit/components/places/src/PlacesCategoriesStarter.js @@ -43,6 +43,9 @@ const Cc = Components.classes; const Ci = Components.interfaces; +// Seconds between maintenance runs. +const MAINTENANCE_INTERVAL_SECONDS = 7 * 86400; + Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); /** @@ -61,9 +64,16 @@ PlacesCategoriesStarter.prototype = { { switch (aTopic) { case "idle-daily": - // Run places.sqlite maintenance tasks. - Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm"); - PlacesDBUtils.maintenanceOnIdle(); + // Once a week run places.sqlite maintenance tasks. + let lastMaintenance = 0; + try { + lastMaintenance = Services.prefs.getIntPref("places.database.lastMaintenance"); + } catch (ex) {} + let nowSeconds = parseInt(Date.now() / 1000); + if (lastMaintenance < nowSeconds - MAINTENANCE_INTERVAL_SECONDS) { + Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm"); + PlacesDBUtils.maintenanceOnIdle(); + } break; default: throw new Error("Trying to handle an unknown category."); diff --git a/toolkit/components/places/src/PlacesDBUtils.jsm b/toolkit/components/places/src/PlacesDBUtils.jsm index 95470aa8cec7..0b5310882ca0 100644 --- a/toolkit/components/places/src/PlacesDBUtils.jsm +++ b/toolkit/components/places/src/PlacesDBUtils.jsm @@ -51,7 +51,7 @@ let EXPORTED_SYMBOLS = [ "PlacesDBUtils" ]; //////////////////////////////////////////////////////////////////////////////// //// Constants -const FINISHED_MAINTENANCE_NOTIFICATION_TOPIC = "places-maintenance-finished"; +const FINISHED_MAINTENANCE_TOPIC = "places-maintenance-finished"; //////////////////////////////////////////////////////////////////////////////// //// Smart getters @@ -61,41 +61,244 @@ XPCOMUtils.defineLazyGetter(this, "DBConn", function() { }); //////////////////////////////////////////////////////////////////////////////// -//// nsPlacesDBUtils class +//// PlacesDBUtils -function nsPlacesDBUtils() { -} +let PlacesDBUtils = { + /** + * Executes a list of maintenance tasks. + * Once finished it will pass a array log to the callback attached to tasks, + * or print out to the error console if no callback is defined. + * FINISHED_MAINTENANCE_TOPIC is notified through observer service on finish. + * + * @param aTasks + * Tasks object to execute. + */ + _executeTasks: function PDBU__executeTasks(aTasks) + { + let task = aTasks.pop(); + if (task) { + task.call(PlacesDBUtils, aTasks); + } + else { + if (aTasks.callback) { + let scope = aTasks.scope || Cu.getGlobalForObject(aTasks.callback); + aTasks.callback.call(scope, aTasks.messages); + } + else { + // Output to the error console. + let messages = aTasks.messages + .unshift("[ Places Maintenance ]"); + try { + Services.console.logStringMessage(messages.join("\n")); + } catch(ex) {} + } -nsPlacesDBUtils.prototype = { - _statementsRunningCount: 0, + // Notify observers that maintenance finished. + Services.prefs.setIntPref("places.database.lastMaintenance", parseInt(Date.now() / 1000)); + Services.obs.notifyObservers(null, FINISHED_MAINTENANCE_TOPIC, null); + } + }, - ////////////////////////////////////////////////////////////////////////////// - //// mozIStorageStatementCallback + /** + * Executes integrity check and common maintenance tasks. + * + * @param [optional] aCallback + * Callback to be invoked when done. The callback will get a array + * of log messages. + * @param [optional] aScope + * Scope for the callback. + */ + maintenanceOnIdle: function PDBU_maintenanceOnIdle(aCallback, aScope) + { + let tasks = new Tasks([ + this.checkIntegrity + , this.checkCoherence + , this._refreshUI + ]); + tasks.callback = aCallback; + tasks.scope = aScope; + this._executeTasks(tasks); + }, - handleError: function PDBU_handleError(aError) { + /** + * Executes integrity check, common and advanced maintenance tasks (like + * expiration and vacuum). Will also collect statistics on the database. + * + * @param [optional] aCallback + * Callback to be invoked when done. The callback will get a array + * of log messages. + * @param [optional] aScope + * Scope for the callback. + */ + checkAndFixDatabase: function PDBU_checkAndFixDatabase(aCallback, aScope) + { + let tasks = new Tasks([ + this.checkIntegrity + , this.checkCoherence + , this.expire + , this.vacuum + , this.stats + , this._refreshUI + ]); + tasks.callback = aCallback; + tasks.scope = aScope; + this._executeTasks(tasks); + }, + + /** + * Forces a full refresh of Places views. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + _refreshUI: function PDBU__refreshUI(aTasks) + { + let tasks = new Tasks(aTasks); + + // Send batch update notifications to update the UI. + PlacesUtils.history.runInBatchMode({ + runBatched: function (aUserData) {} + }, null); + PlacesDBUtils._executeTasks(tasks); + }, + + _handleError: function PDBU__handleError(aError) + { Cu.reportError("Async statement execution returned with '" + aError.result + "', '" + aError.message + "'"); }, - handleCompletion: function PDBU_handleCompletion(aReason) { - // We serve only the last statement completion - if (--this._statementsRunningCount > 0) - return; + /** + * Tries to execute a REINDEX on the database. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + reindex: function PDBU_reindex(aTasks) + { + let tasks = new Tasks(aTasks); + tasks.log("> Reindex"); - // We finished executing all statements. - // Sending Begin/EndUpdateBatch notification will ensure that the UI - // is correctly refreshed. - PlacesUtils.history.runInBatchMode({runBatched: function(aUserData){}}, null); - PlacesUtils.bookmarks.runInBatchMode({runBatched: function(aUserData){}}, null); - // Notify observers that maintenance tasks are complete - Services.obs.notifyObservers(null, FINISHED_MAINTENANCE_NOTIFICATION_TOPIC, null); + let stmt = DBConn.createAsyncStatement("REINDEX"); + stmt.executeAsync({ + handleError: PlacesDBUtils._handleError, + handleResult: function () {}, + + handleCompletion: function (aReason) + { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + tasks.log("+ The database has been reindexed"); + } + else { + tasks.log("- Unable to reindex database"); + } + + PlacesDBUtils._executeTasks(tasks); + } + }); + stmt.finalize(); }, - ////////////////////////////////////////////////////////////////////////////// - //// Tasks + /** + * Checks integrity but does not try to fix the database through a reindex. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + _checkIntegritySkipReindex: function PDBU__checkIntegritySkipReindex(aTasks) + this.checkIntegrity(aTasks, true), - maintenanceOnIdle: function PDBU_maintenanceOnIdle() { - // bug 431558: preventive maintenance for Places database + /** + * Checks integrity and tries to fix the database through a reindex. + * + * @param [optional] aTasks + * Tasks object to execute. + * @param [optional] aSkipdReindex + * Whether to try to reindex database or not. + */ + checkIntegrity: function PDBU_checkIntegrity(aTasks, aSkipReindex) + { + let tasks = new Tasks(aTasks); + tasks.log("> Integrity check"); + + // Run a integrity check, but stop at the first error. + let stmt = DBConn.createAsyncStatement("PRAGMA integrity_check(1)"); + stmt.executeAsync({ + handleError: PlacesDBUtils._handleError, + + _corrupt: false, + handleResult: function (aResultSet) + { + let row = aResultSet.getNextRow(); + this._corrupt = row.getResultByIndex(0) != "ok"; + }, + + handleCompletion: function (aReason) + { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + if (this._corrupt) { + tasks.log("- The database is corrupt"); + if (aSkipReindex) { + tasks.log("- Unable to fix corruption, database will be replaced on next startup"); + Services.prefs.setBoolPref("places.database.replaceOnStartup", true); + tasks.clear(); + } + else { + // Try to reindex, this often fixed simple indices corruption. + // We insert from the top of the queue, they will run inverse. + tasks.push(PlacesDBUtils._checkIntegritySkipReindex); + tasks.push(PlacesDBUtils.reindex); + } + } + else { + tasks.log("+ The database is sane"); + } + } + else { + tasks.log("- Unable to check database status"); + tasks.clear(); + } + + PlacesDBUtils._executeTasks(tasks); + } + }); + stmt.finalize(); + }, + + /** + * Checks data coherence and tries to fix most common errors. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + checkCoherence: function PDBU_checkCoherence(aTasks) + { + let tasks = new Tasks(aTasks); + tasks.log("> Coherence check"); + + let stmts = this._getBoundCoherenceStatements(); + DBConn.executeAsync(stmts, stmts.length, { + handleError: PlacesDBUtils._handleError, + handleResult: function () {}, + + handleCompletion: function (aReason) + { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + tasks.log("+ The database is coherent"); + } + else { + tasks.log("- Unable to check database coherence"); + tasks.clear(); + } + + PlacesDBUtils._executeTasks(tasks); + } + }); + stmts.forEach(function (aStmt) aStmt.finalize()); + }, + + _getBoundCoherenceStatements: function PDBU__getBoundCoherenceStatements() + { let cleanupStatements = []; // MOZ_ANNO_ATTRIBUTES @@ -473,168 +676,206 @@ nsPlacesDBUtils.prototype = { // MAINTENANCE STATEMENTS SHOULD GO ABOVE THIS POINT! - // Used to keep track of last call to handleCompletion - this._statementsRunningCount = cleanupStatements.length; - // Statements are automatically queued-up by mozStorage - cleanupStatements.forEach(function (aStatement) { - aStatement.executeAsync(this); - aStatement.finalize(); - }, this); + return cleanupStatements; }, /** - * This method is only for support purposes, it will run sync and will take - * lot of time on big databases, but can be manually triggered to help - * debugging common issues. + * Tries to vacuum the database. + * + * @param [optional] aTasks + * Tasks object to execute. */ - checkAndFixDatabase: function PDBU_checkAndFixDatabase(aLogCallback) { - let log = []; - let self = this; - let sep = "- - -"; + vacuum: function PDBU_vacuum(aTasks) + { + let tasks = new Tasks(aTasks); + tasks.log("> Vacuum"); - function integrity() { - let integrityCheckStmt = - DBConn.createStatement("PRAGMA integrity_check"); - log.push("INTEGRITY"); - let logIndex = log.length; - while (integrityCheckStmt.executeStep()) { - log.push(integrityCheckStmt.getString(0)); - } - integrityCheckStmt.finalize(); - log.push(sep); - return log[logIndex] == "ok"; - } + let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + DBFile.append("places.sqlite"); + tasks.log("Initial database size is " + + parseInt(DBFile.fileSize / 1024) + " KiB"); - function vacuum(aVacuumCallback) { - log.push("VACUUM"); - let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - let placesDBFile = dirSvc.get("ProfD", Ci.nsILocalFile); - placesDBFile.append("places.sqlite"); - log.push("places.sqlite: " + placesDBFile.fileSize + " byte"); - log.push(sep); - let stmt = DBConn.createStatement("VACUUM"); - stmt.executeAsync({ - handleResult: function() {}, - handleError: function() { - Cu.reportError("Maintenance VACUUM failed"); - }, - handleCompletion: function(aReason) { - aVacuumCallback(); - } - }); - stmt.finalize(); - } + let stmt = DBConn.createAsyncStatement("VACUUM"); + stmt.executeAsync({ + handleError: PlacesDBUtils._handleError, + handleResult: function () {}, - function backup() { - log.push("BACKUP"); - let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - let profD = dirSvc.get("ProfD", Ci.nsILocalFile); - let placesDBFile = profD.clone(); - placesDBFile.append("places.sqlite"); - let backupDBFile = profD.clone(); - backupDBFile.append("places.sqlite.corrupt"); - backupDBFile.createUnique(backupDBFile.NORMAL_FILE_TYPE, 0666); - let backupName = backupDBFile.leafName; - backupDBFile.remove(false); - placesDBFile.copyTo(profD, backupName); - log.push(backupName); - log.push(sep); - } - - function reindex() { - log.push("REINDEX"); - DBConn.executeSimpleSQL("REINDEX"); - log.push(sep); - } - - function cleanup() { - log.push("CLEANUP"); - self.maintenanceOnIdle() - log.push(sep); - } - - function expire() { - log.push("EXPIRE"); - // Get expiration component. This will also ensure it has already been - // initialized. - let expiration = Cc["@mozilla.org/places/expiration;1"]. - getService(Ci.nsIObserver); - // Get maximum number of unique URIs. - let prefs = Cc["@mozilla.org/preferences-service;1"]. - getService(Ci.nsIPrefBranch); - let limitURIs = prefs.getIntPref( - "places.history.expiration.transient_current_max_pages"); - if (limitURIs >= 0) - log.push("Current unique URIs limit: " + limitURIs); - // Force a full expiration step. - expiration.observe(null, "places-debug-start-expiration", - -1 /* expire all expirable entries */); - log.push(sep); - } - - function stats() { - log.push("STATS"); - let dirSvc = Cc["@mozilla.org/file/directory_service;1"]. - getService(Ci.nsIProperties); - let placesDBFile = dirSvc.get("ProfD", Ci.nsILocalFile); - placesDBFile.append("places.sqlite"); - log.push("places.sqlite: " + placesDBFile.fileSize + " byte"); - let stmt = DBConn.createStatement( - "SELECT name FROM sqlite_master WHERE type = :DBType"); - stmt.params["DBType"] = "table"; - while (stmt.executeStep()) { - let tableName = stmt.getString(0); - let countStmt = DBConn.createStatement( - "SELECT count(*) FROM " + tableName); - countStmt.executeStep(); - log.push(tableName + ": " + countStmt.getInt32(0)); - countStmt.finalize(); - } - stmt.finalize(); - log.push(sep); - } - - // First of all execute an integrity check. - let integrityIsGood = integrity(); - - // If integrity check did fail, we can try to fix the database through - // a reindex. - if (!integrityIsGood) { - // Backup current database. - backup(); - // Execute a reindex. - reindex(); - // Now check again the integrity. - integrityIsGood = integrity(); - } - - // If integrity is fine, let's force a maintenance, execute a vacuum and - // get some stats. - if (integrityIsGood) { - cleanup(); - expire(); - vacuum(function () { - stats(); - if (aLogCallback) { - aLogCallback(log); + handleCompletion: function (aReason) + { + if (aReason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + tasks.log("+ The database has been vacuumed"); + let vacuumedDBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + vacuumedDBFile.append("places.sqlite"); + tasks.log("Final database size is " + + parseInt(vacuumedDBFile.fileSize / 1024) + " KiB"); } else { - try { - let console = Cc["@mozilla.org/consoleservice;1"]. - getService(Ci.nsIConsoleService); - console.logStringMessage(log.join('\n')); - } - catch(ex) {} + tasks.log("- Unable to vacuum database"); + tasks.clear(); } - }); - } - } + PlacesDBUtils._executeTasks(tasks); + } + }); + stmt.finalize(); + }, + + /** + * Forces a full expiration on the database. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + expire: function PDBU_expire(aTasks) + { + let tasks = new Tasks(aTasks); + tasks.log("> Orphans expiration"); + + let expiration = Cc["@mozilla.org/places/expiration;1"]. + getService(Ci.nsIObserver); + + Services.obs.addObserver(function (aSubject, aTopic, aData) { + Services.obs.removeObserver(arguments.callee, aTopic); + tasks.log("+ Database cleaned up"); + PlacesDBUtils._executeTasks(tasks); + }, PlacesUtils.TOPIC_EXPIRATION_FINISHED, false); + + // Force a full expiration step. + expiration.observe(null, "places-debug-start-expiration", -1); + }, + + /** + * Collects statistical data on the database. + * + * @param [optional] aTasks + * Tasks object to execute. + */ + stats: function PDBU_stats(aTasks) + { + let tasks = new Tasks(aTasks); + tasks.log("> Statistics"); + + let DBFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile); + DBFile.append("places.sqlite"); + tasks.log("Database size is " + parseInt(DBFile.fileSize / 1024) + " KiB"); + + [ "user_version" + , "page_size" + , "cache_size" + , "journal_mode" + , "synchronous" + ].forEach(function (aPragma) { + let stmt = DBConn.createStatement("PRAGMA " + aPragma); + stmt.executeStep(); + tasks.log(aPragma + " is " + stmt.getString(0)); + stmt.finalize(); + }); + + // Get maximum number of unique URIs. + try { + let limitURIs = Services.prefs.getIntPref( + "places.history.expiration.transient_current_max_pages"); + tasks.log("History can store a maximum of " + limitURIs + " unique pages"); + } catch(ex) {} + + let stmt = DBConn.createStatement( + "SELECT name FROM sqlite_master WHERE type = :type"); + stmt.params.type = "table"; + while (stmt.executeStep()) { + let tableName = stmt.getString(0); + let countStmt = DBConn.createStatement( + "SELECT count(*) FROM " + tableName); + countStmt.executeStep(); + tasks.log("Table " + tableName + " has " + countStmt.getInt32(0) + " records"); + countStmt.finalize(); + } + stmt.reset(); + + stmt.params.type = "index"; + while (stmt.executeStep()) { + tasks.log("Index " + stmt.getString(0)); + } + stmt.reset(); + + stmt.params.type = "trigger"; + while (stmt.executeStep()) { + tasks.log("Trigger " + stmt.getString(0)); + } + stmt.finalize(); + + PlacesDBUtils._executeTasks(tasks); + } }; -__defineGetter__("PlacesDBUtils", function() { - delete this.PlacesDBUtils; - return this.PlacesDBUtils = new nsPlacesDBUtils; -}); +/** + * LIFO tasks stack. + * + * @param [optional] aTasks + * Array of tasks or another Tasks object to clone. + */ +function Tasks(aTasks) +{ + if (Array.isArray(aTasks)) { + this._list = aTasks.slice(0, aTasks.length); + } + else if ("list" in aTasks) { + this._list = aTasks.list; + this._log = aTasks.messages; + this.callback = aTasks.callback; + this.scope = aTasks.scope; + } +} + +Tasks.prototype = { + _list: [], + _log: [], + callback: null, + scope: null, + + /** + * Adds a task to the top of the list. + * + * @param aNewElt + * Task to be added. + */ + push: function T_push(aNewElt) + { + this._list.unshift(aNewElt); + }, + + /** + * Returns and consumes next task. + * + * @return next task or undefined if no task is left. + */ + pop: function T_pop() this._list.shift(), + + /** + * Removes all tasks. + */ + clear: function T_clear() + { + this._list.length = 0; + }, + + /** + * Returns array of tasks ordered from the next to be run to the latest. + */ + get list() this._list.slice(0, this._list.length), + + /** + * Adds a message to the log. + * + * @param aMsg + * String message to be added. + */ + log: function T_log(aMsg) + { + this._log.push(aMsg); + }, + + /** + * Returns array of log messages ordered from oldest to newest. + */ + get messages() this._log.slice(0, this._log.length), +} diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance.js b/toolkit/components/places/tests/unit/test_preventive_maintenance.js index bf842845b1ff..2984b4fa86a3 100644 --- a/toolkit/components/places/tests/unit/test_preventive_maintenance.js +++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js @@ -45,27 +45,15 @@ // Include PlacesDBUtils module Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm"); -const FINISHED_MAINTANANCE_NOTIFICATION_TOPIC = "places-maintenance-finished"; - -const PLACES_STRING_BUNDLE_URI = "chrome://places/locale/places.properties"; +const FINISHED_MAINTENANCE_NOTIFICATION_TOPIC = "places-maintenance-finished"; // Get services and database connection -let os = Cc["@mozilla.org/observer-service;1"]. - getService(Ci.nsIObserverService); -let hs = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); -let bh = hs.QueryInterface(Ci.nsIBrowserHistory); -let bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]. - getService(Ci.nsINavBookmarksService); -let ts = Cc["@mozilla.org/browser/tagging-service;1"]. - getService(Ci.nsITaggingService); -let as = Cc["@mozilla.org/browser/annotation-service;1"]. - getService(Ci.nsIAnnotationService); -let fs = Cc["@mozilla.org/browser/favicon-service;1"]. - getService(Ci.nsIFaviconService); -let bundle = Cc["@mozilla.org/intl/stringbundle;1"]. - getService(Ci.nsIStringBundleService). - createBundle(PLACES_STRING_BUNDLE_URI); +let hs = PlacesUtils.history; +let bh = PlacesUtils.bhistory; +let bs = PlacesUtils.bookmarks; +let ts = PlacesUtils.tagging; +let as = PlacesUtils.annotations; +let fs = PlacesUtils.favicons; let mDBConn = hs.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection; @@ -321,13 +309,13 @@ tests.push({ // Ensure all roots titles are correct. do_check_eq(bs.getItemTitle(bs.placesRoot), ""); do_check_eq(bs.getItemTitle(bs.bookmarksMenuFolder), - bundle.GetStringFromName("BookmarksMenuFolderTitle")); + PlacesUtils.getString("BookmarksMenuFolderTitle")); do_check_eq(bs.getItemTitle(bs.tagsFolder), - bundle.GetStringFromName("TagsFolderTitle")); + PlacesUtils.getString("TagsFolderTitle")); do_check_eq(bs.getItemTitle(bs.unfiledBookmarksFolder), - bundle.GetStringFromName("UnsortedBookmarksFolderTitle")); + PlacesUtils.getString("UnsortedBookmarksFolderTitle")); do_check_eq(bs.getItemTitle(bs.toolbarFolder), - bundle.GetStringFromName("BookmarksToolbarFolderTitle")); + PlacesUtils.getString("BookmarksToolbarFolderTitle")); } }); @@ -1178,10 +1166,15 @@ tests.push({ let observer = { observe: function(aSubject, aTopic, aData) { - if (aTopic == FINISHED_MAINTANANCE_NOTIFICATION_TOPIC) { + if (aTopic == FINISHED_MAINTENANCE_NOTIFICATION_TOPIC) { + // Check the lastMaintenance time has been saved. + do_check_neq(Services.prefs.getIntPref("places.database.lastMaintenance"), null); + try {current_test.check();} catch (ex){ do_throw(ex);} + cleanDatabase(); + if (tests.length) { current_test = tests.shift(); dump("\nExecuting test: " + current_test.name + "\n" + "*** " + current_test.desc + "\n"); @@ -1189,7 +1182,7 @@ let observer = { PlacesDBUtils.maintenanceOnIdle(); } else { - os.removeObserver(this, FINISHED_MAINTANANCE_NOTIFICATION_TOPIC); + Services.obs.removeObserver(this, FINISHED_MAINTENANCE_NOTIFICATION_TOPIC); // Sanity check: all roots should be intact do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0); do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot); @@ -1201,7 +1194,7 @@ let observer = { } } } -os.addObserver(observer, FINISHED_MAINTANANCE_NOTIFICATION_TOPIC, false); +Services.obs.addObserver(observer, FINISHED_MAINTENANCE_NOTIFICATION_TOPIC, false); // main diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js b/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js index 86bece9d0f5f..3567ebb955d6 100644 --- a/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js +++ b/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js @@ -46,23 +46,36 @@ Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm"); function run_test() { do_test_pending(); PlacesDBUtils.checkAndFixDatabase(function(aLog) { - // Integrity should be good. - do_check_eq(aLog[1], "ok"); + let sections = []; + let positives = []; + let negatives = []; + let infos = []; - // Check we are not wrongly executing bad integrity tasks. - const goodMsgs = ["INTEGRITY", "EXPIRE", "VACUUM", "CLEANUP", "STATS"]; - const badMsgs = ["BACKUP", "REINDEX"]; - - aLog.forEach(function (aLogMsg) { - print (aLogMsg); - do_check_eq(badMsgs.indexOf(aLogMsg), -1); - let index = goodMsgs.indexOf(aLogMsg); - if (index != -1) - goodMsgs.splice(index, 1); + aLog.forEach(function (aMsg) { + print (aMsg); + switch (aMsg.substr(0, 1)) { + case "+": + positives.push(aMsg); + break; + case "-": + negatives.push(aMsg); + break; + case ">": + sections.push(aMsg); + break; + default: + infos.push(aMsg); + } }); - // Check we have run all tasks. - do_check_eq(goodMsgs.length, 0); + print("Check that we have run all sections."); + do_check_eq(sections.length, 5); + print("Check that we have no negatives."); + do_check_false(!!negatives.length); + print("Check that we have positives."); + do_check_true(!!positives.length); + print("Check that we have info."); + do_check_true(!!infos.length); do_test_finished(); }); From aad866446483f8aaf0bf47a88c6411d049f114b5 Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Mon, 10 Jan 2011 19:57:24 +0100 Subject: [PATCH 03/23] Bug 609286 (Part 2) - Detect a corrupt Places.sqlite and replace the database on next startup. r=dietrich a=blocking --- .../components/places/src/nsNavHistory.cpp | 42 +++++++++++----- .../places/tests/unit/default.sqlite | Bin 0 -> 1081344 bytes .../unit/test_database_replaceOnStartup.js | 46 ++++++++++++++++++ 3 files changed, 75 insertions(+), 13 deletions(-) create mode 100644 toolkit/components/places/tests/unit/default.sqlite create mode 100644 toolkit/components/places/tests/unit/test_database_replaceOnStartup.js diff --git a/toolkit/components/places/src/nsNavHistory.cpp b/toolkit/components/places/src/nsNavHistory.cpp index 6ef488a6fe92..9b17896eda56 100644 --- a/toolkit/components/places/src/nsNavHistory.cpp +++ b/toolkit/components/places/src/nsNavHistory.cpp @@ -126,6 +126,8 @@ using namespace mozilla::places; #define PREF_CACHE_TO_MEMORY_PERCENTAGE "database.cache_to_memory_percentage" +#define PREF_FORCE_DATABASE_REPLACEMENT "database.replaceOnStartup" + // Default integer value for PREF_CACHE_TO_MEMORY_PERCENTAGE. // This is 6% of machine memory, giving 15MB for a user with 256MB of memory. // Out of this cache, SQLite will use at most the size of the database file. @@ -574,11 +576,9 @@ nsNavHistory::Init() nsresult nsNavHistory::InitDBFile(PRBool aForceInit) { - if (aForceInit) { - NS_ASSERTION(mDBConn, - "When forcing initialization, a database connection must exist!"); - NS_ASSERTION(mDBService, - "When forcing initialization, the database service must exist!"); + if (!mDBService) { + mDBService = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID); + NS_ENSURE_STATE(mDBService); } // Get database file handle. @@ -606,10 +606,12 @@ nsNavHistory::InitDBFile(PRBool aForceInit) } // Close database connection if open. - // If there's any not finalized statement or this fails for any reason - // we won't be able to remove the database. - rv = mDBConn->Close(); - NS_ENSURE_SUCCESS(rv, rv); + if (mDBConn) { + // If there's any not finalized statement or this fails for any reason + // we won't be able to remove the database. + rv = mDBConn->Close(); + NS_ENSURE_SUCCESS(rv, rv); + } // Remove the broken database. rv = mDBFile->Remove(PR_FALSE); @@ -636,14 +638,28 @@ nsNavHistory::InitDBFile(PRBool aForceInit) rv = mDBFile->Exists(&dbExists); NS_ENSURE_SUCCESS(rv, rv); // If the database didn't previously exist, we create it. - if (!dbExists) + if (!dbExists) { mDatabaseStatus = DATABASE_STATUS_CREATE; + } + else { + // Check if maintenance required a database replacement. + PRBool forceDatabaseReplacement; + if (NS_SUCCEEDED(mPrefBranch->GetBoolPref(PREF_FORCE_DATABASE_REPLACEMENT, + &forceDatabaseReplacement)) && + forceDatabaseReplacement) { + // Be sure to clear the pref to avoid handling it more than once. + rv = mPrefBranch->ClearUserPref(PREF_FORCE_DATABASE_REPLACEMENT); + NS_ENSURE_SUCCESS(rv, rv); + // Re-enter this same method, forcing the replacement. + rv = InitDBFile(PR_TRUE); + NS_ENSURE_SUCCESS(rv, rv); + return NS_OK; + } + } } // Open the database file. If it does not exist a new one will be created. - mDBService = do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID, &rv); - NS_ENSURE_SUCCESS(rv, rv); - // Open un unshared connection, both for safety and speed. + // Use a unshared connection, both for safety and performance. rv = mDBService->OpenUnsharedDatabase(mDBFile, getter_AddRefs(mDBConn)); if (rv == NS_ERROR_FILE_CORRUPTED) { // The database is corrupt, try to create a new one. diff --git a/toolkit/components/places/tests/unit/default.sqlite b/toolkit/components/places/tests/unit/default.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..8fbd3bc9acab0abff5305d493d9568a10b36d08b GIT binary patch literal 1081344 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 Vv2M4vtlP~^>-N9_a=W>)@n55 Date: Tue, 11 Jan 2011 10:19:19 -0800 Subject: [PATCH 04/23] Bug 606966 - Need an async history visit API exposed to JS Part 2 - Refactor tests and add a run_next_test method in head_common.js r=mak a=blocking --- .../tests/migration/test_v11_from_v10.js | 27 +---------------- .../test_v11_from_v10_migrated_from_v11.js | 27 +---------------- .../tests/unit/test_sql_guid_functions.js | 29 ++----------------- 3 files changed, 4 insertions(+), 79 deletions(-) diff --git a/toolkit/components/places/tests/migration/test_v11_from_v10.js b/toolkit/components/places/tests/migration/test_v11_from_v10.js index 696e5591cc7c..34cc33ed5ffb 100644 --- a/toolkit/components/places/tests/migration/test_v11_from_v10.js +++ b/toolkit/components/places/tests/migration/test_v11_from_v10.js @@ -305,7 +305,7 @@ function test_final_state() //////////////////////////////////////////////////////////////////////////////// //// Test Runner -let tests = [ +let gTests = [ test_initial_state, test_moz_bookmarks_guid_exists, test_bookmark_guids_non_null, @@ -317,34 +317,9 @@ let tests = [ test_place_guid_annotation_removed, test_final_state, ]; -let index = 0; - -function run_next_test() -{ - function _run_next_test() { - if (index < tests.length) { - do_test_pending(); - print("TEST-INFO | " + _TEST_FILE + " | Starting " + tests[index].name); - - // Exceptions do not kill asynchronous tests, so they'll time out. - try { - tests[index++](); - } - catch (e) { - do_throw(e); - } - } - - do_test_finished(); - } - - // For sane stacks during failures, we execute this code soon, but not now. - do_execute_soon(_run_next_test); -} function run_test() { setPlacesDatabase("places_v10.sqlite"); - do_test_pending(); run_next_test(); } diff --git a/toolkit/components/places/tests/migration/test_v11_from_v10_migrated_from_v11.js b/toolkit/components/places/tests/migration/test_v11_from_v10_migrated_from_v11.js index 682884debdb9..96df31cc70f8 100644 --- a/toolkit/components/places/tests/migration/test_v11_from_v10_migrated_from_v11.js +++ b/toolkit/components/places/tests/migration/test_v11_from_v10_migrated_from_v11.js @@ -118,40 +118,15 @@ function test_final_state() //////////////////////////////////////////////////////////////////////////////// //// Test Runner -let tests = [ +let gTests = [ test_initial_state, test_bookmark_guids_non_null, test_place_guids_non_null, test_final_state, ]; -let index = 0; - -function run_next_test() -{ - function _run_next_test() { - if (index < tests.length) { - do_test_pending(); - print("TEST-INFO | " + _TEST_FILE + " | Starting " + tests[index].name); - - // Exceptions do not kill asynchronous tests, so they'll time out. - try { - tests[index++](); - } - catch (e) { - do_throw(e); - } - } - - do_test_finished(); - } - - // For sane stacks during failures, we execute this code soon, but not now. - do_execute_soon(_run_next_test); -} function run_test() { setPlacesDatabase("places_v10_from_v11.sqlite"); - do_test_pending(); run_next_test(); } diff --git a/toolkit/components/places/tests/unit/test_sql_guid_functions.js b/toolkit/components/places/tests/unit/test_sql_guid_functions.js index 0e4ed7243b1f..ad1cca327896 100644 --- a/toolkit/components/places/tests/unit/test_sql_guid_functions.js +++ b/toolkit/components/places/tests/unit/test_sql_guid_functions.js @@ -14,7 +14,7 @@ */ function check_invariants(aGuid) { - print("TEST-INFO | " + tests[index - 1].name + " | Checking guid '" + + print("TEST-INFO | " + gRunningTest.name + " | Checking guid '" + aGuid + "'"); do_check_valid_places_guid(aGuid); @@ -98,37 +98,12 @@ function test_guid_on_background() //////////////////////////////////////////////////////////////////////////////// //// Test Runner -let tests = [ +let gTests = [ test_guid_invariants, test_guid_on_background, ]; -let index = 0; - -function run_next_test() -{ - function _run_next_test() { - if (index < tests.length) { - do_test_pending(); - print("TEST-INFO | " + _TEST_FILE + " | Starting " + tests[index].name); - - // Exceptions do not kill asynchronous tests, so they'll time out. - try { - tests[index++](); - } - catch (e) { - do_throw(e); - } - } - - do_test_finished(); - } - - // For sane stacks during failures, we execute this code soon, but not now. - do_execute_soon(_run_next_test); -} function run_test() { - do_test_pending(); run_next_test(); } From fdcfd8a0f554e1019d3f7cba3db7ffa4a96a720d Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Tue, 11 Jan 2011 10:26:44 -0800 Subject: [PATCH 05/23] Bug 606966 - Need an async history visit API exposed to JS Part 4 - Refactor hidden flag logic into a helper method. r=mak a=blocking --- toolkit/components/places/src/Helpers.cpp | 9 ++++++++ toolkit/components/places/src/Helpers.h | 13 +++++++++++ toolkit/components/places/src/History.cpp | 5 +---- .../components/places/src/nsNavHistory.cpp | 22 ++++++------------- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/toolkit/components/places/src/Helpers.cpp b/toolkit/components/places/src/Helpers.cpp index 29363f1a6ae5..49bca2978507 100644 --- a/toolkit/components/places/src/Helpers.cpp +++ b/toolkit/components/places/src/Helpers.cpp @@ -348,5 +348,14 @@ ForceWALCheckpoint(mozIStorageConnection* aDBConn) (void)stmt->ExecuteAsync(nsnull, getter_AddRefs(handle)); } +bool +GetHiddenState(bool aIsRedirect, + PRUint32 aTransitionType) +{ + return aTransitionType == nsINavHistoryService::TRANSITION_FRAMED_LINK || + aTransitionType == nsINavHistoryService::TRANSITION_EMBED || + aIsRedirect; +} + } // namespace places } // namespace mozilla diff --git a/toolkit/components/places/src/Helpers.h b/toolkit/components/places/src/Helpers.h index c6adec4abc1a..64e2b713d1ea 100644 --- a/toolkit/components/places/src/Helpers.h +++ b/toolkit/components/places/src/Helpers.h @@ -267,6 +267,19 @@ protected: */ void ForceWALCheckpoint(mozIStorageConnection* aDBConn); +/** + * Determines if a visit should be marked as hidden given its transition type + * and whether or not it was a redirect. + * + * @param aIsRedirect + * True if this visit was a redirect, false otherwise. + * @param aTransitionType + * The transition type of the visit. + * @return true if this visit should be hidden. + */ +bool GetHiddenState(bool aIsRedirect, + PRUint32 aTransitionType); + } // namespace places } // namespace mozilla diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index 9ca6e3459b2d..de38d76ff68a 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -1093,10 +1093,7 @@ History::VisitURI(nsIURI* aURI, } place.typed = place.transitionType == nsINavHistoryService::TRANSITION_TYPED; - place.hidden = - place.transitionType == nsINavHistoryService::TRANSITION_FRAMED_LINK || - place.transitionType == nsINavHistoryService::TRANSITION_EMBED || - redirected; + place.hidden = GetHiddenState(redirected, place.transitionType); place.visitTime = PR_Now(); // EMBED visits are session-persistent and should not go through the database. diff --git a/toolkit/components/places/src/nsNavHistory.cpp b/toolkit/components/places/src/nsNavHistory.cpp index 9b17896eda56..e52734053f46 100644 --- a/toolkit/components/places/src/nsNavHistory.cpp +++ b/toolkit/components/places/src/nsNavHistory.cpp @@ -2726,24 +2726,16 @@ nsNavHistory::AddVisit(nsIURI* aURI, PRTime aTime, nsIURI* aReferringURI, stmt->Reset(); scoper.Abandon(); - // embedded links and redirects will be hidden, but don't hide pages that - // are already unhidden. - // - // Note that we test the redirect flag and not for the redirect transition - // type. The transition type refers to how we got here, and whether a page - // is shown does not depend on whether you got to it through a redirect. - // Rather, we want to hide pages that redirect themselves somewhere - // else, which is what the redirect flag means. - // - // note, we want to unhide any hidden pages that the user explicitly types - // (aTransitionType == TRANSITION_TYPED) so that they will appear in - // the history UI (sidebar, history menu, url bar autocomplete, etc) + // Note that we want to unhide any hidden pages that the user explicitly + // types (aTransitionType == TRANSITION_TYPED) so that they will appear in + // the history UI (sidebar, history menu, url bar autocomplete, etc). + // Additionally, we don't want to hide any pages that are already unhidden. hidden = oldHiddenState; if (hidden == 1 && - (!aIsRedirect || aTransitionType == TRANSITION_TYPED) && - aTransitionType != TRANSITION_EMBED && - aTransitionType != TRANSITION_FRAMED_LINK) + (!GetHiddenState(aIsRedirect, aTransitionType) || + aTransitionType == TRANSITION_TYPED)) { hidden = 0; // unhide + } typed = (PRInt32)(oldTypedState == 1 || (aTransitionType == TRANSITION_TYPED)); From a28588088afecb234830a53da4ebd741c9c4e9df Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Tue, 11 Jan 2011 10:34:04 -0800 Subject: [PATCH 06/23] Bug 606966 - Need an async history visit API exposed to JS Part 5 - Add a new constructor to VisitData that takes a URI and automatically obtains the spec and the reversed hostname. Also add a method to set the transition type that also sets the hidden flag and the typed flag accordingly. Finally, update all the code that sets this stuff manually to use the new helper methods. r=dietrich a=blocking --- toolkit/components/places/src/History.cpp | 66 +++++++++++++++-------- 1 file changed, 45 insertions(+), 21 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index de38d76ff68a..6840f6dcdc47 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -191,13 +191,44 @@ struct VisitData { : placeId(0) , visitId(0) , sessionId(0) - , hidden(false) + , hidden(true) , typed(false) - , transitionType(-1) + , transitionType(PR_UINT32_MAX) , visitTime(0) { } + VisitData(nsIURI* aURI) + : placeId(0) + , visitId(0) + , sessionId(0) + , hidden(true) + , typed(false) + , transitionType(PR_UINT32_MAX) + , visitTime(0) + { + (void)aURI->GetSpec(spec); + (void)GetReversedHostname(aURI, revHost); + } + + /** + * Sets the transition type of the visit, as well as if it was typed and + * should be hidden (based on the transition type specified). + * + * @param aTransitionType + * The transition type constant to set. Must be one of the + * TRANSITION_ constants on nsINavHistoryService. + */ + void SetTransitionType(PRUint32 aTransitionType) + { + typed = aTransitionType == nsINavHistoryService::TRANSITION_TYPED; + bool redirected = + aTransitionType == nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY || + aTransitionType == nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT; + hidden = GetHiddenState(redirected, aTransitionType); + transitionType = aTransitionType; + } + PRInt64 placeId; PRInt64 visitId; PRInt64 sessionId; @@ -205,7 +236,7 @@ struct VisitData { nsString revHost; bool hidden; bool typed; - PRInt32 transitionType; + PRUint32 transitionType; PRTime visitTime; }; @@ -1048,15 +1079,14 @@ History::VisitURI(nsIURI* aURI, } } - VisitData place; - rv = aURI->GetSpec(place.spec); - NS_ENSURE_SUCCESS(rv, rv); - (void)GetReversedHostname(aURI, place.revHost); + VisitData place(aURI); + NS_ENSURE_FALSE(place.spec.IsEmpty(), NS_ERROR_INVALID_ARG); + + place.visitTime = PR_Now(); // Assigns a type to the edge in the visit linked list. Each type will be // considered differently when weighting the frecency of a location. PRUint32 recentFlags = navHistory->GetRecentFlags(aURI); - bool redirected = false; bool isFollowedLink = recentFlags & nsNavHistory::RECENT_ACTIVATED; // Embed visits should never be added to the database, and the same is valid @@ -1067,35 +1097,29 @@ History::VisitURI(nsIURI* aURI, if (!(aFlags & IHistory::TOP_LEVEL) && !isFollowedLink) { // A frame redirected to a new site without user interaction. - place.transitionType = nsINavHistoryService::TRANSITION_EMBED; + place.SetTransitionType(nsINavHistoryService::TRANSITION_EMBED); } else if (aFlags & IHistory::REDIRECT_TEMPORARY) { - place.transitionType = nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY; - redirected = true; + place.SetTransitionType(nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY); } else if (aFlags & IHistory::REDIRECT_PERMANENT) { - place.transitionType = nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT; - redirected = true; + place.SetTransitionType(nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT); } else if (recentFlags & nsNavHistory::RECENT_TYPED) { - place.transitionType = nsINavHistoryService::TRANSITION_TYPED; + place.SetTransitionType(nsINavHistoryService::TRANSITION_TYPED); } else if (recentFlags & nsNavHistory::RECENT_BOOKMARKED) { - place.transitionType = nsINavHistoryService::TRANSITION_BOOKMARK; + place.SetTransitionType(nsINavHistoryService::TRANSITION_BOOKMARK); } else if (!(aFlags & IHistory::TOP_LEVEL) && isFollowedLink) { // User activated a link in a frame. - place.transitionType = nsINavHistoryService::TRANSITION_FRAMED_LINK; + place.SetTransitionType(nsINavHistoryService::TRANSITION_FRAMED_LINK); } else { // User was redirected or link was clicked in the main window. - place.transitionType = nsINavHistoryService::TRANSITION_LINK; + place.SetTransitionType(nsINavHistoryService::TRANSITION_LINK); } - place.typed = place.transitionType == nsINavHistoryService::TRANSITION_TYPED; - place.hidden = GetHiddenState(redirected, place.transitionType); - place.visitTime = PR_Now(); - // EMBED visits are session-persistent and should not go through the database. // They exist only to keep track of isVisited status during the session. if (place.transitionType == nsINavHistoryService::TRANSITION_EMBED) { From 7dae615728f9586be3ff5bad866bf0271b9e0e8f Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Tue, 11 Jan 2011 10:48:14 -0800 Subject: [PATCH 07/23] Bug 606966 - Need an async history visit API exposed to JS Part 6 - Move inserting and updating of moz_place entries from InsertVisitedURI to History. r=dietrich a=blocking --- toolkit/components/places/src/History.cpp | 201 +++++++++++++--------- toolkit/components/places/src/History.h | 18 ++ 2 files changed, 133 insertions(+), 86 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index 6840f6dcdc47..fa2ee06a9804 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -73,6 +73,63 @@ namespace places { // Observer event fired after a visit has been registered in the DB. #define URI_VISIT_SAVED "uri-visit-saved" +//////////////////////////////////////////////////////////////////////////////// +//// VisitData + +struct VisitData { + VisitData() + : placeId(0) + , visitId(0) + , sessionId(0) + , hidden(true) + , typed(false) + , transitionType(PR_UINT32_MAX) + , visitTime(0) + { + } + + VisitData(nsIURI* aURI) + : placeId(0) + , visitId(0) + , sessionId(0) + , hidden(true) + , typed(false) + , transitionType(PR_UINT32_MAX) + , visitTime(0) + { + (void)aURI->GetSpec(spec); + (void)GetReversedHostname(aURI, revHost); + } + + /** + * Sets the transition type of the visit, as well as if it was typed and + * should be hidden (based on the transition type specified). + * + * @param aTransitionType + * The transition type constant to set. Must be one of the + * TRANSITION_ constants on nsINavHistoryService. + */ + void SetTransitionType(PRUint32 aTransitionType) + { + typed = aTransitionType == nsINavHistoryService::TRANSITION_TYPED; + bool redirected = + aTransitionType == nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY || + aTransitionType == nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT; + hidden = GetHiddenState(redirected, aTransitionType); + transitionType = aTransitionType; + } + + PRInt64 placeId; + PRInt64 visitId; + PRInt64 sessionId; + nsCString spec; + nsString revHost; + bool hidden; + bool typed; + PRUint32 transitionType; + PRTime visitTime; +}; + //////////////////////////////////////////////////////////////////////////////// //// Anonymous Helpers @@ -186,61 +243,6 @@ private: bool mIsVisited; }; -struct VisitData { - VisitData() - : placeId(0) - , visitId(0) - , sessionId(0) - , hidden(true) - , typed(false) - , transitionType(PR_UINT32_MAX) - , visitTime(0) - { - } - - VisitData(nsIURI* aURI) - : placeId(0) - , visitId(0) - , sessionId(0) - , hidden(true) - , typed(false) - , transitionType(PR_UINT32_MAX) - , visitTime(0) - { - (void)aURI->GetSpec(spec); - (void)GetReversedHostname(aURI, revHost); - } - - /** - * Sets the transition type of the visit, as well as if it was typed and - * should be hidden (based on the transition type specified). - * - * @param aTransitionType - * The transition type constant to set. Must be one of the - * TRANSITION_ constants on nsINavHistoryService. - */ - void SetTransitionType(PRUint32 aTransitionType) - { - typed = aTransitionType == nsINavHistoryService::TRANSITION_TYPED; - bool redirected = - aTransitionType == nsINavHistoryService::TRANSITION_REDIRECT_TEMPORARY || - aTransitionType == nsINavHistoryService::TRANSITION_REDIRECT_PERMANENT; - hidden = GetHiddenState(redirected, aTransitionType); - transitionType = aTransitionType; - } - - PRInt64 placeId; - PRInt64 visitId; - PRInt64 sessionId; - nsCString spec; - nsString revHost; - bool hidden; - bool typed; - PRUint32 transitionType; - PRTime visitTime; -}; - - /** * Notifies observers about a visit. */ @@ -365,45 +367,16 @@ public: mozStorageTransaction transaction(mDBConn, PR_FALSE, mozIStorageConnection::TRANSACTION_IMMEDIATE); nsresult rv; - nsCOMPtr stmt; // If the page was in moz_places, we need to update the entry. if (known) { - NS_ASSERTION(mPlace.placeId > 0, "must have a valid place id!"); - - stmt = mHistory->syncStatements.GetCachedStatement( - "UPDATE moz_places " - "SET hidden = :hidden, typed = :typed " - "WHERE id = :page_id " - ); - NS_ENSURE_STATE(stmt); - rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPlace.placeId); + rv = mHistory->UpdatePlace(mPlace); NS_ENSURE_SUCCESS(rv, rv); } // Otherwise, the page was not in moz_places, so now we have to add it. else { - NS_ASSERTION(mPlace.placeId == 0, "should not have a valid place id!"); - - stmt = mHistory->syncStatements.GetCachedStatement( - "INSERT INTO moz_places " - "(url, rev_host, hidden, typed, guid) " - "VALUES (:page_url, :rev_host, :hidden, :typed, GENERATE_GUID()) " - ); - NS_ENSURE_STATE(stmt); - - rv = stmt->BindStringByName(NS_LITERAL_CSTRING("rev_host"), - mPlace.revHost); - NS_ENSURE_SUCCESS(rv, rv); - rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), mPlace.spec); + rv = mHistory->InsertPlace(mPlace); NS_ENSURE_SUCCESS(rv, rv); } - rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("typed"), mPlace.typed); - NS_ENSURE_SUCCESS(rv, rv); - rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), mPlace.hidden); - NS_ENSURE_SUCCESS(rv, rv); - - mozStorageStatementScoper scoper(stmt); - rv = stmt->Execute(); - NS_ENSURE_SUCCESS(rv, rv); rv = AddVisit(mPlace, mReferrer); NS_ENSURE_SUCCESS(rv, rv); @@ -967,6 +940,62 @@ History::GetIsVisitedStatement() return mIsVisitedStatement; } +nsresult +History::InsertPlace(const VisitData& aPlace) +{ + NS_PRECONDITION(aPlace.placeId == 0, "should not have a valid place id!"); + NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!"); + + nsCOMPtr stmt = syncStatements.GetCachedStatement( + "INSERT INTO moz_places " + "(url, rev_host, hidden, typed, guid) " + "VALUES (:page_url, :rev_host, :hidden, :typed, GENERATE_GUID()) " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("rev_host"), + aPlace.revHost); + NS_ENSURE_SUCCESS(rv, rv); + rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aPlace.spec); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("typed"), aPlace.typed); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), aPlace.hidden); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult +History::UpdatePlace(const VisitData& aPlace) +{ + NS_PRECONDITION(aPlace.placeId > 0, "must have a valid place id!"); + NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!"); + + nsCOMPtr stmt = syncStatements.GetCachedStatement( + "UPDATE moz_places " + "SET hidden = :hidden, typed = :typed " + "WHERE id = :page_id " + ); + NS_ENSURE_STATE(stmt); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), + aPlace.placeId); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("typed"), aPlace.typed); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), aPlace.hidden); + NS_ENSURE_SUCCESS(rv, rv); + rv = stmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + /* static */ History* History::GetService() diff --git a/toolkit/components/places/src/History.h b/toolkit/components/places/src/History.h index 17a660c5a07d..7cdf2edb7c10 100644 --- a/toolkit/components/places/src/History.h +++ b/toolkit/components/places/src/History.h @@ -54,6 +54,8 @@ namespace mozilla { namespace places { +struct VisitData; + #define NS_HISTORYSERVICE_CID \ {0x0937a705, 0x91a6, 0x417a, {0x82, 0x92, 0xb2, 0x2e, 0xb1, 0x0d, 0xa8, 0x6c}} @@ -80,6 +82,22 @@ public: */ mozIStorageAsyncStatement* GetIsVisitedStatement(); + /** + * Adds an entry in moz_places with the data in aVisitData. + * + * @param aVisitData + * The visit data to use to populate a new row in moz_places. + */ + nsresult InsertPlace(const VisitData& aVisitData); + + /** + * Updates an entry in moz_places with the data in aVisitData. + * + * @param aVisitData + * The visit data to use to update the existing row in moz_places. + */ + nsresult UpdatePlace(const VisitData& aVisitData); + /** * Obtains a pointer to this service. */ From fb4f3e856fad7193e94d8145f1e30b052b83490a Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Tue, 11 Jan 2011 10:48:53 -0800 Subject: [PATCH 08/23] Bug 606966 - Need an async history visit API exposed to JS Part 7 - Pull FetchPageInfo onto History, and make both of our current async operations use it (requires us to add title to VisitData). r=dietrich a=blocking --- toolkit/components/places/src/History.cpp | 165 ++++++++++------------ toolkit/components/places/src/History.h | 9 ++ 2 files changed, 80 insertions(+), 94 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index fa2ee06a9804..a241b98289b3 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -128,6 +128,7 @@ struct VisitData { bool typed; PRUint32 transitionType; PRTime visitTime; + nsString title; }; //////////////////////////////////////////////////////////////////////////////// @@ -344,7 +345,7 @@ public: NS_PRECONDITION(!NS_IsMainThread(), "This should not be called on the main thread"); - bool known = FetchPageInfo(mPlace); + bool known = mHistory->FetchPageInfo(mPlace); // If we had a referrer, we want to know about its last visit to put this // new visit into the same session. @@ -407,61 +408,6 @@ private: } } - /** - * Loads information about the page into _place from moz_places. - * - * @param _place - * The VisitData for the place we need to know information about. - * @return true if the page was recorded in moz_places, false otherwise. - */ - bool FetchPageInfo(VisitData& _place) - { - NS_PRECONDITION(!_place.spec.IsEmpty(), "must have a non-empty spec!"); - - nsCOMPtr stmt = - mHistory->syncStatements.GetCachedStatement( - "SELECT id, typed, hidden " - "FROM moz_places " - "WHERE url = :page_url " - ); - NS_ENSURE_TRUE(stmt, false); - mozStorageStatementScoper scoper(stmt); - - nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), - _place.spec); - NS_ENSURE_SUCCESS(rv, false); - - PRBool hasResult; - rv = stmt->ExecuteStep(&hasResult); - NS_ENSURE_SUCCESS(rv, false); - if (!hasResult) { - return false; - } - - rv = stmt->GetInt64(0, &_place.placeId); - NS_ENSURE_SUCCESS(rv, false); - - if (!_place.typed) { - // If this transition wasn't typed, others might have been. If database - // has location as typed, reflect that in our data structure. - PRInt32 typed; - rv = stmt->GetInt32(1, &typed); - _place.typed = !!typed; - NS_ENSURE_SUCCESS(rv, true); - } - if (_place.hidden) { - // If this transition was hidden, it is possible that others were not. - // Any one visible transition makes this location visible. If database - // has location as visible, reflect that in our data structure. - PRInt32 hidden; - rv = stmt->GetInt32(2, &hidden); - _place.hidden = !!hidden; - NS_ENSURE_SUCCESS(rv, true); - } - - return true; - } - /** * Loads visit information about the page into _place. * @@ -748,48 +694,26 @@ public: "This should not be called on the main thread"); // First, see if the page exists in the database (we'll need its id later). - nsCOMPtr stmt = - mHistory->syncStatements.GetCachedStatement( - "SELECT id, title " - "FROM moz_places " - "WHERE url = :page_url " - ); - NS_ENSURE_STATE(stmt); - - PRInt64 placeId = 0; - nsAutoString title; - { - mozStorageStatementScoper scoper(stmt); - nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), - mSpec); - NS_ENSURE_SUCCESS(rv, rv); - - PRBool hasResult; - rv = stmt->ExecuteStep(&hasResult); - NS_ENSURE_SUCCESS(rv, rv); - if (!hasResult) { - // We have no record of this page, so there is no need to do any further - // work. - return NS_OK; - } - - rv = stmt->GetInt64(0, &placeId); - NS_ENSURE_SUCCESS(rv, rv); - - rv = stmt->GetString(1, title); - NS_ENSURE_SUCCESS(rv, rv); + bool exists = mHistory->FetchPageInfo(mPlace); + if (!exists) { + // We have no record of this page, so there is no need to do any further + // work. + return NS_OK; } - NS_ASSERTION(placeId > 0, "We somehow have an invalid place id here!"); + NS_ASSERTION(mPlace.placeId > 0, + "We somehow have an invalid place id here!"); // Also, if we have the same title, there is no reason to do another write // or notify our observers, so bail early. - if (mTitle.Equals(title) || (mTitle.IsVoid() && title.IsVoid())) { + if (mTitle.Equals(mPlace.title) || + (mTitle.IsVoid() && mPlace.title.IsVoid())) { return NS_OK; } // Now we can update our database record. - stmt = mHistory->syncStatements.GetCachedStatement( + nsCOMPtr stmt = + mHistory->syncStatements.GetCachedStatement( "UPDATE moz_places " "SET title = :page_title " "WHERE id = :page_id " @@ -799,7 +723,7 @@ public: { mozStorageStatementScoper scoper(stmt); nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), - placeId); + mPlace.placeId); NS_ENSURE_SUCCESS(rv, rv); if (mTitle.IsVoid()) { rv = stmt->BindNullByName(NS_LITERAL_CSTRING("page_title")); @@ -813,7 +737,7 @@ public: NS_ENSURE_SUCCESS(rv, rv); } - nsCOMPtr event = new NotifyTitleObservers(mSpec, mTitle); + nsCOMPtr event = new NotifyTitleObservers(mPlace.spec, mTitle); nsresult rv = NS_DispatchToMainThread(event); NS_ENSURE_SUCCESS(rv, rv); @@ -823,13 +747,13 @@ public: private: SetPageTitle(const nsCString& aSpec, const nsString& aTitle) - : mSpec(aSpec) - , mTitle(aTitle) + : mTitle(aTitle) , mHistory(History::GetService()) { + mPlace.spec = aSpec; } - const nsCString mSpec; + VisitData mPlace; const nsString mTitle; /** @@ -996,6 +920,59 @@ History::UpdatePlace(const VisitData& aPlace) return NS_OK; } +bool +History::FetchPageInfo(VisitData& _place) +{ + NS_PRECONDITION(!_place.spec.IsEmpty(), "must have a non-empty spec!"); + NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!"); + + nsCOMPtr stmt = syncStatements.GetCachedStatement( + "SELECT id, typed, hidden, title " + "FROM moz_places " + "WHERE url = :page_url " + ); + NS_ENSURE_TRUE(stmt, false); + mozStorageStatementScoper scoper(stmt); + + nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), + _place.spec); + NS_ENSURE_SUCCESS(rv, false); + + PRBool hasResult; + rv = stmt->ExecuteStep(&hasResult); + NS_ENSURE_SUCCESS(rv, false); + if (!hasResult) { + return false; + } + + rv = stmt->GetInt64(0, &_place.placeId); + NS_ENSURE_SUCCESS(rv, false); + + if (!_place.typed) { + // If this transition wasn't typed, others might have been. If database + // has location as typed, reflect that in our data structure. + PRInt32 typed; + rv = stmt->GetInt32(1, &typed); + _place.typed = !!typed; + NS_ENSURE_SUCCESS(rv, true); + } + + if (_place.hidden) { + // If this transition was hidden, it is possible that others were not. + // Any one visible transition makes this location visible. If database + // has location as visible, reflect that in our data structure. + PRInt32 hidden; + rv = stmt->GetInt32(2, &hidden); + _place.hidden = !!hidden; + NS_ENSURE_SUCCESS(rv, true); + } + + rv = stmt->GetString(3, _place.title); + NS_ENSURE_SUCCESS(rv, true); + + return true; +} + /* static */ History* History::GetService() diff --git a/toolkit/components/places/src/History.h b/toolkit/components/places/src/History.h index 7cdf2edb7c10..74176a511c12 100644 --- a/toolkit/components/places/src/History.h +++ b/toolkit/components/places/src/History.h @@ -98,6 +98,15 @@ public: */ nsresult UpdatePlace(const VisitData& aVisitData); + /** + * Loads information about the page into _place from moz_places. + * + * @param _place + * The VisitData for the place we need to know information about. + * @return true if the page was recorded in moz_places, false otherwise. + */ + bool FetchPageInfo(VisitData& _place); + /** * Obtains a pointer to this service. */ From 59e51e47ffe9d085d1e0a158fe0a72f7126bf4d7 Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Tue, 11 Jan 2011 11:01:26 -0800 Subject: [PATCH 09/23] Bug 606966 - Need an async history visit API exposed to JS Part 8 - add a referrerSpec and guid property to VisitData. We'll need to potentially pass both of these in the new updatePlaces method. r=mak a=blocking --- toolkit/components/places/src/History.cpp | 28 ++++++++++++----------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index a241b98289b3..ee7deecc147e 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -86,9 +86,11 @@ struct VisitData { , transitionType(PR_UINT32_MAX) , visitTime(0) { + guid.SetIsVoid(PR_TRUE); } - VisitData(nsIURI* aURI) + VisitData(nsIURI* aURI, + nsIURI* aReferrer = NULL) : placeId(0) , visitId(0) , sessionId(0) @@ -99,6 +101,10 @@ struct VisitData { { (void)aURI->GetSpec(spec); (void)GetReversedHostname(aURI, revHost); + if (aReferrer) { + (void)aReferrer->GetSpec(referrerSpec); + } + guid.SetIsVoid(PR_TRUE); } /** @@ -120,6 +126,7 @@ struct VisitData { } PRInt64 placeId; + nsCString guid; PRInt64 visitId; PRInt64 sessionId; nsCString spec; @@ -129,6 +136,7 @@ struct VisitData { PRUint32 transitionType; PRTime visitTime; nsString title; + nsCString referrerSpec; }; //////////////////////////////////////////////////////////////////////////////// @@ -310,18 +318,15 @@ public: * The database connection to use for these operations. * @param aPlace * The location to record a visit. - * @param [optional] aReferrer - * The page that "referred" us to aPlace. */ static nsresult Start(mozIStorageConnection* aConnection, - VisitData& aPlace, - nsIURI* aReferrer = nsnull) + VisitData& aPlace) { NS_PRECONDITION(NS_IsMainThread(), "This should be called on the main thread"); nsRefPtr event = - new InsertVisitedURI(aConnection, aPlace, aReferrer); + new InsertVisitedURI(aConnection, aPlace); // Speculatively get a new session id for our visit. While it is true that // we will use the session id from the referrer if the visit was "recent" @@ -397,15 +402,12 @@ public: } private: InsertVisitedURI(mozIStorageConnection* aConnection, - VisitData& aPlace, - nsIURI* aReferrer) + VisitData& aPlace) : mDBConn(aConnection) , mPlace(aPlace) , mHistory(History::GetService()) { - if (aReferrer) { - (void)aReferrer->GetSpec(mReferrer.spec); - } + mReferrer.spec = mPlace.referrerSpec; } /** @@ -1085,7 +1087,7 @@ History::VisitURI(nsIURI* aURI, } } - VisitData place(aURI); + VisitData place(aURI, aLastVisitedURI); NS_ENSURE_FALSE(place.spec.IsEmpty(), NS_ERROR_INVALID_ARG); place.visitTime = PR_Now(); @@ -1140,7 +1142,7 @@ History::VisitURI(nsIURI* aURI, mozIStorageConnection* dbConn = GetDBConn(); NS_ENSURE_STATE(dbConn); - rv = InsertVisitedURI::Start(dbConn, place, aLastVisitedURI); + rv = InsertVisitedURI::Start(dbConn, place); NS_ENSURE_SUCCESS(rv, rv); } From 080b66d5298c7f364cc6661bec3bd610819b908b Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Tue, 11 Jan 2011 11:03:22 -0800 Subject: [PATCH 10/23] Bug 606966 - Need an async history visit API exposed to JS Part 9 - Move referrer loading logic into a helper method. r=dietrich a=blocking --- toolkit/components/places/src/History.cpp | 53 +++++++++++++++-------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index ee7deecc147e..6b0f6f400b72 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -352,23 +352,7 @@ public: bool known = mHistory->FetchPageInfo(mPlace); - // If we had a referrer, we want to know about its last visit to put this - // new visit into the same session. - if (!mReferrer.spec.IsEmpty()) { - bool recentVisit = FetchVisitInfo(mReferrer, mPlace.visitTime); - // At this point, we know the referrer's session id, which this new visit - // should also share. - if (recentVisit) { - mPlace.sessionId = mReferrer.sessionId; - } - // However, if it isn't recent enough, we don't care to log anything about - // the referrer and we'll start a new session. - else { - // This is sufficient to ignore our referrer. This behavior has test - // coverage, so if this invariant changes, we'll know. - mReferrer.visitId = 0; - } - } + FetchReferrerInfo(mReferrer, mPlace); mozStorageTransaction transaction(mDBConn, PR_FALSE, mozIStorageConnection::TRANSACTION_IMMEDIATE); @@ -464,6 +448,41 @@ private: return false; } + /** + * Fetches information about a referrer and sets the session id for aPlace if + * it was a recent visit or not. + * + * @param aReferrer + * The VisitData for the referrer. This will be populated with + * FetchVisitInfo. + * @param aPlace + * The VisitData for the visit we will eventually add. + * + */ + void FetchReferrerInfo(VisitData& aReferrer, + VisitData& aPlace) + { + if (aReferrer.spec.IsEmpty()) { + return; + } + + // If we had a referrer, we want to know about its last visit to put this + // new visit into the same session. + bool recentVisit = FetchVisitInfo(aReferrer, aPlace.visitTime); + // At this point, we know the referrer's session id, which this new visit + // should also share. + if (recentVisit) { + aPlace.sessionId = aReferrer.sessionId; + } + // However, if it isn't recent enough, we don't care to log anything about + // the referrer and we'll start a new session. + else { + // This is sufficient to ignore our referrer. This behavior has test + // coverage, so if this invariant changes, we'll know. + aReferrer.visitId = 0; + } + } + /** * Adds a visit for _place and updates it with the right visit id. * From 82fbc721e965b05d5bd324a0d93109923c890dbb Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Tue, 11 Jan 2011 11:13:17 -0800 Subject: [PATCH 11/23] Bug 606966 - Need an async history visit API exposed to JS Part 10 - Refactor InsertVistedURI to InsertVisitedURIs and have it take an array of VisitData objects to add. r=mak a=blocking --- toolkit/components/places/src/History.cpp | 135 ++++++++++++++-------- 1 file changed, 87 insertions(+), 48 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index 6b0f6f400b72..544189cbdba8 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -125,6 +125,19 @@ struct VisitData { transitionType = aTransitionType; } + /** + * Determines if this refers to the same url as aOther. + * + * @param aOther + * The other place to check against. + * @return true if this is a visit for the same place as aOther, false + * otherwise. + */ + bool IsSamePlaceAs(const VisitData& aOther) const + { + return spec.Equals(aOther.spec); + } + PRInt64 placeId; nsCString guid; PRInt64 visitId; @@ -308,7 +321,7 @@ private: /** * Adds a visit to the database. */ -class InsertVisitedURI : public nsRunnable +class InsertVisitedURIs : public nsRunnable { public: /** @@ -316,25 +329,18 @@ public: * * @param aConnection * The database connection to use for these operations. - * @param aPlace - * The location to record a visit. + * @param aPlaces + * The locations to record visits. */ static nsresult Start(mozIStorageConnection* aConnection, - VisitData& aPlace) + nsTArray& aPlaces) { NS_PRECONDITION(NS_IsMainThread(), "This should be called on the main thread"); + NS_PRECONDITION(aPlaces.Length() > 0, "Must pass a non-empty array!"); - nsRefPtr event = - new InsertVisitedURI(aConnection, aPlace); - - // Speculatively get a new session id for our visit. While it is true that - // we will use the session id from the referrer if the visit was "recent" - // enough, we cannot call this method off of the main thread, so we have to - // consume an id now. - nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); - NS_ENSURE_TRUE(navHistory, NS_ERROR_UNEXPECTED); - event->mPlace.sessionId = navHistory->GetNewSessionID(); + nsRefPtr event = + new InsertVisitedURIs(aConnection, aPlaces); // Get the target thread, and then start the work! nsCOMPtr target = do_GetInterface(aConnection); @@ -350,48 +356,78 @@ public: NS_PRECONDITION(!NS_IsMainThread(), "This should not be called on the main thread"); - bool known = mHistory->FetchPageInfo(mPlace); - - FetchReferrerInfo(mReferrer, mPlace); - mozStorageTransaction transaction(mDBConn, PR_FALSE, mozIStorageConnection::TRANSACTION_IMMEDIATE); + + const VisitData* lastPlace; nsresult rv; - // If the page was in moz_places, we need to update the entry. - if (known) { - rv = mHistory->UpdatePlace(mPlace); - NS_ENSURE_SUCCESS(rv, rv); - } - // Otherwise, the page was not in moz_places, so now we have to add it. - else { - rv = mHistory->InsertPlace(mPlace); - NS_ENSURE_SUCCESS(rv, rv); - } + for (nsTArray::size_type i = 0; i < mPlaces.Length(); i++) { + VisitData& place = mPlaces.ElementAt(i); + VisitData& referrer = mReferrers.ElementAt(i); - rv = AddVisit(mPlace, mReferrer); - NS_ENSURE_SUCCESS(rv, rv); + // We can avoid a database lookup if it's the same place as the last + // visit we added. + bool known = (lastPlace && lastPlace->IsSamePlaceAs(place)) || + mHistory->FetchPageInfo(place); - rv = UpdateFrecency(mPlace); - NS_ENSURE_SUCCESS(rv, rv); + FetchReferrerInfo(referrer, place); + + // If the page was in moz_places, we need to update the entry. + if (known) { + rv = mHistory->UpdatePlace(place); + NS_ENSURE_SUCCESS(rv, rv); + } + // Otherwise, the page was not in moz_places, so now we have to add it. + else { + rv = mHistory->InsertPlace(place); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = AddVisit(place, referrer); + NS_ENSURE_SUCCESS(rv, rv); + + // TODO (bug 623969) we shouldn't update this after each visit, but + // rather only for each unique place to save disk I/O. + rv = UpdateFrecency(place); + NS_ENSURE_SUCCESS(rv, rv); + + // Dispatch an event to the main thread to notify observers. + nsCOMPtr event = new NotifyVisitObservers(place, referrer); + rv = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv, rv); + + lastPlace = &mPlaces.ElementAt(i); + } rv = transaction.Commit(); NS_ENSURE_SUCCESS(rv, rv); - // Finally, dispatch an event to the main thread to notify observers. - nsCOMPtr event = new NotifyVisitObservers(mPlace, mReferrer); - rv = NS_DispatchToMainThread(event); - NS_ENSURE_SUCCESS(rv, rv); - return NS_OK; } private: - InsertVisitedURI(mozIStorageConnection* aConnection, - VisitData& aPlace) + InsertVisitedURIs(mozIStorageConnection* aConnection, + nsTArray& aPlaces) : mDBConn(aConnection) - , mPlace(aPlace) , mHistory(History::GetService()) { - mReferrer.spec = mPlace.referrerSpec; + NS_PRECONDITION(NS_IsMainThread(), + "This should be called on the main thread"); + + (void)mPlaces.SwapElements(aPlaces); + (void)mReferrers.SetLength(mPlaces.Length()); + + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + NS_ABORT_IF_FALSE(navHistory, "Could not get nsNavHistory?!"); + + for (nsTArray::size_type i = 0; i < mPlaces.Length(); i++) { + mReferrers[i].spec = mPlaces[i].referrerSpec; + + // Speculatively get a new session id for our visit. While it is true + // that we will use the session id from the referrer if the visit was + // "recent" enough, we cannot call this method off of the main thread, so + // we have to consume an id now. + mPlaces[i].sessionId = navHistory->GetNewSessionID(); + } } /** @@ -503,7 +539,7 @@ private: "VALUES (:from_visit, :page_id, :visit_date, :visit_type, :session) " ); NS_ENSURE_STATE(stmt); - rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPlace.placeId); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), _place.placeId); NS_ENSURE_SUCCESS(rv, rv); } else { @@ -565,7 +601,7 @@ private: "WHERE id = :page_id" ); NS_ENSURE_STATE(stmt); - rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPlace.placeId); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId); NS_ENSURE_SUCCESS(rv, rv); } else { @@ -594,7 +630,7 @@ private: "WHERE id = :page_id AND frecency <> 0" ); NS_ENSURE_STATE(stmt); - rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), mPlace.placeId); + rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId); NS_ENSURE_SUCCESS(rv, rv); } else { @@ -618,8 +654,8 @@ private: mozIStorageConnection* mDBConn; - VisitData mPlace; - VisitData mReferrer; + nsTArray mPlaces; + nsTArray mReferrers; /** * Strong reference to the History object because we do not want it to @@ -1106,7 +1142,10 @@ History::VisitURI(nsIURI* aURI, } } - VisitData place(aURI, aLastVisitedURI); + nsTArray placeArray(1); + NS_ENSURE_TRUE(placeArray.AppendElement(VisitData(aURI, aLastVisitedURI)), + NS_ERROR_OUT_OF_MEMORY); + VisitData& place = placeArray.ElementAt(0); NS_ENSURE_FALSE(place.spec.IsEmpty(), NS_ERROR_INVALID_ARG); place.visitTime = PR_Now(); @@ -1161,7 +1200,7 @@ History::VisitURI(nsIURI* aURI, mozIStorageConnection* dbConn = GetDBConn(); NS_ENSURE_STATE(dbConn); - rv = InsertVisitedURI::Start(dbConn, place); + rv = InsertVisitedURIs::Start(dbConn, placeArray); NS_ENSURE_SUCCESS(rv, rv); } From ee86060ac0a49769d7b76ee3013e7b65ec910adf Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Wed, 12 Jan 2011 10:22:39 -0800 Subject: [PATCH 12/23] Bug 606966 - Need an async history visit API exposed to JS Part 11 - Refactor embed visit adding into a helper method. r=mak a=blocking --- toolkit/components/places/src/History.cpp | 34 +++++++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index 544189cbdba8..ea0ef906d5bb 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -820,6 +820,33 @@ private: nsRefPtr mHistory; }; +/** + * Stores an embed visit, and notifies observers. + * + * @param aPlace + * The VisitData of the visit to store as an embed visit. + */ +void +StoreAndNotifyEmbedVisit(VisitData& aPlace) +{ + NS_PRECONDITION(aPlace.transitionType == nsINavHistoryService::TRANSITION_EMBED, + "Must only pass TRANSITION_EMBED visits to this!"); + NS_PRECONDITION(NS_IsMainThread(), "Must be called on the main thread!"); + + nsCOMPtr uri; + (void)NS_NewURI(getter_AddRefs(uri), aPlace.spec); + + nsNavHistory* navHistory = nsNavHistory::GetHistoryService(); + if (!navHistory || !uri) { + return; + } + + navHistory->registerEmbedVisit(uri, aPlace.visitTime); + VisitData noReferrer; + nsCOMPtr event = new NotifyVisitObservers(aPlace, noReferrer); + (void)NS_DispatchToMainThread(event); +} + } // anonymous namespace //////////////////////////////////////////////////////////////////////////////// @@ -1189,12 +1216,7 @@ History::VisitURI(nsIURI* aURI, // EMBED visits are session-persistent and should not go through the database. // They exist only to keep track of isVisited status during the session. if (place.transitionType == nsINavHistoryService::TRANSITION_EMBED) { - navHistory->registerEmbedVisit(aURI, place.visitTime); - // Finally, enqueue an event to notify observers. - VisitData noReferrer; - nsCOMPtr event = new NotifyVisitObservers(place, noReferrer); - rv = NS_DispatchToMainThread(event); - NS_ENSURE_SUCCESS(rv, rv); + StoreAndNotifyEmbedVisit(place); } else { mozIStorageConnection* dbConn = GetDBConn(); From bf35f30bd627051268db563cc4353ab207379ab4 Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Wed, 12 Jan 2011 10:22:55 -0800 Subject: [PATCH 13/23] Bug 606966 - Need an async history visit API exposed to JS Part 12 - InsertVisitedURIs should handle adding multiple visits to the same place. Fixes a minor bug in Part 10. r=mak a=blocking --- toolkit/components/places/src/History.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index ea0ef906d5bb..e1862dd10873 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -126,16 +126,23 @@ struct VisitData { } /** - * Determines if this refers to the same url as aOther. + * Determines if this refers to the same url as aOther, and updates aOther + * with missing information if so. * * @param aOther * The other place to check against. * @return true if this is a visit for the same place as aOther, false * otherwise. */ - bool IsSamePlaceAs(const VisitData& aOther) const + bool IsSamePlaceAs(VisitData& aOther) { - return spec.Equals(aOther.spec); + if (!spec.Equals(aOther.spec)) { + return false; + } + + aOther.placeId = placeId; + aOther.guid = guid; + return true; } PRInt64 placeId; @@ -359,7 +366,7 @@ public: mozStorageTransaction transaction(mDBConn, PR_FALSE, mozIStorageConnection::TRANSACTION_IMMEDIATE); - const VisitData* lastPlace; + VisitData* lastPlace; nsresult rv; for (nsTArray::size_type i = 0; i < mPlaces.Length(); i++) { VisitData& place = mPlaces.ElementAt(i); From b98d708920c4d501932b11802f343d48914b7615 Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Thu, 13 Jan 2011 13:37:02 -0800 Subject: [PATCH 14/23] Bug 606966 - Need an async history visit API exposed to JS Part 1 - Public interfaces for the new API r=mak sr=rs a=blocking --- toolkit/components/places/public/Makefile.in | 1 + .../places/public/mozIAsyncHistory.idl | 163 ++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 toolkit/components/places/public/mozIAsyncHistory.idl diff --git a/toolkit/components/places/public/Makefile.in b/toolkit/components/places/public/Makefile.in index dcd73dca6410..de610b412308 100644 --- a/toolkit/components/places/public/Makefile.in +++ b/toolkit/components/places/public/Makefile.in @@ -64,6 +64,7 @@ XPIDLSRCS += \ mozIPlacesAutoComplete.idl \ nsIMicrosummaryService.idl \ nsIPlacesImportExportService.idl \ + mozIAsyncHistory.idl \ $(NULL) endif diff --git a/toolkit/components/places/public/mozIAsyncHistory.idl b/toolkit/components/places/public/mozIAsyncHistory.idl new file mode 100644 index 000000000000..bed4602a3060 --- /dev/null +++ b/toolkit/components/places/public/mozIAsyncHistory.idl @@ -0,0 +1,163 @@ +/* ***** 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 Places Async History API. + * + * The Initial Developer of the Original Code is + * the Mozilla Foundation. + * Portions created by the Initial Developer are Copyright (C) 2010 + * the Initial Developer. All Rights Reserved. + * + * Contributor(s): + * Shawn Wilsher + * + * 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 "nsISupports.idl" + +interface nsIURI; +interface nsIVariant; + +%{C++ +#include "jsapi.h" +%} + +[scriptable, uuid(1a3b1260-4bdb-45d0-a306-dc377dd9baa4)] +interface mozIVisitInfo : nsISupports +{ + /** + * The machine-local (internal) id of the visit. + */ + readonly attribute long long visitId; + + /** + * The time the visit occurred. + */ + readonly attribute PRTime visitDate; + + /** + * The transition type used to get to this visit. One of the TRANSITION_TYPE + * constants on nsINavHistory. + * + * @see nsINavHistory.idl + */ + readonly attribute unsigned long transitionType; + + /** + * The referring URI of this visit. This may be null. + */ + readonly attribute nsIURI referrerURI; + + /** + * The sessionId of this visit. + * + * @see nsINavHistory.idl + */ + readonly attribute long long sessionId; +}; + +[scriptable, uuid(ad83e137-c92a-4b7b-b67e-0a318811f91e)] +interface mozIPlaceInfo : nsISupports +{ + /** + * The machine-local (internal) id of the place. + */ + readonly attribute long long placeId; + + /** + * The globally unique id of the place. + */ + readonly attribute ACString guid; + + /** + * The URI of the place. + */ + readonly attribute nsIURI uri; + + /** + * The title associated with the place. + */ + readonly attribute AString title; + + /** + * The frecency of the place. + */ + readonly attribute long long frecency; + + /** + * An array of mozIVisitInfo objects for the place. + */ + [implicit_jscontext] + readonly attribute jsval visits; +}; + +[scriptable,function, uuid(3b97ca3c-5ea8-424f-b429-797477c52302)] +interface mozIVisitInfoCallback : nsISupports +{ + /** + * Called for each visit added, title change, or guid change when passed to + * mozIAsyncHistory::updatePlaces. + * + * @param aResultCode + * nsresult of the change indicating success or failure reason. + * @param aPlaceInfo + * The information that was being entered into the database. + */ + void onComplete(in nsresult aResultCode, + in mozIPlaceInfo aPlaceInfo); +}; + +[scriptable, uuid(f79ca67c-7e57-4511-a400-ea31001c762f)] +interface mozIAsyncHistory : nsISupports +{ + /** + * Adds a set of visits for one or more mozIPlaceInfo objects, and updates + * each mozIPlaceInfo's title or guid. It is not necessary to add visits to + * change a Place's title or guid. + * + * @param aPlaceInfo + * The mozIPlaceInfo object[s] containing the information to store or + * update. This can be a single object, or an array of objects. + * @param [optional] aCallback + * Callback to be notified for each visit addition, title change, or + * guid change. If more than one operation is done for a given place, + * only one callback will be given (i.e. title change and add visit). + * + * @throws NS_ERROR_INVALID_ARG + * - Passing in NULL for aPlaceInfo. + * - Not providing at least one valid guid, placeId, or uri for all + * mozIPlaceInfo object[s]. + * - Not providing an array or nothing for the visits property of + * mozIPlaceInfo (the property can be undefined for title or guid + * changes). + * - Not providing a visitDate and transitionType for each + * mozIVisitInfo. + * - Providing an invalid transitionType for a mozIVisitInfo. + */ + [implicit_jscontext] + void updatePlaces(in jsval aPlaceInfo, + [optional] in mozIVisitInfoCallback aCallback); + +}; From 64111382894f39cf192a43748cda40cf715733f2 Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Thu, 13 Jan 2011 13:37:04 -0800 Subject: [PATCH 15/23] Bug 606966 - Need an async history visit API exposed to JS Part 3 - Stub out the methods on mozilla::places::History. r=mak a=blocking --- toolkit/components/places/src/History.cpp | 16 ++++++++++- toolkit/components/places/src/History.h | 5 +++- .../tests/unit/test_async_history_api.js | 28 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 toolkit/components/places/tests/unit/test_async_history_api.js diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index e1862dd10873..1dc82fe6ab41 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -1402,6 +1402,19 @@ History::SetURITitle(nsIURI* aURI, const nsAString& aTitle) return NS_OK; } +//////////////////////////////////////////////////////////////////////////////// +//// mozIAsyncHistory + +NS_IMETHODIMP +History::UpdatePlaces(const jsval& aPlaceInfos, + mozIVisitInfoCallback* aCallback, + JSContext* aCtx) +{ + NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_UNEXPECTED); + + return NS_ERROR_NOT_IMPLEMENTED; +} + //////////////////////////////////////////////////////////////////////////////// //// nsIObserver @@ -1424,9 +1437,10 @@ History::Observe(nsISupports* aSubject, const char* aTopic, //////////////////////////////////////////////////////////////////////////////// //// nsISupports -NS_IMPL_THREADSAFE_ISUPPORTS2( +NS_IMPL_THREADSAFE_ISUPPORTS3( History , IHistory +, mozIAsyncHistory , nsIObserver ) diff --git a/toolkit/components/places/src/History.h b/toolkit/components/places/src/History.h index 74176a511c12..25c8de5a4a6b 100644 --- a/toolkit/components/places/src/History.h +++ b/toolkit/components/places/src/History.h @@ -16,7 +16,7 @@ * The Original Code is Places code. * * The Initial Developer of the Original Code is - * Mozilla Foundation. + * the Mozilla Foundation. * Portions created by the Initial Developer are Copyright (C) 2009 * the Initial Developer. All Rights Reserved. * @@ -41,6 +41,7 @@ #define mozilla_places_History_h_ #include "mozilla/IHistory.h" +#include "mozIAsyncHistory.h" #include "mozilla/dom/Link.h" #include "nsTHashtable.h" #include "nsString.h" @@ -60,11 +61,13 @@ struct VisitData; {0x0937a705, 0x91a6, 0x417a, {0x82, 0x92, 0xb2, 0x2e, 0xb1, 0x0d, 0xa8, 0x6c}} class History : public IHistory + , public mozIAsyncHistory , public nsIObserver { public: NS_DECL_ISUPPORTS NS_DECL_IHISTORY + NS_DECL_MOZIASYNCHISTORY NS_DECL_NSIOBSERVER History(); diff --git a/toolkit/components/places/tests/unit/test_async_history_api.js b/toolkit/components/places/tests/unit/test_async_history_api.js new file mode 100644 index 000000000000..f9873d81c78e --- /dev/null +++ b/toolkit/components/places/tests/unit/test_async_history_api.js @@ -0,0 +1,28 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * This file tests the async history API exposed by mozIAsyncHistory. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +function test_interface_exists() +{ + let history = Cc["@mozilla.org/browser/history;1"].getService(Ci.nsISupports); + do_check_true(history instanceof Ci.mozIAsyncHistory); + run_next_test(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Test Runner + +let gTests = [ + test_interface_exists, +]; + +function run_test() +{ + run_next_test(); +} From 1f57fe2898ad117987f5ed2dfeb3374bed118444 Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Fri, 14 Jan 2011 02:33:33 +0100 Subject: [PATCH 16/23] Bug 621274 - Location Bar remembers invalid (Server not found) URLs. r=sdwilsh a=blocker --- browser/components/feeds/src/FeedWriter.js | 98 ++++++------------- .../places/src/AsyncFaviconHelpers.cpp | 8 +- .../components/places/tests/head_common.js | 31 +++++- .../unit/test_doSetAndLoadFaviconForPage.js | 10 +- ...est_doSetAndLoadFaviconForPage_failures.js | 34 ++++--- 5 files changed, 92 insertions(+), 89 deletions(-) diff --git a/browser/components/feeds/src/FeedWriter.js b/browser/components/feeds/src/FeedWriter.js index 2c1b5b47952b..ffa248407941 100644 --- a/browser/components/feeds/src/FeedWriter.js +++ b/browser/components/feeds/src/FeedWriter.js @@ -1036,10 +1036,6 @@ FeedWriter.prototype = { Cu.evalInSandbox(codeStr, this._contentSandbox); - var historySvc = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - historySvc.addObserver(this, false); - // List of web handlers var wccr = Cc["@mozilla.org/embeddor.implemented/web-content-handler-registrar;1"]. getService(Ci.nsIWebContentConverterService); @@ -1056,15 +1052,7 @@ FeedWriter.prototype = { codeStr = "handlersMenuPopup.appendChild(menuItem);"; Cu.evalInSandbox(codeStr, this._contentSandbox); - // For privacy reasons we cannot set the image attribute directly - // to the icon url, see Bug 358878 - var uri = makeURI(handlers[i].uri); - if (!this._setFaviconForWebReader(uri, menuItem)) { - if (uri && /^https?/.test(uri.scheme)) { - var iconURL = makeURI(uri.prePath + "/favicon.ico"); - this._faviconService.setAndLoadFaviconForPage(uri, iconURL, true); - } - } + this._setFaviconForWebReader(handlers[i].uri, menuItem); } this._contentSandbox.menuItem = null; } @@ -1235,10 +1223,6 @@ FeedWriter.prototype = { this.__bundle = null; this._feedURI = null; this.__contentSandbox = null; - - var historySvc = Cc["@mozilla.org/browser/nav-history-service;1"]. - getService(Ci.nsINavHistoryService); - historySvc.removeObserver(this); }, _removeFeedFromCache: function FW__removeFeedFromCache() { @@ -1358,65 +1342,41 @@ FeedWriter.prototype = { }, /** - * Sets the icon for the given web-reader item in the readers menu - * if the favicon-service has the necessary icon stored. - * @param aURI - * the reader URI. + * Sets the icon for the given web-reader item in the readers menu. + * The icon is fetched and stored through the favicon service. + * + * @param aReaderUrl + * the reader url. * @param aMenuItem * the reader item in the readers menulist. - * @return true if the icon was set, false otherwise. + * + * @note For privacy reasons we cannot set the image attribute directly + * to the icon url. See Bug 358878 for details. */ _setFaviconForWebReader: - function FW__setFaviconForWebReader(aURI, aMenuItem) { - var faviconsSvc = this._faviconService; - var faviconURI = null; - try { - faviconURI = faviconsSvc.getFaviconForPage(aURI); + function FW__setFaviconForWebReader(aReaderUrl, aMenuItem) { + var readerURI = makeURI(aReaderUrl); + if (!/^https?/.test(readerURI.scheme)) { + // Don't try to get a favicon for non http(s) URIs. + return; } - catch(ex) { } - - if (faviconURI) { - var dataURL = faviconsSvc.getFaviconDataAsDataURL(faviconURI); - if (dataURL) { - this._contentSandbox.menuItem = aMenuItem; - this._contentSandbox.dataURL = dataURL; - var codeStr = "menuItem.setAttribute('image', dataURL);"; - Cu.evalInSandbox(codeStr, this._contentSandbox); - this._contentSandbox.menuItem = null; - this._contentSandbox.dataURL = null; - - return true; - } - } - - return false; + var faviconURI = makeURI(readerURI.prePath + "/favicon.ico"); + var self = this; + this._faviconService.setAndLoadFaviconForPage(readerURI, faviconURI, false, + function (aURI, aDataLen, aData, aMimeType) { + if (aDataLen > 0) { + var dataURL = "data:" + aMimeType + ";base64," + + btoa(String.fromCharCode.apply(null, aData)); + self._contentSandbox.menuItem = aMenuItem; + self._contentSandbox.dataURL = dataURL; + var codeStr = "menuItem.setAttribute('image', dataURL);"; + Cu.evalInSandbox(codeStr, self._contentSandbox); + self._contentSandbox.menuItem = null; + self._contentSandbox.dataURL = null; + } + }); }, - // nsINavHistoryService - onPageChanged: function FW_onPageChanged(aURI, aWhat, aValue) { - if (aWhat == Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON) { - // Go through the readers menu and look for the corresponding - // reader menu-item for the page if any. - var spec = aURI.spec; - var possibleHandlers = this._handlersMenuList.firstChild.childNodes; - for (var i=0; i < possibleHandlers.length ; i++) { - if (possibleHandlers[i].getAttribute("webhandlerurl") == spec) { - this._setFaviconForWebReader(aURI, possibleHandlers[i]); - return; - } - } - } - }, - - onBeginUpdateBatch: function() { }, - onEndUpdateBatch: function() { }, - onVisit: function() { }, - onTitleChanged: function() { }, - onBeforeDeleteURI: function() { }, - onDeleteURI: function() { }, - onClearHistory: function() { }, - onDeleteVisits: function() { }, - // nsIClassInfo getInterfaces: function FW_getInterfaces(countRef) { var interfaces = [Ci.nsIFeedWriter, Ci.nsIClassInfo, Ci.nsISupports]; diff --git a/toolkit/components/places/src/AsyncFaviconHelpers.cpp b/toolkit/components/places/src/AsyncFaviconHelpers.cpp index 2df8ad6acf2d..1ab4ab77ad23 100644 --- a/toolkit/components/places/src/AsyncFaviconHelpers.cpp +++ b/toolkit/components/places/src/AsyncFaviconHelpers.cpp @@ -406,8 +406,8 @@ AsyncFetchAndSetIconForPage::start(nsIURI* aFaviconURI, // In future evaluate to store a resample of the image. For now avoid that // for database size concerns. // Don't store favicons for error pages too. - if (page.spec.Equals(icon.spec) || - page.spec.Equals(FAVICON_ERRORPAGE_URL)) { + if (icon.spec.Equals(page.spec) || + icon.spec.Equals(FAVICON_ERRORPAGE_URL)) { return NS_OK; } @@ -754,8 +754,8 @@ AsyncAssociateIconToPage::Run() if (mPage.id == 0) { nsCOMPtr stmt = mFaviconSvc->mSyncStatements.GetCachedStatement(NS_LITERAL_CSTRING( - "INSERT INTO moz_places (url, rev_host, favicon_id, guid) " - "VALUES (:page_url, :rev_host, :favicon_id, GENERATE_GUID()) " + "INSERT INTO moz_places (url, rev_host, hidden, favicon_id, guid) " + "VALUES (:page_url, :rev_host, 1, :favicon_id, GENERATE_GUID()) " )); NS_ENSURE_STATE(stmt); mozStorageStatementScoper scoper(stmt); diff --git a/toolkit/components/places/tests/head_common.js b/toolkit/components/places/tests/head_common.js index a118ea473449..7bbd0b8ce64a 100644 --- a/toolkit/components/places/tests/head_common.js +++ b/toolkit/components/places/tests/head_common.js @@ -50,6 +50,10 @@ const TRANSITION_REDIRECT_PERMANENT = Ci.nsINavHistoryService.TRANSITION_REDIREC const TRANSITION_REDIRECT_TEMPORARY = Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY; const TRANSITION_DOWNLOAD = Ci.nsINavHistoryService.TRANSITION_DOWNLOAD; +// This error icon must stay in sync with FAVICON_ERRORPAGE_URL in +// nsIFaviconService.idl, aboutCertError.xhtml and netError.xhtml. +const FAVICON_ERRORPAGE_URL = "chrome://global/skin/icons/warning-16.png"; + Cu.import("resource://gre/modules/XPCOMUtils.jsm"); XPCOMUtils.defineLazyGetter(this, "Services", function() { @@ -495,8 +499,8 @@ function waitForFrecency(aURI, aValidator, aCallback, aCbScope, aCbArguments) { /** * Returns the frecency of a url. * - * @param aURI - * The URI or spec to get frecency for. + * @param aURI + * The URI or spec to get frecency for. * @return the frecency value. */ function frecencyForUrl(aURI) @@ -507,13 +511,34 @@ function frecencyForUrl(aURI) ); stmt.bindUTF8StringParameter(0, url); if (!stmt.executeStep()) - throw "No result for frecency."; + throw new Error("No result for frecency."); let frecency = stmt.getInt32(0); stmt.finalize(); return frecency; } +/** + * Returns the hidden status of a url. + * + * @param aURI + * The URI or spec to get hidden for. + * @return @return true if the url is hidden, false otherwise. + */ +function isUrlHidden(aURI) +{ + let url = aURI instanceof Ci.nsIURI ? aURI.spec : aURI; + let stmt = DBConn().createStatement( + "SELECT hidden FROM moz_places WHERE url = ?1" + ); + stmt.bindUTF8StringParameter(0, url); + if (!stmt.executeStep()) + throw new Error("No result for hidden."); + let hidden = stmt.getInt32(0); + stmt.finalize(); + + return !!hidden; +} /** * Compares two times in usecs, considering eventual platform timers skews. diff --git a/toolkit/components/places/tests/unit/test_doSetAndLoadFaviconForPage.js b/toolkit/components/places/tests/unit/test_doSetAndLoadFaviconForPage.js index e18bb492cf5f..90c45a20b679 100644 --- a/toolkit/components/places/tests/unit/test_doSetAndLoadFaviconForPage.js +++ b/toolkit/components/places/tests/unit/test_doSetAndLoadFaviconForPage.js @@ -78,6 +78,8 @@ var tests = [ }, check: function check1() { checkAddSucceeded(this.pageURI, this.favicon.mimetype, this.favicon.data); + do_log_info("Check that the added page is marked as hidden."); + do_check_true(isUrlHidden(this.pageURI)); } }, @@ -164,10 +166,13 @@ var historyObserver = { if (pageURI.equals(tests[currentTestIndex].pageURI)) { tests[currentTestIndex].check(); currentTestIndex++; - if (currentTestIndex == tests.length) + if (currentTestIndex == tests.length) { do_test_finished(); - else + } + else { + do_log_info(tests[currentTestIndex].desc); tests[currentTestIndex].go(); + } } else do_throw("Received PageChanged for a non-current test!"); @@ -192,5 +197,6 @@ function run_test() { PlacesUtils.history.addObserver(historyObserver, false); // Start the tests + do_log_info(tests[currentTestIndex].desc); tests[currentTestIndex].go(); }; diff --git a/toolkit/components/places/tests/unit/test_doSetAndLoadFaviconForPage_failures.js b/toolkit/components/places/tests/unit/test_doSetAndLoadFaviconForPage_failures.js index e63f663879e9..ee9ec3ad36c7 100644 --- a/toolkit/components/places/tests/unit/test_doSetAndLoadFaviconForPage_failures.js +++ b/toolkit/components/places/tests/unit/test_doSetAndLoadFaviconForPage_failures.js @@ -50,13 +50,13 @@ XPCOMUtils.defineLazyServiceGetter(this, "pb", let favicons = [ { - uri: uri(do_get_file("favicon-normal16.png")), + uri: NetUtil.newURI(do_get_file("favicon-normal16.png")), data: readFileData(do_get_file("favicon-normal16.png")), mimetype: "image/png", size: 286 }, { - uri: uri(do_get_file("favicon-normal32.png")), + uri: NetUtil.newURI(do_get_file("favicon-normal32.png")), data: readFileData(do_get_file("favicon-normal32.png")), mimetype: "image/png", size: 344 @@ -67,7 +67,7 @@ let tests = [ { desc: "test setAndLoadFaviconForPage for about: URIs", - pageURI: uri("about:test1"), + pageURI: NetUtil.newURI("about:test1"), go: function go1() { PlacesUtils.favicons.setAndLoadFaviconForPage(this.pageURI, favicons[0].uri, true); @@ -77,7 +77,7 @@ let tests = [ { desc: "test setAndLoadFaviconForPage with history disabled", - pageURI: uri("http://test2.bar/"), + pageURI: NetUtil.newURI("http://test2.bar/"), go: function go2() { // Temporarily disable history. Services.prefs.setBoolPref("places.history.enabled", false); @@ -91,7 +91,7 @@ let tests = [ { desc: "test setAndLoadFaviconForPage in PB mode for non-bookmarked URI", - pageURI: uri("http://test3.bar/"), + pageURI: NetUtil.newURI("http://test3.bar/"), go: function go3() { if (!("@mozilla.org/privatebrowsing;1" in Cc)) return; @@ -110,15 +110,27 @@ let tests = [ } }, - { // This is a valid icon set test, that will cause the closing notification. - desc: "test setAndLoadFaviconForPage for valid history uri", - pageURI: uri("http://test4.bar/"), + { + desc: "test setAndLoadFaviconForPage with error icon", + pageURI: NetUtil.newURI("http://test4.bar/"), go: function go4() { - PlacesUtils.favicons.setAndLoadFaviconForPage(this.pageURI, favicons[1].uri, true); + + PlacesUtils.favicons.setAndLoadFaviconForPage( + this.pageURI, NetUtil.newURI(FAVICON_ERRORPAGE_URL), true + ); }, clean: function clean4() {} }, + { // This is a valid icon set test, that will cause the closing notification. + desc: "test setAndLoadFaviconForPage for valid history uri", + pageURI: NetUtil.newURI("http://testfinal.bar/"), + go: function goFinal() { + PlacesUtils.favicons.setAndLoadFaviconForPage(this.pageURI, favicons[1].uri, true); + }, + clean: function cleanFinal() {} + }, + ]; let historyObserver = { @@ -140,7 +152,7 @@ let historyObserver = { //dump_table("moz_favicons"); // Ensure we have been called by the last test. - do_check_true(pageURI.equals(uri("http://test4.bar/"))); + do_check_true(pageURI.equals(NetUtil.newURI("http://testfinal.bar/"))); // Ensure there is only one entry in favicons table. let stmt = DBConn().createStatement( @@ -189,7 +201,7 @@ function runNextTest() { if (tests.length) { currentTest = tests.shift(); - print(currentTest.desc); + do_log_info(currentTest.desc); currentTest.go(); // Wait some time before calling the next test, this is needed to avoid // invoking clean() too early. The first async step should run at least. From ed989b372f576b1f4b124e0ea7793ae03e9e6b8d Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Thu, 13 Jan 2011 18:31:34 -0800 Subject: [PATCH 17/23] Bug 606966 - Need an async history visit API exposed to JS Part 13 - Set the page title for new visits when it is given. r=mak a=blocking --- toolkit/components/places/src/History.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index 1dc82fe6ab41..66c2e980e8b7 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -87,6 +87,7 @@ struct VisitData { , visitTime(0) { guid.SetIsVoid(PR_TRUE); + title.SetIsVoid(PR_TRUE); } VisitData(nsIURI* aURI, @@ -105,6 +106,7 @@ struct VisitData { (void)aReferrer->GetSpec(referrerSpec); } guid.SetIsVoid(PR_TRUE); + title.SetIsVoid(PR_TRUE); } /** @@ -963,8 +965,8 @@ History::InsertPlace(const VisitData& aPlace) nsCOMPtr stmt = syncStatements.GetCachedStatement( "INSERT INTO moz_places " - "(url, rev_host, hidden, typed, guid) " - "VALUES (:page_url, :rev_host, :hidden, :typed, GENERATE_GUID()) " + "(url, title, rev_host, hidden, typed, guid) " + "VALUES (:url, :title, :rev_host, :hidden, :typed, GENERATE_GUID()) " ); NS_ENSURE_STATE(stmt); mozStorageStatementScoper scoper(stmt); @@ -972,7 +974,15 @@ History::InsertPlace(const VisitData& aPlace) nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("rev_host"), aPlace.revHost); NS_ENSURE_SUCCESS(rv, rv); - rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aPlace.spec); + rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("url"), aPlace.spec); + NS_ENSURE_SUCCESS(rv, rv); + if (aPlace.title.IsVoid()) { + rv = stmt->BindNullByName(NS_LITERAL_CSTRING("title")); + } + else { + rv = stmt->BindStringByName(NS_LITERAL_CSTRING("title"), + StringHead(aPlace.title, TITLE_LENGTH_MAX)); + } NS_ENSURE_SUCCESS(rv, rv); rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("typed"), aPlace.typed); NS_ENSURE_SUCCESS(rv, rv); From 6bdea953a235802f9cc114aeda5d89638dda29d5 Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Thu, 13 Jan 2011 18:31:34 -0800 Subject: [PATCH 18/23] Bug 606966 - Need an async history visit API exposed to JS Part 14 - Set the guid for new visits when it is given. r=mak a=blocking --- toolkit/components/places/src/History.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index 66c2e980e8b7..f5d8e29a2278 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -966,7 +966,7 @@ History::InsertPlace(const VisitData& aPlace) nsCOMPtr stmt = syncStatements.GetCachedStatement( "INSERT INTO moz_places " "(url, title, rev_host, hidden, typed, guid) " - "VALUES (:url, :title, :rev_host, :hidden, :typed, GENERATE_GUID()) " + "VALUES (:url, :title, :rev_host, :hidden, :typed, :guid) " ); NS_ENSURE_STATE(stmt); mozStorageStatementScoper scoper(stmt); @@ -988,6 +988,13 @@ History::InsertPlace(const VisitData& aPlace) NS_ENSURE_SUCCESS(rv, rv); rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), aPlace.hidden); NS_ENSURE_SUCCESS(rv, rv); + nsCAutoString guid(aPlace.guid); + if (aPlace.guid.IsVoid()) { + rv = GenerateGUID(guid); + NS_ENSURE_SUCCESS(rv, rv); + } + rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), guid); + NS_ENSURE_SUCCESS(rv, rv); rv = stmt->Execute(); NS_ENSURE_SUCCESS(rv, rv); From 1639c43019113f5f34c76bafedeb48f2c83a60f2 Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Thu, 13 Jan 2011 18:31:34 -0800 Subject: [PATCH 19/23] Bug 606966 - Need an async history visit API exposed to JS Part 15 - Only get a new session id if we need to. r=mak a=blocking --- toolkit/components/places/src/History.cpp | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index f5d8e29a2278..1155bc163d82 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -431,11 +431,13 @@ private: for (nsTArray::size_type i = 0; i < mPlaces.Length(); i++) { mReferrers[i].spec = mPlaces[i].referrerSpec; - // Speculatively get a new session id for our visit. While it is true - // that we will use the session id from the referrer if the visit was - // "recent" enough, we cannot call this method off of the main thread, so - // we have to consume an id now. - mPlaces[i].sessionId = navHistory->GetNewSessionID(); + // Speculatively get a new session id for our visit if the current session + // id is non-valid. While it is true that we will use the session id from + // the referrer if the visit was "recent" enough, we cannot call this + // method off of the main thread, so we have to consume an id now. + if (mPlaces[i].sessionId <= 0) { + mPlaces[i].sessionId = navHistory->GetNewSessionID(); + } } } From a6f9b438d12a0615384689a959506af1b5a43aa5 Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Fri, 14 Jan 2011 14:51:56 +0100 Subject: [PATCH 20/23] Bug 597995 - Tags of bookmarks is lost doing UNDO after a CUT or DELETE. r+a=dietrich --- .../places/tests/unit/test_placesTxn.js | 29 ++++++++++++++++++- toolkit/components/places/src/PlacesUtils.jsm | 16 +++++----- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/browser/components/places/tests/unit/test_placesTxn.js b/browser/components/places/tests/unit/test_placesTxn.js index 1a05b01168f0..df6c16d3769b 100644 --- a/browser/components/places/tests/unit/test_placesTxn.js +++ b/browser/components/places/tests/unit/test_placesTxn.js @@ -276,27 +276,54 @@ function run_test() { do_check_eq(observer._itemAddedParent, root); do_check_eq(observer._itemAddedIndex, 3); - // Test removing an item with a keyword + // Test removing an item with a keyword and a tag. + // Notice in this case the tag persists since other bookmarks have same uri. bmsvc.setKeywordForBookmark(bkmk2Id, "test_keyword"); + tagssvc.tagURI(uri("http://www.example3.com"), ["test-tag"]); var txn5 = ptSvc.removeItem(bkmk2Id); txn5.doTransaction(); do_check_eq(observer._itemRemovedId, bkmk2Id); do_check_eq(observer._itemRemovedFolder, root); do_check_eq(observer._itemRemovedIndex, 2); do_check_eq(bmsvc.getKeywordForBookmark(bkmk2Id), null); + do_check_eq(tagssvc.getTagsForURI(uri("http://www.example3.com"))[0], "test-tag"); txn5.undoTransaction(); var newbkmk2Id = observer._itemAddedId; do_check_eq(observer._itemAddedParent, root); do_check_eq(observer._itemAddedIndex, 2); do_check_eq(bmsvc.getKeywordForBookmark(newbkmk2Id), "test_keyword"); + do_check_eq(tagssvc.getTagsForURI(uri("http://www.example3.com"))[0], "test-tag"); txn5.redoTransaction(); do_check_eq(observer._itemRemovedId, newbkmk2Id); do_check_eq(observer._itemRemovedFolder, root); do_check_eq(observer._itemRemovedIndex, 2); do_check_eq(bmsvc.getKeywordForBookmark(newbkmk2Id), null); + do_check_eq(tagssvc.getTagsForURI(uri("http://www.example3.com"))[0], "test-tag"); txn5.undoTransaction(); do_check_eq(observer._itemAddedParent, root); do_check_eq(observer._itemAddedIndex, 2); + do_check_eq(tagssvc.getTagsForURI(uri("http://www.example3.com"))[0], "test-tag"); + tagssvc.untagURI(uri("http://www.example3.com"), ["test-tag"]); + + { + // Test removing an item with a tag (last bookmark for a uri). + let testURI = uri("http://www.taggedbm.com/"); + ptSvc.doTransaction( + ptSvc.createItem(testURI, fldrId, bmStartIndex, "TaggedBm") + ); + tagssvc.tagURI(testURI, ["test-tag"]); + let itemId = observer._itemAddedId; + txn = ptSvc.removeItem(itemId); + txn.doTransaction(); + do_check_true(tagssvc.getTagsForURI(testURI).length == 0); + txn.undoTransaction(); + do_check_eq(tagssvc.getTagsForURI(testURI)[0], "test-tag"); + txn.redoTransaction(); + do_check_true(tagssvc.getTagsForURI(testURI).length == 0); + txn.undoTransaction(); + do_check_eq(tagssvc.getTagsForURI(testURI)[0], "test-tag"); + txn.redoTransaction(); + } // Test creating a separator var txn6 = ptSvc.createSeparator(root, 1); diff --git a/toolkit/components/places/src/PlacesUtils.jsm b/toolkit/components/places/src/PlacesUtils.jsm index aa2227d2b69b..84d762e112f0 100644 --- a/toolkit/components/places/src/PlacesUtils.jsm +++ b/toolkit/components/places/src/PlacesUtils.jsm @@ -2699,15 +2699,15 @@ PlacesRemoveItemTransaction.prototype = { txn.doTransaction(); } else { + // Before removing the bookmark, save its tags. + let tags = this._uri ? PlacesUtils.tagging.getTagsForURI(this._uri) : null; + PlacesUtils.bookmarks.removeItem(this._id); - if (this._uri) { - // if this was the last bookmark (excluding tag-items and livemark - // children, see getMostRecentBookmarkForURI) for the bookmark's url, - // remove the url from tag containers as well. - if (PlacesUtils.getMostRecentBookmarkForURI(this._uri) == -1) { - this._tags = PlacesUtils.tagging.getTagsForURI(this._uri); - PlacesUtils.tagging.untagURI(this._uri, this._tags); - } + + // If this was the last bookmark (excluding tag-items and livemark + // children) for this url, persist the tags. + if (tags && PlacesUtils.getMostRecentBookmarkForURI(this._uri) == -1) { + this._tags = tags; } } }, From ece01d864ccccfe730039257f63a73394a5af1cf Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Fri, 14 Jan 2011 17:20:39 +0100 Subject: [PATCH 21/23] Bug 591850 - Intermittent failure in places/tests/queries/test_tags.js | 0 == 1, 1==2 a=tests-only --- .../places/tests/queries/test_tags.js | 1020 ++++++++--------- 1 file changed, 500 insertions(+), 520 deletions(-) diff --git a/toolkit/components/places/tests/queries/test_tags.js b/toolkit/components/places/tests/queries/test_tags.js index 68c848bd23da..1e93944e6a32 100644 --- a/toolkit/components/places/tests/queries/test_tags.js +++ b/toolkit/components/places/tests/queries/test_tags.js @@ -44,567 +44,552 @@ // Add your tests here. Each is an object with a summary string |desc| and a // method run() that's called to run the test. var gTests = [ + + function tags_getter_setter() { - desc: "Tags getter/setter should work correctly", - run: function () { - print(" Without setting tags, tags getter should return empty array"); - var [query, dummy] = makeQuery(); - do_check_eq(query.tags.length, 0); + do_log_info("Tags getter/setter should work correctly"); + do_log_info("Without setting tags, tags getter should return empty array"); + var [query, dummy] = makeQuery(); + do_check_eq(query.tags.length, 0); - print(" Setting tags to an empty array, tags getter should return "+ - "empty array"); - [query, dummy] = makeQuery([]); - do_check_eq(query.tags.length, 0); + do_log_info("Setting tags to an empty array, tags getter should return "+ + "empty array"); + [query, dummy] = makeQuery([]); + do_check_eq(query.tags.length, 0); - print(" Setting a few tags, tags getter should return correct array"); - var tags = ["bar", "baz", "foo"]; - [query, dummy] = makeQuery(tags); - setsAreEqual(query.tags, tags, true); + do_log_info("Setting a few tags, tags getter should return correct array"); + var tags = ["bar", "baz", "foo"]; + [query, dummy] = makeQuery(tags); + setsAreEqual(query.tags, tags, true); - print(" Setting some dupe tags, tags getter return unique tags"); - [query, dummy] = makeQuery(["foo", "foo", "bar", "foo", "baz", "bar"]); - setsAreEqual(query.tags, ["bar", "baz", "foo"], true); - } + do_log_info("Setting some dupe tags, tags getter return unique tags"); + [query, dummy] = makeQuery(["foo", "foo", "bar", "foo", "baz", "bar"]); + setsAreEqual(query.tags, ["bar", "baz", "foo"], true); + run_next_test(); }, + function invalid_setter_calls() { - desc: "Invalid calls to tags setter should fail", - run: function () { - try { - var query = PlacesUtils.history.getNewQuery(); - query.tags = null; - do_throw(" Passing null to SetTags should fail"); - } - catch (exc) {} - - try { - query = PlacesUtils.history.getNewQuery(); - query.tags = "this should not work"; - do_throw(" Passing a string to SetTags should fail"); - } - catch (exc) {} - - try { - makeQuery([null]); - do_throw(" Passing one-element array with null to SetTags should fail"); - } - catch (exc) {} - - try { - makeQuery([undefined]); - do_throw(" Passing one-element array with undefined to SetTags " + - "should fail"); - } - catch (exc) {} - - try { - makeQuery(["foo", null, "bar"]); - do_throw(" Passing mixture of tags and null to SetTags should fail"); - } - catch (exc) {} - - try { - makeQuery(["foo", undefined, "bar"]); - do_throw(" Passing mixture of tags and undefined to SetTags " + - "should fail"); - } - catch (exc) {} - - try { - makeQuery([1, 2, 3]); - do_throw(" Passing numbers to SetTags should fail"); - } - catch (exc) {} - - try { - makeQuery(["foo", 1, 2, 3]); - do_throw(" Passing mixture of tags and numbers to SetTags should fail"); - } - catch (exc) {} - - try { - var str = Cc["@mozilla.org/supports-string;1"]. - createInstance(Ci.nsISupportsString); - str.data = "foo"; - query = PlacesUtils.history.getNewQuery(); - query.tags = str; - do_throw(" Passing nsISupportsString to SetTags should fail"); - } - catch (exc) {} - - try { - makeQuery([str]); - do_throw(" Passing array of nsISupportsStrings to SetTags should fail"); - } - catch (exc) {} + do_log_info("Invalid calls to tags setter should fail"); + try { + var query = PlacesUtils.history.getNewQuery(); + query.tags = null; + do_throw("Passing null to SetTags should fail"); } + catch (exc) {} + + try { + query = PlacesUtils.history.getNewQuery(); + query.tags = "this should not work"; + do_throw("Passing a string to SetTags should fail"); + } + catch (exc) {} + + try { + makeQuery([null]); + do_throw("Passing one-element array with null to SetTags should fail"); + } + catch (exc) {} + + try { + makeQuery([undefined]); + do_throw("Passing one-element array with undefined to SetTags " + + "should fail"); + } + catch (exc) {} + + try { + makeQuery(["foo", null, "bar"]); + do_throw("Passing mixture of tags and null to SetTags should fail"); + } + catch (exc) {} + + try { + makeQuery(["foo", undefined, "bar"]); + do_throw("Passing mixture of tags and undefined to SetTags " + + "should fail"); + } + catch (exc) {} + + try { + makeQuery([1, 2, 3]); + do_throw("Passing numbers to SetTags should fail"); + } + catch (exc) {} + + try { + makeQuery(["foo", 1, 2, 3]); + do_throw("Passing mixture of tags and numbers to SetTags should fail"); + } + catch (exc) {} + + try { + var str = Cc["@mozilla.org/supports-string;1"]. + createInstance(Ci.nsISupportsString); + str.data = "foo"; + query = PlacesUtils.history.getNewQuery(); + query.tags = str; + do_throw("Passing nsISupportsString to SetTags should fail"); + } + catch (exc) {} + + try { + makeQuery([str]); + do_throw("Passing array of nsISupportsStrings to SetTags should fail"); + } + catch (exc) {} + run_next_test(); }, + function not_setting_tags() { - desc: "Not setting tags at all should not affect query URI", - run: function () { - checkQueryURI(); - } + do_log_info("Not setting tags at all should not affect query URI"); + checkQueryURI(); + run_next_test(); }, + function empty_array_tags() { - desc: "Setting tags with an empty array should not affect query URI", - run: function () { - checkQueryURI([]); - } + do_log_info("Setting tags with an empty array should not affect query URI"); + checkQueryURI([]); + run_next_test(); }, + function set_tags() { - desc: "Setting some tags should result in correct query URI", - run: function () { - checkQueryURI([ - "foo", - "七難", - "", - "いっぱいおっぱい", - "Abracadabra", - "123", - "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!", - "アスキーでございません", - "あいうえお", - ]); - } + do_log_info("Setting some tags should result in correct query URI"); + checkQueryURI([ + "foo", + "七難", + "", + "いっぱいおっぱい", + "Abracadabra", + "123", + "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!", + "アスキーでございません", + "あいうえお", + ]); + run_next_test(); }, + function no_tags_tagsAreNot() { - desc: "Not setting tags at all but setting tagsAreNot should affect " + - "query URI", - run: function () { - checkQueryURI(null, true); - } + do_log_info("Not setting tags at all but setting tagsAreNot should " + + "affect query URI"); + checkQueryURI(null, true); + run_next_test(); }, + function empty_array_tags_tagsAreNot() { - desc: "Setting tags with an empty array and setting tagsAreNot should " + - "affect query URI", - run: function () { - checkQueryURI([], true); - } + do_log_info("Setting tags with an empty array and setting tagsAreNot " + + "should affect query URI"); + checkQueryURI([], true); + run_next_test(); }, + function () { - desc: "Setting some tags and setting tagsAreNot should result in correct " + - "query URI", - run: function () { - checkQueryURI([ - "foo", - "七難", - "", - "いっぱいおっぱい", - "Abracadabra", - "123", - "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!", - "アスキーでございません", - "あいうえお", - ], true); - } + do_log_info("Setting some tags and setting tagsAreNot should result in " + + "correct query URI"); + checkQueryURI([ + "foo", + "七難", + "", + "いっぱいおっぱい", + "Abracadabra", + "123", + "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!", + "アスキーでございません", + "あいうえお", + ], true); + run_next_test(); }, + function tag_to_uri() { - desc: "Querying history on tag associated with a URI should return " + - "that URI", - run: function () { - doWithVisit(["foo", "bar", "baz"], function (aURI) { - var [query, opts] = makeQuery(["foo"]); - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["bar"]); - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["baz"]); - executeAndCheckQueryResults(query, opts, [aURI.spec]); - }); - } + do_log_info("Querying history on tag associated with a URI should return " + + "that URI"); + doWithVisit(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["bar"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["baz"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); }, + function tags_to_uri() { - desc: "Querying history on many tags associated with a URI should " + - "return that URI", - run: function () { - doWithVisit(["foo", "bar", "baz"], function (aURI) { - var [query, opts] = makeQuery(["foo", "bar"]); - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["foo", "baz"]); - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["bar", "baz"]); - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["foo", "bar", "baz"]); - executeAndCheckQueryResults(query, opts, [aURI.spec]); - }); - } + do_log_info("Querying history on many tags associated with a URI should " + + "return that URI"); + doWithVisit(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "bar"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "baz"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["bar", "baz"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "bar", "baz"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); }, + function repeated_tag() { - desc: "Specifying the same tag multiple times in a history query should " + - "not matter", - run: function () { - doWithVisit(["foo", "bar", "baz"], function (aURI) { - var [query, opts] = makeQuery(["foo", "foo"]); - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["foo", "foo", "foo", "bar", "bar", "baz"]); - executeAndCheckQueryResults(query, opts, [aURI.spec]); - }); - } + do_log_info("Specifying the same tag multiple times in a history query " + + "should not matter"); + doWithVisit(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "foo"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "foo", "foo", "bar", "bar", "baz"]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); }, + function many_tags_no_uri() { - desc: "Querying history on many tags associated with a URI and tags " + - "not associated with that URI should not return that URI", - run: function () { - doWithVisit(["foo", "bar", "baz"], function (aURI) { - var [query, opts] = makeQuery(["foo", "bogus"]); - executeAndCheckQueryResults(query, opts, []); - [query, opts] = makeQuery(["foo", "bar", "bogus"]); - executeAndCheckQueryResults(query, opts, []); - [query, opts] = makeQuery(["foo", "bar", "baz", "bogus"]); - executeAndCheckQueryResults(query, opts, []); - }); - } + do_log_info("Querying history on many tags associated with a URI and " + + "tags not associated with that URI should not return that URI"); + doWithVisit(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "bogus"]); + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["foo", "bar", "bogus"]); + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["foo", "bar", "baz", "bogus"]); + executeAndCheckQueryResults(query, opts, []); + }); }, + function nonexistent_tags() { - desc: "Querying history on nonexistent tags should return no results", - run: function () { - doWithVisit(["foo", "bar", "baz"], function (aURI) { - var [query, opts] = makeQuery(["bogus"]); - executeAndCheckQueryResults(query, opts, []); - [query, opts] = makeQuery(["bogus", "gnarly"]); - executeAndCheckQueryResults(query, opts, []); - }); - } + do_log_info("Querying history on nonexistent tags should return no results"); + doWithVisit(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["bogus"]); + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["bogus", "gnarly"]); + executeAndCheckQueryResults(query, opts, []); + }); }, + function tag_to_bookmark() { - desc: "Querying bookmarks on tag associated with a URI should return " + - "that URI", - run: function () { - doWithBookmark(["foo", "bar", "baz"], function (aURI) { - var [query, opts] = makeQuery(["foo"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["bar"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["baz"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, [aURI.spec]); - }); - } - }, - - { - desc: "Querying bookmarks on many tags associated with a URI should " + - "return that URI", - run: function () { - doWithBookmark(["foo", "bar", "baz"], function (aURI) { - var [query, opts] = makeQuery(["foo", "bar"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["foo", "baz"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["bar", "baz"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["foo", "bar", "baz"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, [aURI.spec]); - }); - } - }, - - { - desc: "Specifying the same tag multiple times in a bookmark query should " + - "not matter", - run: function () { - doWithBookmark(["foo", "bar", "baz"], function (aURI) { - var [query, opts] = makeQuery(["foo", "foo"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, [aURI.spec]); - [query, opts] = makeQuery(["foo", "foo", "foo", "bar", "bar", "baz"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, [aURI.spec]); - }); - } - }, - - { - desc: "Querying bookmarks on many tags associated with a URI and tags " + - "not associated with that URI should not return that URI", - run: function () { - doWithBookmark(["foo", "bar", "baz"], function (aURI) { - var [query, opts] = makeQuery(["foo", "bogus"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, []); - [query, opts] = makeQuery(["foo", "bar", "bogus"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, []); - [query, opts] = makeQuery(["foo", "bar", "baz", "bogus"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, []); - }); - } - }, - - { - desc: "Querying bookmarks on nonexistent tag should return no results", - run: function () { - doWithBookmark(["foo", "bar", "baz"], function (aURI) { - var [query, opts] = makeQuery(["bogus"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, []); - [query, opts] = makeQuery(["bogus", "gnarly"]); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - executeAndCheckQueryResults(query, opts, []); - }); - } - }, - - { - desc: "Querying history using tagsAreNot should work correctly", - run: function () { - var urisAndTags = { - "http://example.com/1": ["foo", "bar"], - "http://example.com/2": ["baz", "qux"], - "http://example.com/3": null - }; - - print(" Add visits and tag the URIs"); - for (let [pURI, tags] in Iterator(urisAndTags)) { - let nsiuri = uri(pURI); - addVisit(nsiuri); - if (tags) - PlacesUtils.tagging.tagURI(nsiuri, tags); - } - - print(' Querying for "foo" should match only /2 and /3'); - var [query, opts] = makeQuery(["foo"], true); - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, - ["http://example.com/2", "http://example.com/3"]); - - print(' Querying for "foo" and "bar" should match only /2 and /3'); - [query, opts] = makeQuery(["foo", "bar"], true); - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, - ["http://example.com/2", "http://example.com/3"]); - - print(' Querying for "foo" and "bogus" should match only /2 and /3'); - [query, opts] = makeQuery(["foo", "bogus"], true); - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, - ["http://example.com/2", "http://example.com/3"]); - - print(' Querying for "foo" and "baz" should match only /3'); - [query, opts] = makeQuery(["foo", "baz"], true); - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, - ["http://example.com/3"]); - - print(' Querying for "bogus" should match all'); - [query, opts] = makeQuery(["bogus"], true); - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, - ["http://example.com/1", - "http://example.com/2", - "http://example.com/3"]); - - // Clean up. - for (let [pURI, tags] in Iterator(urisAndTags)) { - let nsiuri = uri(pURI); - if (tags) - PlacesUtils.tagging.untagURI(nsiuri, tags); - } - cleanDatabase(); - } - }, - - { - desc: "Querying bookmarks using tagsAreNot should work correctly", - run: function () { - var urisAndTags = { - "http://example.com/1": ["foo", "bar"], - "http://example.com/2": ["baz", "qux"], - "http://example.com/3": null - }; - - print(" Add bookmarks and tag the URIs"); - for (let [pURI, tags] in Iterator(urisAndTags)) { - let nsiuri = uri(pURI); - addBookmark(nsiuri); - if (tags) - PlacesUtils.tagging.tagURI(nsiuri, tags); - } - - print(' Querying for "foo" should match only /2 and /3'); - var [query, opts] = makeQuery(["foo"], true); + do_log_info("Querying bookmarks on tag associated with a URI should " + + "return that URI"); + doWithBookmark(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo"]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, - ["http://example.com/2", "http://example.com/3"]); - - print(' Querying for "foo" and "bar" should match only /2 and /3'); - [query, opts] = makeQuery(["foo", "bar"], true); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["bar"]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, - ["http://example.com/2", "http://example.com/3"]); - - print(' Querying for "foo" and "bogus" should match only /2 and /3'); - [query, opts] = makeQuery(["foo", "bogus"], true); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["baz"]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, - ["http://example.com/2", "http://example.com/3"]); - - print(' Querying for "foo" and "baz" should match only /3'); - [query, opts] = makeQuery(["foo", "baz"], true); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, - ["http://example.com/3"]); - - print(' Querying for "bogus" should match all'); - [query, opts] = makeQuery(["bogus"], true); - opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, - ["http://example.com/1", - "http://example.com/2", - "http://example.com/3"]); - - // Clean up. - for (let [pURI, tags] in Iterator(urisAndTags)) { - let nsiuri = uri(pURI); - if (tags) - PlacesUtils.tagging.untagURI(nsiuri, tags); - } - cleanDatabase(); - } + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); }, + function many_tags_to_bookmark() { - desc: "Duplicate existing tags (i.e., multiple tag folders with same " + - "name) should not throw off query results", - run: function () { - var tagName = "foo"; - - print(" Add bookmark and tag it normally"); - addBookmark(TEST_URI); - PlacesUtils.tagging.tagURI(TEST_URI, [tagName]); - - print(" Manually create tag folder with same name as tag and insert " + - "bookmark"); - var dupTagId = PlacesUtils.bookmarks.createFolder(PlacesUtils.tagsFolderId, - tagName, - Ci.nsINavBookmarksService.DEFAULT_INDEX); - do_check_true(dupTagId > 0); - var bmId = PlacesUtils.bookmarks.insertBookmark(dupTagId, - TEST_URI, - Ci.nsINavBookmarksService.DEFAULT_INDEX, - "title"); - do_check_true(bmId > 0); - - print(" Querying for tag should match URI"); - var [query, opts] = makeQuery([tagName]); + do_log_info("Querying bookmarks on many tags associated with a URI " + + "should return that URI"); + doWithBookmark(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "bar"]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [TEST_URI.spec]); - - PlacesUtils.tagging.untagURI(TEST_URI, [tagName]); - cleanDatabase(); - } - }, - - { - desc: "Regular folders with the same name as tag should not throw off " + - "query results", - run: function () { - var tagName = "foo"; - - print(" Add bookmark and tag it"); - addBookmark(TEST_URI); - PlacesUtils.tagging.tagURI(TEST_URI, [tagName]); - - print(" Create folder with same name as tag"); - var folderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, - tagName, - Ci.nsINavBookmarksService.DEFAULT_INDEX); - do_check_true(folderId > 0); - - print(" Querying for tag should match URI"); - var [query, opts] = makeQuery([tagName]); + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "baz"]); opts.queryType = opts.QUERY_TYPE_BOOKMARKS; - queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [TEST_URI.spec]); - - PlacesUtils.tagging.untagURI(TEST_URI, [tagName]); - cleanDatabase(); - } + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["bar", "baz"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "bar", "baz"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); }, + function repeated_tag_to_bookmarks() { - desc: "Multiple queries ORed together should work", - run: function () { - var urisAndTags = { - "http://example.com/1": [], - "http://example.com/2": [] - }; - - // Search with lots of tags to make sure tag parameter substitution in SQL - // can handle it with more than one query. - for (let i = 0; i < 11; i++) { - urisAndTags["http://example.com/1"].push("/1 tag " + i); - urisAndTags["http://example.com/2"].push("/2 tag " + i); - } - - print(" Add visits and tag the URIs"); - for (let [pURI, tags] in Iterator(urisAndTags)) { - let nsiuri = uri(pURI); - addVisit(nsiuri); - if (tags) - PlacesUtils.tagging.tagURI(nsiuri, tags); - } - - print(" Query for /1 OR query for /2 should match both /1 and /2"); - var [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); - var [query2, dummy] = makeQuery(urisAndTags["http://example.com/2"]); - var root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; - queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); - - print(" Query for /1 OR query on bogus tag should match only /1"); - [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); - [query2, dummy] = makeQuery(["bogus"]); - root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; - queryResultsAre(root, ["http://example.com/1"]); - - print(" Query for /1 OR query for /1 should match only /1"); - [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); - [query2, dummy] = makeQuery(urisAndTags["http://example.com/1"]); - root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; - queryResultsAre(root, ["http://example.com/1"]); - - print(" Query for /1 with tagsAreNot OR query for /2 with tagsAreNot " + - "should match both /1 and /2"); - [query1, opts] = makeQuery(urisAndTags["http://example.com/1"], true); - [query2, dummy] = makeQuery(urisAndTags["http://example.com/2"], true); - root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; - queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); - - print(" Query for /1 OR query for /2 with tagsAreNot should match " + - "only /1"); - [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); - [query2, dummy] = makeQuery(urisAndTags["http://example.com/2"], true); - root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; - queryResultsAre(root, ["http://example.com/1"]); - - print(" Query for /1 OR query for /1 with tagsAreNot should match " + - "both URIs"); - [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); - [query2, dummy] = makeQuery(urisAndTags["http://example.com/1"], true); - root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; - queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); - - // Clean up. - for (let [pURI, tags] in Iterator(urisAndTags)) { - let nsiuri = uri(pURI); - if (tags) - PlacesUtils.tagging.untagURI(nsiuri, tags); - } - cleanDatabase(); - } + do_log_info("Specifying the same tag multiple times in a bookmark query " + + "should not matter"); + doWithBookmark(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "foo"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + [query, opts] = makeQuery(["foo", "foo", "foo", "bar", "bar", "baz"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, [aURI.spec]); + }); }, + + function many_tags_no_bookmark() + { + do_log_info("Querying bookmarks on many tags associated with a URI and " + + "tags not associated with that URI should not return that URI"); + doWithBookmark(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["foo", "bogus"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["foo", "bar", "bogus"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["foo", "bar", "baz", "bogus"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, []); + }); + }, + + function nonexistent_tags_bookmark() + { + do_log_info("Querying bookmarks on nonexistent tag should return no results"); + doWithBookmark(["foo", "bar", "baz"], function (aURI) { + var [query, opts] = makeQuery(["bogus"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, []); + [query, opts] = makeQuery(["bogus", "gnarly"]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + executeAndCheckQueryResults(query, opts, []); + }); + }, + + function tagsAreNot_history() + { + do_log_info("Querying history using tagsAreNot should work correctly"); + var urisAndTags = { + "http://example.com/1": ["foo", "bar"], + "http://example.com/2": ["baz", "qux"], + "http://example.com/3": null + }; + + do_log_info("Add visits and tag the URIs"); + for (let [pURI, tags] in Iterator(urisAndTags)) { + let nsiuri = uri(pURI); + addVisit(nsiuri); + if (tags) + PlacesUtils.tagging.tagURI(nsiuri, tags); + } + + do_log_info(' Querying for "foo" should match only /2 and /3'); + var [query, opts] = makeQuery(["foo"], true); + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_log_info(' Querying for "foo" and "bar" should match only /2 and /3'); + [query, opts] = makeQuery(["foo", "bar"], true); + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_log_info(' Querying for "foo" and "bogus" should match only /2 and /3'); + [query, opts] = makeQuery(["foo", "bogus"], true); + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_log_info(' Querying for "foo" and "baz" should match only /3'); + [query, opts] = makeQuery(["foo", "baz"], true); + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/3"]); + + do_log_info(' Querying for "bogus" should match all'); + [query, opts] = makeQuery(["bogus"], true); + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/1", + "http://example.com/2", + "http://example.com/3"]); + + // Clean up. + for (let [pURI, tags] in Iterator(urisAndTags)) { + let nsiuri = uri(pURI); + if (tags) + PlacesUtils.tagging.untagURI(nsiuri, tags); + } + cleanDatabase(run_next_test); + }, + + function tagsAreNot_bookmarks() + { + do_log_info("Querying bookmarks using tagsAreNot should work correctly"); + var urisAndTags = { + "http://example.com/1": ["foo", "bar"], + "http://example.com/2": ["baz", "qux"], + "http://example.com/3": null + }; + + do_log_info("Add bookmarks and tag the URIs"); + for (let [pURI, tags] in Iterator(urisAndTags)) { + let nsiuri = uri(pURI); + addBookmark(nsiuri); + if (tags) + PlacesUtils.tagging.tagURI(nsiuri, tags); + } + + do_log_info(' Querying for "foo" should match only /2 and /3'); + var [query, opts] = makeQuery(["foo"], true); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_log_info(' Querying for "foo" and "bar" should match only /2 and /3'); + [query, opts] = makeQuery(["foo", "bar"], true); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_log_info(' Querying for "foo" and "bogus" should match only /2 and /3'); + [query, opts] = makeQuery(["foo", "bogus"], true); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/2", "http://example.com/3"]); + + do_log_info(' Querying for "foo" and "baz" should match only /3'); + [query, opts] = makeQuery(["foo", "baz"], true); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/3"]); + + do_log_info(' Querying for "bogus" should match all'); + [query, opts] = makeQuery(["bogus"], true); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, + ["http://example.com/1", + "http://example.com/2", + "http://example.com/3"]); + + // Clean up. + for (let [pURI, tags] in Iterator(urisAndTags)) { + let nsiuri = uri(pURI); + if (tags) + PlacesUtils.tagging.untagURI(nsiuri, tags); + } + cleanDatabase(run_next_test); + }, + + function duplicate_tags() { + do_log_info("Duplicate existing tags (i.e., multiple tag folders with " + + "same name) should not throw off query results"); + var tagName = "foo"; + + do_log_info("Add bookmark and tag it normally"); + addBookmark(TEST_URI); + PlacesUtils.tagging.tagURI(TEST_URI, [tagName]); + + do_log_info("Manually create tag folder with same name as tag and insert " + + "bookmark"); + var dupTagId = PlacesUtils.bookmarks.createFolder(PlacesUtils.tagsFolderId, + tagName, + Ci.nsINavBookmarksService.DEFAULT_INDEX); + do_check_true(dupTagId > 0); + var bmId = PlacesUtils.bookmarks.insertBookmark(dupTagId, + TEST_URI, + Ci.nsINavBookmarksService.DEFAULT_INDEX, + "title"); + do_check_true(bmId > 0); + + do_log_info("Querying for tag should match URI"); + var [query, opts] = makeQuery([tagName]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [TEST_URI.spec]); + + PlacesUtils.tagging.untagURI(TEST_URI, [tagName]); + cleanDatabase(run_next_test); + }, + + function folder_named_as_tag() + { + do_log_info("Regular folders with the same name as tag should not throw " + + "off query results"); + var tagName = "foo"; + + do_log_info("Add bookmark and tag it"); + addBookmark(TEST_URI); + PlacesUtils.tagging.tagURI(TEST_URI, [tagName]); + + do_log_info("Create folder with same name as tag"); + var folderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId, + tagName, + Ci.nsINavBookmarksService.DEFAULT_INDEX); + do_check_true(folderId > 0); + + do_log_info("Querying for tag should match URI"); + var [query, opts] = makeQuery([tagName]); + opts.queryType = opts.QUERY_TYPE_BOOKMARKS; + queryResultsAre(PlacesUtils.history.executeQuery(query, opts).root, [TEST_URI.spec]); + + PlacesUtils.tagging.untagURI(TEST_URI, [tagName]); + cleanDatabase(run_next_test); + }, + + function ORed_queries() { + do_log_info("Multiple queries ORed together should work"); + var urisAndTags = { + "http://example.com/1": [], + "http://example.com/2": [] + }; + + // Search with lots of tags to make sure tag parameter substitution in SQL + // can handle it with more than one query. + for (let i = 0; i < 11; i++) { + urisAndTags["http://example.com/1"].push("/1 tag " + i); + urisAndTags["http://example.com/2"].push("/2 tag " + i); + } + + do_log_info("Add visits and tag the URIs"); + for (let [pURI, tags] in Iterator(urisAndTags)) { + let nsiuri = uri(pURI); + addVisit(nsiuri); + if (tags) + PlacesUtils.tagging.tagURI(nsiuri, tags); + } + + do_log_info("Query for /1 OR query for /2 should match both /1 and /2"); + var [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); + var [query2, dummy] = makeQuery(urisAndTags["http://example.com/2"]); + var root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); + + do_log_info("Query for /1 OR query on bogus tag should match only /1"); + [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); + [query2, dummy] = makeQuery(["bogus"]); + root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1"]); + + do_log_info("Query for /1 OR query for /1 should match only /1"); + [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); + [query2, dummy] = makeQuery(urisAndTags["http://example.com/1"]); + root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1"]); + + do_log_info("Query for /1 with tagsAreNot OR query for /2 with tagsAreNot " + + "should match both /1 and /2"); + [query1, opts] = makeQuery(urisAndTags["http://example.com/1"], true); + [query2, dummy] = makeQuery(urisAndTags["http://example.com/2"], true); + root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); + + do_log_info("Query for /1 OR query for /2 with tagsAreNot should match " + + "only /1"); + [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); + [query2, dummy] = makeQuery(urisAndTags["http://example.com/2"], true); + root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1"]); + + do_log_info("Query for /1 OR query for /1 with tagsAreNot should match " + + "both URIs"); + [query1, opts] = makeQuery(urisAndTags["http://example.com/1"]); + [query2, dummy] = makeQuery(urisAndTags["http://example.com/1"], true); + root = PlacesUtils.history.executeQueries([query1, query2], 2, opts).root; + queryResultsAre(root, ["http://example.com/1", "http://example.com/2"]); + + // Clean up. + for (let [pURI, tags] in Iterator(urisAndTags)) { + let nsiuri = uri(pURI); + if (tags) + PlacesUtils.tagging.untagURI(nsiuri, tags); + } + cleanDatabase(run_next_test); + }, + ]; // The tag keys in query URIs, i.e., "place:tag=foo&!tags=1" @@ -627,7 +612,7 @@ function addBookmark(aURI) { aURI, Ci.nsINavBookmarksService.DEFAULT_INDEX, aURI.spec); - print(" Sanity check: insertBookmark should not fail"); + do_log_info("Sanity check: insertBookmark should not fail"); do_check_true(bmId > 0); } @@ -644,16 +629,16 @@ function addVisit(aURI) { Ci.nsINavHistoryService.TRANSITION_LINK, false, 0); - print(" Sanity check: addVisit should not fail"); + do_log_info("Sanity check: addVisit should not fail"); do_check_true(visitId > 0); } /** * Removes all pages from history and bookmarks. */ -function cleanDatabase() { - PlacesUtils.bhistory.removeAllPages(); +function cleanDatabase(aCallback) { remove_all_bookmarks(); + waitForClearHistory(aCallback); } /** @@ -672,7 +657,7 @@ function checkQueryURI(aTags, aTagsAreNot) { var expURI = "place:" + pairs.join("&"); var [query, opts] = makeQuery(aTags, aTagsAreNot); var actualURI = queryURI(query, opts); - print(" Query URI should be what we expect for the given tags"); + do_log_info("Query URI should be what we expect for the given tags"); do_check_eq(actualURI, expURI); } @@ -691,7 +676,7 @@ function doWithBookmark(aTags, aCallback) { PlacesUtils.tagging.tagURI(TEST_URI, aTags); aCallback(TEST_URI); PlacesUtils.tagging.untagURI(TEST_URI, aTags); - cleanDatabase(); + cleanDatabase(run_next_test); } /** @@ -709,7 +694,7 @@ function doWithVisit(aTags, aCallback) { PlacesUtils.tagging.tagURI(TEST_URI, aTags); aCallback(TEST_URI); PlacesUtils.tagging.untagURI(TEST_URI, aTags); - cleanDatabase(); + cleanDatabase(run_next_test); } /** @@ -759,7 +744,7 @@ function executeAndCheckQueryResults(aQuery, aQueryOpts, aExpectedURIs) { */ function makeQuery(aTags, aTagsAreNot) { aTagsAreNot = !!aTagsAreNot; - print(" Making a query " + + do_log_info("Making a query " + (aTags ? "with tags " + aTags.toSource() : "without calling setTags() at all") + @@ -777,7 +762,7 @@ function makeQuery(aTags, aTagsAreNot) { uniqueTags.sort(); } - print(" Made query should be correct for tags and tagsAreNot"); + do_log_info("Made query should be correct for tags and tagsAreNot"); if (uniqueTags) setsAreEqual(query.tags, uniqueTags, true); var expCount = uniqueTags ? uniqueTags.length : 0; @@ -841,10 +826,5 @@ function setsAreEqual(aArr1, aArr2, aIsOrdered) { /////////////////////////////////////////////////////////////////////////////// function run_test() { - cleanDatabase(); - gTests.forEach(function (t) { - print("Running test: " + t.desc); - t.run() - }); - cleanDatabase(); + run_next_test(); } From 00ed549578ab95708f182ccfbd41064438ef1fee Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Fri, 14 Jan 2011 10:42:41 -0800 Subject: [PATCH 22/23] Bug 606966 - Need an async history visit API exposed to JS Part 16 - Handle a callback if it is provided. r=mak a=blocking --- toolkit/components/places/src/History.cpp | 157 ++++++++++++++++++---- 1 file changed, 133 insertions(+), 24 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index 1155bc163d82..6d07b76a679a 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -327,6 +327,43 @@ private: VisitData mReferrer; }; +/** + * Notifies a callback object about completion. + */ +class NotifyCompletion : public nsRunnable +{ +public: + NotifyCompletion(mozIVisitInfoCallback* aCallback, + const VisitData& aPlace, + nsresult aResult) + : mCallback(aCallback) + , mPlace(aPlace) + , mResult(aResult) + { + NS_PRECONDITION(aCallback, "Must pass a non-null callback!"); + } + + NS_IMETHOD Run() + { + NS_PRECONDITION(NS_IsMainThread(), + "This should be called on the main thread"); + + // TODO build a mozIPlaceInfo object for the visit. + (void)mCallback->OnComplete(mResult, nsnull); + return NS_OK; + } + +private: + /** + * Callers MUST hold a strong refernce to this that outlives us because we may + * be created off of the main thread, and therefore cannot call AddRef on this + * object (and therefore cannot hold a strong reference to it). + */ + mozIVisitInfoCallback* mCallback; + VisitData mPlace; + const nsresult mResult; +}; + /** * Adds a visit to the database. */ @@ -340,16 +377,19 @@ public: * The database connection to use for these operations. * @param aPlaces * The locations to record visits. + * @param [optional] aCallback + * The callback to notify about the visit. */ static nsresult Start(mozIStorageConnection* aConnection, - nsTArray& aPlaces) + nsTArray& aPlaces, + mozIVisitInfoCallback* aCallback = NULL) { NS_PRECONDITION(NS_IsMainThread(), "This should be called on the main thread"); NS_PRECONDITION(aPlaces.Length() > 0, "Must pass a non-empty array!"); nsRefPtr event = - new InsertVisitedURIs(aConnection, aPlaces); + new InsertVisitedURIs(aConnection, aPlaces, aCallback); // Get the target thread, and then start the work! nsCOMPtr target = do_GetInterface(aConnection); @@ -368,8 +408,7 @@ public: mozStorageTransaction transaction(mDBConn, PR_FALSE, mozIStorageConnection::TRANSACTION_IMMEDIATE); - VisitData* lastPlace; - nsresult rv; + VisitData* lastPlace = NULL; for (nsTArray::size_type i = 0; i < mPlaces.Length(); i++) { VisitData& place = mPlaces.ElementAt(i); VisitData& referrer = mReferrers.ElementAt(i); @@ -381,26 +420,15 @@ public: FetchReferrerInfo(referrer, place); - // If the page was in moz_places, we need to update the entry. - if (known) { - rv = mHistory->UpdatePlace(place); - NS_ENSURE_SUCCESS(rv, rv); + nsresult rv = DoDatabaseInserts(known, place, referrer); + if (mCallback) { + nsCOMPtr event = + new NotifyCompletion(mCallback, place, rv); + nsresult rv2 = NS_DispatchToMainThread(event); + NS_ENSURE_SUCCESS(rv2, rv2); } - // Otherwise, the page was not in moz_places, so now we have to add it. - else { - rv = mHistory->InsertPlace(place); - NS_ENSURE_SUCCESS(rv, rv); - } - - rv = AddVisit(place, referrer); NS_ENSURE_SUCCESS(rv, rv); - // TODO (bug 623969) we shouldn't update this after each visit, but - // rather only for each unique place to save disk I/O. - rv = UpdateFrecency(place); - NS_ENSURE_SUCCESS(rv, rv); - - // Dispatch an event to the main thread to notify observers. nsCOMPtr event = new NotifyVisitObservers(place, referrer); rv = NS_DispatchToMainThread(event); NS_ENSURE_SUCCESS(rv, rv); @@ -408,15 +436,17 @@ public: lastPlace = &mPlaces.ElementAt(i); } - rv = transaction.Commit(); + nsresult rv = transaction.Commit(); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } private: InsertVisitedURIs(mozIStorageConnection* aConnection, - nsTArray& aPlaces) + nsTArray& aPlaces, + mozIVisitInfoCallback* aCallback) : mDBConn(aConnection) + , mCallback(aCallback) , mHistory(History::GetService()) { NS_PRECONDITION(NS_IsMainThread(), @@ -439,6 +469,59 @@ private: mPlaces[i].sessionId = navHistory->GetNewSessionID(); } } + + // We AddRef on the main thread, and release it when we are destroyed. + NS_IF_ADDREF(mCallback); + } + + virtual ~InsertVisitedURIs() + { + if (mCallback) { + nsCOMPtr mainThread = do_GetMainThread(); + (void)NS_ProxyRelease(mainThread, mCallback, PR_TRUE); + } + } + + /** + * Inserts or updates the entry in moz_places for this visit, adds the visit, + * and updates the frecency of the place. + * + * @param aKnown + * True if we already have an entry for this place in moz_places, false + * otherwise. + * @param aPlace + * The place we are adding a visit for. + * @param aReferrer + * The referrer for aPlace. + */ + nsresult DoDatabaseInserts(bool aKnown, + VisitData& aPlace, + VisitData& aReferrer) + { + NS_PRECONDITION(!NS_IsMainThread(), + "This should not be called on the main thread"); + + // If the page was in moz_places, we need to update the entry. + nsresult rv; + if (aKnown) { + rv = mHistory->UpdatePlace(aPlace); + NS_ENSURE_SUCCESS(rv, rv); + } + // Otherwise, the page was not in moz_places, so now we have to add it. + else { + rv = mHistory->InsertPlace(aPlace); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = AddVisit(aPlace, aReferrer); + NS_ENSURE_SUCCESS(rv, rv); + + // TODO (bug 623969) we shouldn't update this after each visit, but + // rather only for each unique place to save disk I/O. + rv = UpdateFrecency(aPlace); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; } /** @@ -668,6 +751,13 @@ private: nsTArray mPlaces; nsTArray mReferrers; + /** + * We own a strong reference to this, but in an indirect way. We call AddRef + * in our constructor, which happens on the main thread, and proxy the relase + * of the object to the main thread in our destructor. + */ + mozIVisitInfoCallback* mCallback; + /** * Strong reference to the History object because we do not want it to * disappear out from under us. @@ -836,9 +926,12 @@ private: * * @param aPlace * The VisitData of the visit to store as an embed visit. + * @param [optional] aCallback + * The mozIVisitInfoCallback to notify, if provided. */ void -StoreAndNotifyEmbedVisit(VisitData& aPlace) +StoreAndNotifyEmbedVisit(VisitData& aPlace, + mozIVisitInfoCallback* aCallback = NULL) { NS_PRECONDITION(aPlace.transitionType == nsINavHistoryService::TRANSITION_EMBED, "Must only pass TRANSITION_EMBED visits to this!"); @@ -853,6 +946,22 @@ StoreAndNotifyEmbedVisit(VisitData& aPlace) } navHistory->registerEmbedVisit(uri, aPlace.visitTime); + + if (aCallback) { + // NotifyCompletion does not hold a strong reference to the callback, so we + // have to manage it by AddRefing now and then releasing it after the event + // has run. + NS_ADDREF(aCallback); + nsCOMPtr event = + new NotifyCompletion(aCallback, aPlace, NS_OK); + (void)NS_DispatchToMainThread(event); + + // Also dispatch an event to release our reference to the callback after + // NotifyCompletion has run. + nsCOMPtr mainThread = do_GetMainThread(); + (void)NS_ProxyRelease(mainThread, aCallback, PR_TRUE); + } + VisitData noReferrer; nsCOMPtr event = new NotifyVisitObservers(aPlace, noReferrer); (void)NS_DispatchToMainThread(event); From 0ae4277be7eb1dbf7e864362bd1a985045467a3c Mon Sep 17 00:00:00 2001 From: Shawn Wilsher Date: Fri, 14 Jan 2011 11:10:11 -0800 Subject: [PATCH 23/23] Fix typo in comment. a=stupidity --- toolkit/components/places/src/History.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/toolkit/components/places/src/History.cpp b/toolkit/components/places/src/History.cpp index 6d07b76a679a..3634cf5edeca 100644 --- a/toolkit/components/places/src/History.cpp +++ b/toolkit/components/places/src/History.cpp @@ -355,9 +355,9 @@ public: private: /** - * Callers MUST hold a strong refernce to this that outlives us because we may - * be created off of the main thread, and therefore cannot call AddRef on this - * object (and therefore cannot hold a strong reference to it). + * Callers MUST hold a strong reference to this that outlives us because we + * may be created off of the main thread, and therefore cannot call AddRef on + * this object (and therefore cannot hold a strong reference to it). */ mozIVisitInfoCallback* mCallback; VisitData mPlace;