зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1043863 - Use AsyncShutdown to shutdown Places. r=mak
--HG-- extra : rebase_source : 3a593651ac1fc995e01d00af037aaac8b81c7c32
This commit is contained in:
Родитель
945cfe86e7
Коммит
38a8ed9399
|
@ -36,56 +36,14 @@ var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
|
|||
getService(Ci.nsIObserver);
|
||||
formHistoryStartup.observe(null, "profile-after-change", null);
|
||||
|
||||
let notificationIndex = 0;
|
||||
|
||||
let notificationsObserver = {
|
||||
observe: function observe(aSubject, aTopic, aData) {
|
||||
print("Received notification: " + aTopic);
|
||||
|
||||
// Note that some of these notifications could arrive multiple times, for
|
||||
// example in case of sync, we allow that.
|
||||
if (EXPECTED_NOTIFICATIONS[notificationIndex] != aTopic)
|
||||
notificationIndex++;
|
||||
do_check_eq(EXPECTED_NOTIFICATIONS[notificationIndex], aTopic);
|
||||
|
||||
if (aTopic != TOPIC_CONNECTION_CLOSED)
|
||||
return;
|
||||
|
||||
getDistinctNotifications().forEach(
|
||||
function (topic) Services.obs.removeObserver(notificationsObserver, topic)
|
||||
);
|
||||
|
||||
print("Looking for uncleared stuff.");
|
||||
|
||||
let stmt = DBConn().createStatement(
|
||||
"SELECT id FROM moz_places WHERE url = :page_url "
|
||||
);
|
||||
|
||||
try {
|
||||
URIS.forEach(function(aUrl) {
|
||||
stmt.params.page_url = aUrl;
|
||||
do_check_false(stmt.executeStep());
|
||||
stmt.reset();
|
||||
});
|
||||
} finally {
|
||||
stmt.finalize();
|
||||
}
|
||||
|
||||
// Check cache.
|
||||
checkCache(FTP_URL);
|
||||
}
|
||||
}
|
||||
|
||||
let timeInMicroseconds = Date.now() * 1000;
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_task(function test_execute() {
|
||||
do_test_pending();
|
||||
|
||||
print("Initialize browserglue before Places");
|
||||
add_task(function* test_execute() {
|
||||
do_print("Initialize browserglue before Places");
|
||||
|
||||
// Avoid default bookmarks import.
|
||||
let glue = Cc["@mozilla.org/browser/browserglue;1"].
|
||||
|
@ -105,66 +63,78 @@ add_task(function test_execute() {
|
|||
|
||||
Services.prefs.setBoolPref("privacy.sanitize.sanitizeOnShutdown", true);
|
||||
|
||||
print("Add visits.");
|
||||
do_print("Add visits.");
|
||||
for (let aUrl of URIS) {
|
||||
yield PlacesTestUtils.addVisits({
|
||||
uri: uri(aUrl), visitDate: timeInMicroseconds++,
|
||||
transition: PlacesUtils.history.TRANSITION_TYPED
|
||||
});
|
||||
}
|
||||
print("Add cache.");
|
||||
storeCache(FTP_URL, "testData");
|
||||
do_print("Add cache.");
|
||||
yield storeCache(FTP_URL, "testData");
|
||||
});
|
||||
|
||||
function run_test_continue()
|
||||
{
|
||||
print("Simulate and wait shutdown.");
|
||||
getDistinctNotifications().forEach(
|
||||
function (topic)
|
||||
Services.obs.addObserver(notificationsObserver, topic, false)
|
||||
add_task(function* run_test_continue() {
|
||||
do_print("Simulate and wait shutdown.");
|
||||
yield shutdownPlaces();
|
||||
|
||||
let stmt = DBConn().createStatement(
|
||||
"SELECT id FROM moz_places WHERE url = :page_url "
|
||||
);
|
||||
|
||||
shutdownPlaces();
|
||||
try {
|
||||
URIS.forEach(function(aUrl) {
|
||||
stmt.params.page_url = aUrl;
|
||||
do_check_false(stmt.executeStep());
|
||||
stmt.reset();
|
||||
});
|
||||
} finally {
|
||||
stmt.finalize();
|
||||
}
|
||||
|
||||
do_print("Check cache");
|
||||
// Check cache.
|
||||
let promiseCacheChecked = checkCache(FTP_URL);
|
||||
|
||||
do_print("Shutdown the download manager");
|
||||
// Shutdown the download manager.
|
||||
Services.obs.notifyObservers(null, "quit-application", null);
|
||||
}
|
||||
|
||||
function getDistinctNotifications() {
|
||||
let ar = EXPECTED_NOTIFICATIONS.concat(UNEXPECTED_NOTIFICATIONS);
|
||||
return [ar[i] for (i in ar) if (ar.slice(0, i).indexOf(ar[i]) == -1)];
|
||||
}
|
||||
yield promiseCacheChecked;
|
||||
});
|
||||
|
||||
function storeCache(aURL, aContent) {
|
||||
let cache = Services.cache2;
|
||||
let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
|
||||
|
||||
var storeCacheListener = {
|
||||
onCacheEntryCheck: function (entry, appcache) {
|
||||
return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
|
||||
},
|
||||
return new Promise(resolve => {
|
||||
let storeCacheListener = {
|
||||
onCacheEntryCheck: function (entry, appcache) {
|
||||
return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
|
||||
},
|
||||
|
||||
onCacheEntryAvailable: function (entry, isnew, appcache, status) {
|
||||
do_check_eq(status, Cr.NS_OK);
|
||||
onCacheEntryAvailable: function (entry, isnew, appcache, status) {
|
||||
do_check_eq(status, Cr.NS_OK);
|
||||
|
||||
entry.setMetaDataElement("servertype", "0");
|
||||
var os = entry.openOutputStream(0);
|
||||
entry.setMetaDataElement("servertype", "0");
|
||||
var os = entry.openOutputStream(0);
|
||||
|
||||
var written = os.write(aContent, aContent.length);
|
||||
if (written != aContent.length) {
|
||||
do_throw("os.write has not written all data!\n" +
|
||||
" Expected: " + written + "\n" +
|
||||
" Actual: " + aContent.length + "\n");
|
||||
var written = os.write(aContent, aContent.length);
|
||||
if (written != aContent.length) {
|
||||
do_throw("os.write has not written all data!\n" +
|
||||
" Expected: " + written + "\n" +
|
||||
" Actual: " + aContent.length + "\n");
|
||||
}
|
||||
os.close();
|
||||
entry.close();
|
||||
resolve();
|
||||
}
|
||||
os.close();
|
||||
entry.close();
|
||||
do_execute_soon(run_test_continue);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
storage.asyncOpenURI(Services.io.newURI(aURL, null, null), "",
|
||||
Ci.nsICacheStorage.OPEN_NORMALLY,
|
||||
storeCacheListener);
|
||||
storage.asyncOpenURI(Services.io.newURI(aURL, null, null), "",
|
||||
Ci.nsICacheStorage.OPEN_NORMALLY,
|
||||
storeCacheListener);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -172,14 +142,16 @@ function checkCache(aURL) {
|
|||
let cache = Services.cache2;
|
||||
let storage = cache.diskCacheStorage(LoadContextInfo.default, false);
|
||||
|
||||
var checkCacheListener = {
|
||||
onCacheEntryAvailable: function (entry, isnew, appcache, status) {
|
||||
do_check_eq(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
|
||||
do_test_finished();
|
||||
}
|
||||
};
|
||||
return new Promise(resolve => {
|
||||
let checkCacheListener = {
|
||||
onCacheEntryAvailable: function (entry, isnew, appcache, status) {
|
||||
do_check_eq(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
storage.asyncOpenURI(Services.io.newURI(aURL, null, null), "",
|
||||
Ci.nsICacheStorage.OPEN_READONLY,
|
||||
checkCacheListener);
|
||||
storage.asyncOpenURI(Services.io.newURI(aURL, null, null), "",
|
||||
Ci.nsICacheStorage.OPEN_READONLY,
|
||||
checkCacheListener);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -258,104 +258,106 @@ let Bookmarks = Object.freeze({
|
|||
, validIf: b => b.lastModified >= item.dateAdded }
|
||||
});
|
||||
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
let parent;
|
||||
if (updateInfo.hasOwnProperty("parentGuid")) {
|
||||
if (item.type == this.TYPE_FOLDER) {
|
||||
// Make sure we are not moving a folder into itself or one of its
|
||||
// descendants.
|
||||
let rows = yield db.executeCached(
|
||||
`WITH RECURSIVE
|
||||
descendants(did) AS (
|
||||
VALUES(:id)
|
||||
UNION ALL
|
||||
SELECT id FROM moz_bookmarks
|
||||
JOIN descendants ON parent = did
|
||||
WHERE type = :type
|
||||
)
|
||||
SELECT guid FROM moz_bookmarks
|
||||
WHERE id IN descendants
|
||||
`, { id: item._id, type: this.TYPE_FOLDER });
|
||||
if ([r.getResultByName("guid") for (r of rows)].indexOf(updateInfo.parentGuid) != -1)
|
||||
throw new Error("Cannot insert a folder into itself or one of its descendants");
|
||||
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: update",
|
||||
Task.async(function*(db) {
|
||||
let parent;
|
||||
if (updateInfo.hasOwnProperty("parentGuid")) {
|
||||
if (item.type == this.TYPE_FOLDER) {
|
||||
// Make sure we are not moving a folder into itself or one of its
|
||||
// descendants.
|
||||
let rows = yield db.executeCached(
|
||||
`WITH RECURSIVE
|
||||
descendants(did) AS (
|
||||
VALUES(:id)
|
||||
UNION ALL
|
||||
SELECT id FROM moz_bookmarks
|
||||
JOIN descendants ON parent = did
|
||||
WHERE type = :type
|
||||
)
|
||||
SELECT guid FROM moz_bookmarks
|
||||
WHERE id IN descendants
|
||||
`, { id: item._id, type: this.TYPE_FOLDER });
|
||||
if ([r.getResultByName("guid") for (r of rows)].indexOf(updateInfo.parentGuid) != -1)
|
||||
throw new Error("Cannot insert a folder into itself or one of its descendants");
|
||||
}
|
||||
|
||||
parent = yield fetchBookmark({ guid: updateInfo.parentGuid });
|
||||
if (!parent)
|
||||
throw new Error("No bookmarks found for the provided parentGuid");
|
||||
}
|
||||
|
||||
parent = yield fetchBookmark({ guid: updateInfo.parentGuid });
|
||||
if (!parent)
|
||||
throw new Error("No bookmarks found for the provided parentGuid");
|
||||
}
|
||||
if (updateInfo.hasOwnProperty("index")) {
|
||||
// If at this point we don't have a parent yet, we are moving into
|
||||
// the same container. Thus we know it exists.
|
||||
if (!parent)
|
||||
parent = yield fetchBookmark({ guid: item.parentGuid });
|
||||
|
||||
if (updateInfo.hasOwnProperty("index")) {
|
||||
// If at this point we don't have a parent yet, we are moving into
|
||||
// the same container. Thus we know it exists.
|
||||
if (!parent)
|
||||
parent = yield fetchBookmark({ guid: item.parentGuid });
|
||||
if (updateInfo.index >= parent._childCount ||
|
||||
updateInfo.index == this.DEFAULT_INDEX) {
|
||||
updateInfo.index = parent._childCount;
|
||||
|
||||
if (updateInfo.index >= parent._childCount ||
|
||||
updateInfo.index == this.DEFAULT_INDEX) {
|
||||
updateInfo.index = parent._childCount;
|
||||
|
||||
// Fix the index when moving within the same container.
|
||||
if (parent.guid == item.parentGuid)
|
||||
updateInfo.index--;
|
||||
// Fix the index when moving within the same container.
|
||||
if (parent.guid == item.parentGuid)
|
||||
updateInfo.index--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let updatedItem = yield updateBookmark(updateInfo, item, parent);
|
||||
let updatedItem = yield updateBookmark(updateInfo, item, parent);
|
||||
|
||||
if (item.type == this.TYPE_BOOKMARK &&
|
||||
item.url.href != updatedItem.url.href) {
|
||||
// ...though we don't wait for the calculation.
|
||||
updateFrecency(db, [item.url]).then(null, Cu.reportError);
|
||||
updateFrecency(db, [updatedItem.url]).then(null, Cu.reportError);
|
||||
}
|
||||
if (item.type == this.TYPE_BOOKMARK &&
|
||||
item.url.href != updatedItem.url.href) {
|
||||
// ...though we don't wait for the calculation.
|
||||
updateFrecency(db, [item.url]).then(null, Cu.reportError);
|
||||
updateFrecency(db, [updatedItem.url]).then(null, Cu.reportError);
|
||||
}
|
||||
|
||||
// Notify onItemChanged to listeners.
|
||||
let observers = PlacesUtils.bookmarks.getObservers();
|
||||
// For lastModified, we only care about the original input, since we
|
||||
// should not notify implciit lastModified changes.
|
||||
if (info.hasOwnProperty("lastModified") &&
|
||||
updateInfo.hasOwnProperty("lastModified") &&
|
||||
item.lastModified != updatedItem.lastModified) {
|
||||
notify(observers, "onItemChanged", [ updatedItem._id, "lastModified",
|
||||
false,
|
||||
`${toPRTime(updatedItem.lastModified)}`,
|
||||
toPRTime(updatedItem.lastModified),
|
||||
updatedItem.type,
|
||||
updatedItem._parentId,
|
||||
updatedItem.guid,
|
||||
// Notify onItemChanged to listeners.
|
||||
let observers = PlacesUtils.bookmarks.getObservers();
|
||||
// For lastModified, we only care about the original input, since we
|
||||
// should not notify implciit lastModified changes.
|
||||
if (info.hasOwnProperty("lastModified") &&
|
||||
updateInfo.hasOwnProperty("lastModified") &&
|
||||
item.lastModified != updatedItem.lastModified) {
|
||||
notify(observers, "onItemChanged", [ updatedItem._id, "lastModified",
|
||||
false,
|
||||
`${toPRTime(updatedItem.lastModified)}`,
|
||||
toPRTime(updatedItem.lastModified),
|
||||
updatedItem.type,
|
||||
updatedItem._parentId,
|
||||
updatedItem.guid,
|
||||
updatedItem.parentGuid ]);
|
||||
}
|
||||
if (updateInfo.hasOwnProperty("title")) {
|
||||
notify(observers, "onItemChanged", [ updatedItem._id, "title",
|
||||
false, updatedItem.title,
|
||||
toPRTime(updatedItem.lastModified),
|
||||
updatedItem.type,
|
||||
updatedItem._parentId,
|
||||
updatedItem.guid,
|
||||
updatedItem.parentGuid ]);
|
||||
}
|
||||
if (updateInfo.hasOwnProperty("url")) {
|
||||
notify(observers, "onItemChanged", [ updatedItem._id, "uri",
|
||||
false, updatedItem.url.href,
|
||||
toPRTime(updatedItem.lastModified),
|
||||
updatedItem.type,
|
||||
updatedItem._parentId,
|
||||
updatedItem.guid,
|
||||
updatedItem.parentGuid ]);
|
||||
}
|
||||
// If the item was moved, notify onItemMoved.
|
||||
if (item.parentGuid != updatedItem.parentGuid ||
|
||||
item.index != updatedItem.index) {
|
||||
notify(observers, "onItemMoved", [ updatedItem._id, item._parentId,
|
||||
item.index, updatedItem._parentId,
|
||||
updatedItem.index, updatedItem.type,
|
||||
updatedItem.guid, item.parentGuid,
|
||||
updatedItem.parentGuid ]);
|
||||
}
|
||||
if (updateInfo.hasOwnProperty("title")) {
|
||||
notify(observers, "onItemChanged", [ updatedItem._id, "title",
|
||||
false, updatedItem.title,
|
||||
toPRTime(updatedItem.lastModified),
|
||||
updatedItem.type,
|
||||
updatedItem._parentId,
|
||||
updatedItem.guid,
|
||||
updatedItem.parentGuid ]);
|
||||
}
|
||||
if (updateInfo.hasOwnProperty("url")) {
|
||||
notify(observers, "onItemChanged", [ updatedItem._id, "uri",
|
||||
false, updatedItem.url.href,
|
||||
toPRTime(updatedItem.lastModified),
|
||||
updatedItem.type,
|
||||
updatedItem._parentId,
|
||||
updatedItem.guid,
|
||||
updatedItem.parentGuid ]);
|
||||
}
|
||||
// If the item was moved, notify onItemMoved.
|
||||
if (item.parentGuid != updatedItem.parentGuid ||
|
||||
item.index != updatedItem.index) {
|
||||
notify(observers, "onItemMoved", [ updatedItem._id, item._parentId,
|
||||
item.index, updatedItem._parentId,
|
||||
updatedItem.index, updatedItem.type,
|
||||
updatedItem.guid, item.parentGuid,
|
||||
updatedItem.parentGuid ]);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove non-enumerable properties.
|
||||
return Object.assign({}, updatedItem);
|
||||
// Remove non-enumerable properties.
|
||||
return Object.assign({}, updatedItem);
|
||||
}.bind(this)));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
|
@ -425,20 +427,21 @@ let Bookmarks = Object.freeze({
|
|||
* @return {Promise} resolved when the removal is complete.
|
||||
* @resolves once the removal is complete.
|
||||
*/
|
||||
eraseEverything: Task.async(function* () {
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
yield db.executeTransaction(function* () {
|
||||
const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid];
|
||||
yield removeFoldersContents(db, folderGuids);
|
||||
const time = toPRTime(new Date());
|
||||
for (let folderGuid of folderGuids) {
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET lastModified = :time
|
||||
WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
|
||||
`, { folderGuid, time });
|
||||
}
|
||||
}.bind(this));
|
||||
}),
|
||||
eraseEverything: function() {
|
||||
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: eraseEverything",
|
||||
db => db.executeTransaction(function* () {
|
||||
const folderGuids = [this.toolbarGuid, this.menuGuid, this.unfiledGuid];
|
||||
yield removeFoldersContents(db, folderGuids);
|
||||
const time = toPRTime(new Date());
|
||||
for (let folderGuid of folderGuids) {
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET lastModified = :time
|
||||
WHERE id IN (SELECT id FROM moz_bookmarks WHERE guid = :folderGuid )
|
||||
`, { folderGuid, time });
|
||||
}
|
||||
}.bind(this))
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches information about a bookmark-item.
|
||||
|
@ -673,340 +676,355 @@ function notify(observers, notification, args) {
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Update implementation.
|
||||
|
||||
function* updateBookmark(info, item, newParent) {
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
function updateBookmark(info, item, newParent) {
|
||||
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
|
||||
Task.async(function*(db) {
|
||||
|
||||
let tuples = new Map();
|
||||
if (info.hasOwnProperty("lastModified"))
|
||||
tuples.set("lastModified", { value: toPRTime(info.lastModified) });
|
||||
if (info.hasOwnProperty("title"))
|
||||
tuples.set("title", { value: info.title });
|
||||
let tuples = new Map();
|
||||
if (info.hasOwnProperty("lastModified"))
|
||||
tuples.set("lastModified", { value: toPRTime(info.lastModified) });
|
||||
if (info.hasOwnProperty("title"))
|
||||
tuples.set("title", { value: info.title });
|
||||
|
||||
yield db.executeTransaction(function* () {
|
||||
if (info.hasOwnProperty("url")) {
|
||||
// Ensure a page exists in moz_places for this URL.
|
||||
yield db.executeCached(
|
||||
`INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid)
|
||||
VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
|
||||
`, { url: info.url ? info.url.href : null,
|
||||
rev_host: PlacesUtils.getReversedHost(info.url),
|
||||
frecency: info.url.protocol == "place:" ? 0 : -1 });
|
||||
tuples.set("url", { value: info.url.href
|
||||
, fragment: "fk = (SELECT id FROM moz_places WHERE url = :url)" });
|
||||
}
|
||||
|
||||
if (newParent) {
|
||||
// For simplicity, update the index regardless.
|
||||
let newIndex = info.hasOwnProperty("index") ? info.index : item.index;
|
||||
tuples.set("position", { value: newIndex });
|
||||
|
||||
if (newParent.guid == item.parentGuid) {
|
||||
// Moving inside the original container.
|
||||
// When moving "up", add 1 to each index in the interval.
|
||||
// Otherwise when moving down, we subtract 1.
|
||||
let sign = newIndex < item.index ? +1 : -1;
|
||||
yield db.executeTransaction(function* () {
|
||||
if (info.hasOwnProperty("url")) {
|
||||
// Ensure a page exists in moz_places for this URL.
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = position + :sign
|
||||
WHERE parent = :newParentId
|
||||
AND position BETWEEN :lowIndex AND :highIndex
|
||||
`, { sign: sign, newParentId: newParent._id,
|
||||
lowIndex: Math.min(item.index, newIndex),
|
||||
highIndex: Math.max(item.index, newIndex) });
|
||||
} else {
|
||||
// Moving across different containers.
|
||||
tuples.set("parent", { value: newParent._id} );
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = position + :sign
|
||||
WHERE parent = :oldParentId
|
||||
AND position >= :oldIndex
|
||||
`, { sign: -1, oldParentId: item._parentId, oldIndex: item.index });
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = position + :sign
|
||||
WHERE parent = :newParentId
|
||||
AND position >= :newIndex
|
||||
`, { sign: +1, newParentId: newParent._id, newIndex: newIndex });
|
||||
|
||||
yield setAncestorsLastModified(db, item.parentGuid, info.lastModified);
|
||||
`INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid)
|
||||
VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
|
||||
`, { url: info.url ? info.url.href : null,
|
||||
rev_host: PlacesUtils.getReversedHost(info.url),
|
||||
frecency: info.url.protocol == "place:" ? 0 : -1 });
|
||||
tuples.set("url", { value: info.url.href
|
||||
, fragment: "fk = (SELECT id FROM moz_places WHERE url = :url)" });
|
||||
}
|
||||
yield setAncestorsLastModified(db, newParent.guid, info.lastModified);
|
||||
|
||||
if (newParent) {
|
||||
// For simplicity, update the index regardless.
|
||||
let newIndex = info.hasOwnProperty("index") ? info.index : item.index;
|
||||
tuples.set("position", { value: newIndex });
|
||||
|
||||
if (newParent.guid == item.parentGuid) {
|
||||
// Moving inside the original container.
|
||||
// When moving "up", add 1 to each index in the interval.
|
||||
// Otherwise when moving down, we subtract 1.
|
||||
let sign = newIndex < item.index ? +1 : -1;
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = position + :sign
|
||||
WHERE parent = :newParentId
|
||||
AND position BETWEEN :lowIndex AND :highIndex
|
||||
`, { sign: sign, newParentId: newParent._id,
|
||||
lowIndex: Math.min(item.index, newIndex),
|
||||
highIndex: Math.max(item.index, newIndex) });
|
||||
} else {
|
||||
// Moving across different containers.
|
||||
tuples.set("parent", { value: newParent._id} );
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = position + :sign
|
||||
WHERE parent = :oldParentId
|
||||
AND position >= :oldIndex
|
||||
`, { sign: -1, oldParentId: item._parentId, oldIndex: item.index });
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = position + :sign
|
||||
WHERE parent = :newParentId
|
||||
AND position >= :newIndex
|
||||
`, { sign: +1, newParentId: newParent._id, newIndex: newIndex });
|
||||
|
||||
yield setAncestorsLastModified(db, item.parentGuid, info.lastModified);
|
||||
}
|
||||
yield setAncestorsLastModified(db, newParent.guid, info.lastModified);
|
||||
}
|
||||
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks
|
||||
SET ${[tuples.get(v).fragment || `${v} = :${v}` for (v of tuples.keys())].join(", ")}
|
||||
WHERE guid = :guid
|
||||
`, Object.assign({ guid: info.guid },
|
||||
[...tuples.entries()].reduce((p, c) => { p[c[0]] = c[1].value; return p; }, {})));
|
||||
});
|
||||
|
||||
// If the parent changed, update related non-enumerable properties.
|
||||
let additionalParentInfo = {};
|
||||
if (newParent) {
|
||||
Object.defineProperty(additionalParentInfo, "_parentId",
|
||||
{ value: newParent._id, enumerable: false });
|
||||
Object.defineProperty(additionalParentInfo, "_grandParentId",
|
||||
{ value: newParent._parentId, enumerable: false });
|
||||
}
|
||||
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks
|
||||
SET ${[tuples.get(v).fragment || `${v} = :${v}` for (v of tuples.keys())].join(", ")}
|
||||
WHERE guid = :guid
|
||||
`, Object.assign({ guid: info.guid },
|
||||
[...tuples.entries()].reduce((p, c) => { p[c[0]] = c[1].value; return p; }, {})));
|
||||
});
|
||||
let updatedItem = mergeIntoNewObject(item, info, additionalParentInfo);
|
||||
|
||||
// If the parent changed, update related non-enumerable properties.
|
||||
let additionalParentInfo = {};
|
||||
if (newParent) {
|
||||
Object.defineProperty(additionalParentInfo, "_parentId",
|
||||
{ value: newParent._id, enumerable: false });
|
||||
Object.defineProperty(additionalParentInfo, "_grandParentId",
|
||||
{ value: newParent._parentId, enumerable: false });
|
||||
}
|
||||
// Don't return an empty title to the caller.
|
||||
if (updatedItem.hasOwnProperty("title") && updatedItem.title === null)
|
||||
delete updatedItem.title;
|
||||
|
||||
let updatedItem = mergeIntoNewObject(item, info, additionalParentInfo);
|
||||
|
||||
// Don't return an empty title to the caller.
|
||||
if (updatedItem.hasOwnProperty("title") && updatedItem.title === null)
|
||||
delete updatedItem.title;
|
||||
|
||||
return updatedItem;
|
||||
return updatedItem;
|
||||
}));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Insert implementation.
|
||||
|
||||
function* insertBookmark(item, parent) {
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
function insertBookmark(item, parent) {
|
||||
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: insertBookmark",
|
||||
Task.async(function*(db) {
|
||||
|
||||
// If a guid was not provided, generate one, so we won't need to fetch the
|
||||
// bookmark just after having created it.
|
||||
if (!item.hasOwnProperty("guid"))
|
||||
item.guid = (yield db.executeCached("SELECT GENERATE_GUID() AS guid"))[0].getResultByName("guid");
|
||||
// If a guid was not provided, generate one, so we won't need to fetch the
|
||||
// bookmark just after having created it.
|
||||
if (!item.hasOwnProperty("guid"))
|
||||
item.guid = (yield db.executeCached("SELECT GENERATE_GUID() AS guid"))[0].getResultByName("guid");
|
||||
|
||||
yield db.executeTransaction(function* transaction() {
|
||||
if (item.type == Bookmarks.TYPE_BOOKMARK) {
|
||||
// Ensure a page exists in moz_places for this URL.
|
||||
yield db.executeTransaction(function* transaction() {
|
||||
if (item.type == Bookmarks.TYPE_BOOKMARK) {
|
||||
// Ensure a page exists in moz_places for this URL.
|
||||
yield db.executeCached(
|
||||
`INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid)
|
||||
VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
|
||||
`, { url: item.url.href, rev_host: PlacesUtils.getReversedHost(item.url),
|
||||
frecency: item.url.protocol == "place:" ? 0 : -1 });
|
||||
}
|
||||
|
||||
// Adjust indices.
|
||||
yield db.executeCached(
|
||||
`INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid)
|
||||
VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
|
||||
`, { url: item.url.href, rev_host: PlacesUtils.getReversedHost(item.url),
|
||||
frecency: item.url.protocol == "place:" ? 0 : -1 });
|
||||
`UPDATE moz_bookmarks SET position = position + 1
|
||||
WHERE parent = :parent
|
||||
AND position >= :index
|
||||
`, { parent: parent._id, index: item.index });
|
||||
|
||||
// Insert the bookmark into the database.
|
||||
yield db.executeCached(
|
||||
`INSERT INTO moz_bookmarks (fk, type, parent, position, title,
|
||||
dateAdded, lastModified, guid)
|
||||
VALUES ((SELECT id FROM moz_places WHERE url = :url), :type, :parent,
|
||||
:index, :title, :date_added, :last_modified, :guid)
|
||||
`, { url: item.hasOwnProperty("url") ? item.url.href : "nonexistent",
|
||||
type: item.type, parent: parent._id, index: item.index,
|
||||
title: item.title, date_added: toPRTime(item.dateAdded),
|
||||
last_modified: toPRTime(item.lastModified), guid: item.guid });
|
||||
|
||||
yield setAncestorsLastModified(db, item.parentGuid, item.dateAdded);
|
||||
});
|
||||
|
||||
// If not a tag recalculate frecency...
|
||||
let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
|
||||
if (item.type == Bookmarks.TYPE_BOOKMARK && !isTagging) {
|
||||
// ...though we don't wait for the calculation.
|
||||
updateFrecency(db, [item.url]).then(null, Cu.reportError);
|
||||
}
|
||||
|
||||
// Adjust indices.
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = position + 1
|
||||
WHERE parent = :parent
|
||||
AND position >= :index
|
||||
`, { parent: parent._id, index: item.index });
|
||||
// Don't return an empty title to the caller.
|
||||
if (item.hasOwnProperty("title") && item.title === null)
|
||||
delete item.title;
|
||||
|
||||
// Insert the bookmark into the database.
|
||||
yield db.executeCached(
|
||||
`INSERT INTO moz_bookmarks (fk, type, parent, position, title,
|
||||
dateAdded, lastModified, guid)
|
||||
VALUES ((SELECT id FROM moz_places WHERE url = :url), :type, :parent,
|
||||
:index, :title, :date_added, :last_modified, :guid)
|
||||
`, { url: item.hasOwnProperty("url") ? item.url.href : "nonexistent",
|
||||
type: item.type, parent: parent._id, index: item.index,
|
||||
title: item.title, date_added: toPRTime(item.dateAdded),
|
||||
last_modified: toPRTime(item.lastModified), guid: item.guid });
|
||||
|
||||
yield setAncestorsLastModified(db, item.parentGuid, item.dateAdded);
|
||||
});
|
||||
|
||||
// If not a tag recalculate frecency...
|
||||
let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
|
||||
if (item.type == Bookmarks.TYPE_BOOKMARK && !isTagging) {
|
||||
// ...though we don't wait for the calculation.
|
||||
updateFrecency(db, [item.url]).then(null, Cu.reportError);
|
||||
}
|
||||
|
||||
// Don't return an empty title to the caller.
|
||||
if (item.hasOwnProperty("title") && item.title === null)
|
||||
delete item.title;
|
||||
return item;
|
||||
return item;
|
||||
}));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Fetch implementation.
|
||||
|
||||
function* fetchBookmark(info) {
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
function fetchBookmark(info) {
|
||||
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmark",
|
||||
Task.async(function*(db) {
|
||||
|
||||
let rows = yield db.executeCached(
|
||||
`SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
|
||||
b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
|
||||
b.id AS _id, b.parent AS _parentId,
|
||||
(SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
|
||||
p.parent AS _grandParentId
|
||||
FROM moz_bookmarks b
|
||||
LEFT JOIN moz_bookmarks p ON p.id = b.parent
|
||||
LEFT JOIN moz_places h ON h.id = b.fk
|
||||
WHERE b.guid = :guid
|
||||
`, { guid: info.guid });
|
||||
let rows = yield db.executeCached(
|
||||
`SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
|
||||
b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
|
||||
b.id AS _id, b.parent AS _parentId,
|
||||
(SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
|
||||
p.parent AS _grandParentId
|
||||
FROM moz_bookmarks b
|
||||
LEFT JOIN moz_bookmarks p ON p.id = b.parent
|
||||
LEFT JOIN moz_places h ON h.id = b.fk
|
||||
WHERE b.guid = :guid
|
||||
`, { guid: info.guid });
|
||||
|
||||
return rows.length ? rowsToItemsArray(rows)[0] : null;
|
||||
return rows.length ? rowsToItemsArray(rows)[0] : null;
|
||||
}));
|
||||
}
|
||||
|
||||
function* fetchBookmarkByPosition(info) {
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index;
|
||||
function fetchBookmarkByPosition(info) {
|
||||
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarkByPosition",
|
||||
Task.async(function*(db) {
|
||||
let index = info.index == Bookmarks.DEFAULT_INDEX ? null : info.index;
|
||||
|
||||
let rows = yield db.executeCached(
|
||||
`SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
|
||||
b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
|
||||
b.id AS _id, b.parent AS _parentId,
|
||||
(SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
|
||||
p.parent AS _grandParentId
|
||||
FROM moz_bookmarks b
|
||||
LEFT JOIN moz_bookmarks p ON p.id = b.parent
|
||||
LEFT JOIN moz_places h ON h.id = b.fk
|
||||
WHERE p.guid = :parentGuid
|
||||
AND b.position = IFNULL(:index, (SELECT count(*) - 1
|
||||
FROM moz_bookmarks
|
||||
WHERE parent = p.id))
|
||||
`, { parentGuid: info.parentGuid, index });
|
||||
let rows = yield db.executeCached(
|
||||
`SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
|
||||
b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
|
||||
b.id AS _id, b.parent AS _parentId,
|
||||
(SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
|
||||
p.parent AS _grandParentId
|
||||
FROM moz_bookmarks b
|
||||
LEFT JOIN moz_bookmarks p ON p.id = b.parent
|
||||
LEFT JOIN moz_places h ON h.id = b.fk
|
||||
WHERE p.guid = :parentGuid
|
||||
AND b.position = IFNULL(:index, (SELECT count(*) - 1
|
||||
FROM moz_bookmarks
|
||||
WHERE parent = p.id))
|
||||
`, { parentGuid: info.parentGuid, index });
|
||||
|
||||
return rows.length ? rowsToItemsArray(rows)[0] : null;
|
||||
return rows.length ? rowsToItemsArray(rows)[0] : null;
|
||||
}));
|
||||
}
|
||||
|
||||
function* fetchBookmarksByURL(info) {
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
function fetchBookmarksByURL(info) {
|
||||
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByURL",
|
||||
Task.async(function*(db) {
|
||||
|
||||
let rows = yield db.executeCached(
|
||||
`SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
|
||||
b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
|
||||
b.id AS _id, b.parent AS _parentId,
|
||||
(SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
|
||||
p.parent AS _grandParentId
|
||||
FROM moz_bookmarks b
|
||||
LEFT JOIN moz_bookmarks p ON p.id = b.parent
|
||||
LEFT JOIN moz_places h ON h.id = b.fk
|
||||
WHERE h.url = :url
|
||||
AND _grandParentId <> :tags_folder
|
||||
ORDER BY b.lastModified DESC
|
||||
`, { url: info.url.href,
|
||||
tags_folder: PlacesUtils.tagsFolderId });
|
||||
let rows = yield db.executeCached(
|
||||
`SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
|
||||
b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
|
||||
b.id AS _id, b.parent AS _parentId,
|
||||
(SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
|
||||
p.parent AS _grandParentId
|
||||
FROM moz_bookmarks b
|
||||
LEFT JOIN moz_bookmarks p ON p.id = b.parent
|
||||
LEFT JOIN moz_places h ON h.id = b.fk
|
||||
WHERE h.url = :url
|
||||
AND _grandParentId <> :tags_folder
|
||||
ORDER BY b.lastModified DESC
|
||||
`, { url: info.url.href,
|
||||
tags_folder: PlacesUtils.tagsFolderId });
|
||||
|
||||
return rows.length ? rowsToItemsArray(rows) : null;
|
||||
return rows.length ? rowsToItemsArray(rows) : null;
|
||||
}));
|
||||
}
|
||||
|
||||
function* fetchBookmarksByParent(info) {
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
function fetchBookmarksByParent(info) {
|
||||
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByParent",
|
||||
Task.async(function*(db) {
|
||||
|
||||
let rows = yield db.executeCached(
|
||||
`SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
|
||||
b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
|
||||
b.id AS _id, b.parent AS _parentId,
|
||||
(SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
|
||||
p.parent AS _grandParentId
|
||||
FROM moz_bookmarks b
|
||||
LEFT JOIN moz_bookmarks p ON p.id = b.parent
|
||||
LEFT JOIN moz_places h ON h.id = b.fk
|
||||
WHERE p.guid = :parentGuid
|
||||
ORDER BY b.position ASC
|
||||
`, { parentGuid: info.parentGuid });
|
||||
let rows = yield db.executeCached(
|
||||
`SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
|
||||
b.dateAdded, b.lastModified, b.type, b.title, h.url AS url,
|
||||
b.id AS _id, b.parent AS _parentId,
|
||||
(SELECT count(*) FROM moz_bookmarks WHERE parent = b.id) AS _childCount,
|
||||
p.parent AS _grandParentId
|
||||
FROM moz_bookmarks b
|
||||
LEFT JOIN moz_bookmarks p ON p.id = b.parent
|
||||
LEFT JOIN moz_places h ON h.id = b.fk
|
||||
WHERE p.guid = :parentGuid
|
||||
ORDER BY b.position ASC
|
||||
`, { parentGuid: info.parentGuid });
|
||||
|
||||
return rowsToItemsArray(rows);
|
||||
return rowsToItemsArray(rows);
|
||||
}));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Remove implementation.
|
||||
|
||||
function* removeBookmark(item) {
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
function removeBookmark(item) {
|
||||
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
|
||||
Task.async(function*(db) {
|
||||
|
||||
let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
|
||||
let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
|
||||
|
||||
yield db.executeTransaction(function* transaction() {
|
||||
// If it's a folder, remove its contents first.
|
||||
if (item.type == Bookmarks.TYPE_FOLDER)
|
||||
yield removeFoldersContents(db, [item.guid]);
|
||||
yield db.executeTransaction(function* transaction() {
|
||||
// If it's a folder, remove its contents first.
|
||||
if (item.type == Bookmarks.TYPE_FOLDER)
|
||||
yield removeFoldersContents(db, [item.guid]);
|
||||
|
||||
// Remove annotations first. If it's a tag, we can avoid paying that cost.
|
||||
if (!isUntagging) {
|
||||
// We don't go through the annotations service for this cause otherwise
|
||||
// we'd get a pointless onItemChanged notification and it would also
|
||||
// set lastModified to an unexpected value.
|
||||
yield removeAnnotationsForItem(db, item._id);
|
||||
// Remove annotations first. If it's a tag, we can avoid paying that cost.
|
||||
if (!isUntagging) {
|
||||
// We don't go through the annotations service for this cause otherwise
|
||||
// we'd get a pointless onItemChanged notification and it would also
|
||||
// set lastModified to an unexpected value.
|
||||
yield removeAnnotationsForItem(db, item._id);
|
||||
}
|
||||
|
||||
// Remove the bookmark from the database.
|
||||
yield db.executeCached(
|
||||
`DELETE FROM moz_bookmarks WHERE guid = :guid`, { guid: item.guid });
|
||||
|
||||
// Fix indices in the parent.
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = position - 1 WHERE
|
||||
parent = :parentId AND position > :index
|
||||
`, { parentId: item._parentId, index: item.index });
|
||||
|
||||
yield setAncestorsLastModified(db, item.parentGuid, new Date());
|
||||
});
|
||||
|
||||
// If not a tag recalculate frecency...
|
||||
if (item.type == Bookmarks.TYPE_BOOKMARK && !isUntagging) {
|
||||
// ...though we don't wait for the calculation.
|
||||
updateFrecency(db, [item.url]).then(null, Cu.reportError);
|
||||
}
|
||||
|
||||
// Remove the bookmark from the database.
|
||||
yield db.executeCached(
|
||||
`DELETE FROM moz_bookmarks WHERE guid = :guid`, { guid: item.guid });
|
||||
|
||||
// Fix indices in the parent.
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = position - 1 WHERE
|
||||
parent = :parentId AND position > :index
|
||||
`, { parentId: item._parentId, index: item.index });
|
||||
|
||||
yield setAncestorsLastModified(db, item.parentGuid, new Date());
|
||||
});
|
||||
|
||||
// If not a tag recalculate frecency...
|
||||
if (item.type == Bookmarks.TYPE_BOOKMARK && !isUntagging) {
|
||||
// ...though we don't wait for the calculation.
|
||||
updateFrecency(db, [item.url]).then(null, Cu.reportError);
|
||||
}
|
||||
|
||||
return item;
|
||||
return item;
|
||||
}));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Reorder implementation.
|
||||
|
||||
function* reorderChildren(parent, orderedChildrenGuids) {
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
function reorderChildren(parent, orderedChildrenGuids) {
|
||||
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: updateBookmark",
|
||||
db => db.executeTransaction(function* () {
|
||||
// Select all of the direct children for the given parent.
|
||||
let children = yield fetchBookmarksByParent({ parentGuid: parent.guid });
|
||||
if (!children.length)
|
||||
return;
|
||||
|
||||
return db.executeTransaction(function* () {
|
||||
// Select all of the direct children for the given parent.
|
||||
let children = yield fetchBookmarksByParent({ parentGuid: parent.guid });
|
||||
if (!children.length)
|
||||
return;
|
||||
// Reorder the children array according to the specified order, provided
|
||||
// GUIDs come first, others are appended in somehow random order.
|
||||
children.sort((a, b) => {
|
||||
let i = orderedChildrenGuids.indexOf(a.guid);
|
||||
let j = orderedChildrenGuids.indexOf(b.guid);
|
||||
// This works provided fetchBookmarksByParent returns sorted children.
|
||||
return (i == -1 && j == -1) ? 0 :
|
||||
(i != -1 && j != -1 && i < j) || (i != -1 && j == -1) ? -1 : 1;
|
||||
});
|
||||
|
||||
// Reorder the children array according to the specified order, provided
|
||||
// GUIDs come first, others are appended in somehow random order.
|
||||
children.sort((a, b) => {
|
||||
let i = orderedChildrenGuids.indexOf(a.guid);
|
||||
let j = orderedChildrenGuids.indexOf(b.guid);
|
||||
// This works provided fetchBookmarksByParent returns sorted children.
|
||||
return (i == -1 && j == -1) ? 0 :
|
||||
(i != -1 && j != -1 && i < j) || (i != -1 && j == -1) ? -1 : 1;
|
||||
});
|
||||
// Update the bookmarks position now. If any unknown guid have been
|
||||
// inserted meanwhile, its position will be set to -position, and we'll
|
||||
// handle it later.
|
||||
// To do the update in a single step, we build a VALUES (guid, position)
|
||||
// table. We then use count() in the sorting table to avoid skipping values
|
||||
// when no more existing GUIDs have been provided.
|
||||
let valuesTable = children.map((child, i) => `("${child.guid}", ${i})`)
|
||||
.join();
|
||||
yield db.execute(
|
||||
`WITH sorting(g, p) AS (
|
||||
VALUES ${valuesTable}
|
||||
)
|
||||
UPDATE moz_bookmarks SET position = (
|
||||
SELECT CASE count(a.g) WHEN 0 THEN -position
|
||||
ELSE count(a.g) - 1
|
||||
END
|
||||
FROM sorting a
|
||||
JOIN sorting b ON b.p <= a.p
|
||||
WHERE a.g = guid
|
||||
AND parent = :parentId
|
||||
)`, { parentId: parent._id});
|
||||
|
||||
// Update the bookmarks position now. If any unknown guid have been
|
||||
// inserted meanwhile, its position will be set to -position, and we'll
|
||||
// handle it later.
|
||||
// To do the update in a single step, we build a VALUES (guid, position)
|
||||
// table. We then use count() in the sorting table to avoid skipping values
|
||||
// when no more existing GUIDs have been provided.
|
||||
let valuesTable = children.map((child, i) => `("${child.guid}", ${i})`)
|
||||
.join();
|
||||
yield db.execute(
|
||||
`WITH sorting(g, p) AS (
|
||||
VALUES ${valuesTable}
|
||||
)
|
||||
UPDATE moz_bookmarks SET position = (
|
||||
SELECT CASE count(a.g) WHEN 0 THEN -position
|
||||
ELSE count(a.g) - 1
|
||||
END
|
||||
FROM sorting a
|
||||
JOIN sorting b ON b.p <= a.p
|
||||
WHERE a.g = guid
|
||||
AND parent = :parentId
|
||||
)`, { parentId: parent._id});
|
||||
// Update position of items that could have been inserted in the meanwhile.
|
||||
// Since this can happen rarely and it's only done for schema coherence
|
||||
// resonds, we won't notify about these changes.
|
||||
yield db.executeCached(
|
||||
`CREATE TEMP TRIGGER moz_bookmarks_reorder_trigger
|
||||
AFTER UPDATE OF position ON moz_bookmarks
|
||||
WHEN NEW.position = -1
|
||||
BEGIN
|
||||
UPDATE moz_bookmarks
|
||||
SET position = (SELECT MAX(position) FROM moz_bookmarks
|
||||
WHERE parent = NEW.parent) +
|
||||
(SELECT count(*) FROM moz_bookmarks
|
||||
WHERE parent = NEW.parent
|
||||
AND position BETWEEN OLD.position AND -1)
|
||||
WHERE guid = NEW.guid;
|
||||
END
|
||||
`);
|
||||
|
||||
// Update position of items that could have been inserted in the meanwhile.
|
||||
// Since this can happen rarely and it's only done for schema coherence
|
||||
// resonds, we won't notify about these changes.
|
||||
yield db.executeCached(
|
||||
`CREATE TEMP TRIGGER moz_bookmarks_reorder_trigger
|
||||
AFTER UPDATE OF position ON moz_bookmarks
|
||||
WHEN NEW.position = -1
|
||||
BEGIN
|
||||
UPDATE moz_bookmarks
|
||||
SET position = (SELECT MAX(position) FROM moz_bookmarks
|
||||
WHERE parent = NEW.parent) +
|
||||
(SELECT count(*) FROM moz_bookmarks
|
||||
WHERE parent = NEW.parent
|
||||
AND position BETWEEN OLD.position AND -1)
|
||||
WHERE guid = NEW.guid;
|
||||
END
|
||||
`);
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = -1 WHERE position < 0`);
|
||||
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_bookmarks SET position = -1 WHERE position < 0`);
|
||||
yield db.executeCached(`DROP TRIGGER moz_bookmarks_reorder_trigger`);
|
||||
|
||||
yield db.executeCached(`DROP TRIGGER moz_bookmarks_reorder_trigger`);
|
||||
|
||||
return children;
|
||||
}.bind(this));
|
||||
return children;
|
||||
}.bind(this))
|
||||
);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -220,43 +220,6 @@ SetJournalMode(nsCOMPtr<mozIStorageConnection>& aDBConn,
|
|||
return JOURNAL_DELETE;
|
||||
}
|
||||
|
||||
class ConnectionCloseCallback final : public mozIStorageCompletionCallback {
|
||||
bool mDone;
|
||||
|
||||
~ConnectionCloseCallback() {}
|
||||
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_MOZISTORAGECOMPLETIONCALLBACK
|
||||
ConnectionCloseCallback();
|
||||
};
|
||||
|
||||
NS_IMETHODIMP
|
||||
ConnectionCloseCallback::Complete(nsresult, nsISupports*)
|
||||
{
|
||||
mDone = true;
|
||||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||||
MOZ_ASSERT(os);
|
||||
if (!os)
|
||||
return NS_OK;
|
||||
DebugOnly<nsresult> rv = os->NotifyObservers(nullptr,
|
||||
TOPIC_PLACES_CONNECTION_CLOSED,
|
||||
nullptr);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
ConnectionCloseCallback::ConnectionCloseCallback()
|
||||
: mDone(false)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS(
|
||||
ConnectionCloseCallback
|
||||
, mozIStorageCompletionCallback
|
||||
)
|
||||
|
||||
nsresult
|
||||
CreateRoot(nsCOMPtr<mozIStorageConnection>& aDBConn,
|
||||
const nsCString& aRootName, const nsCString& aGuid,
|
||||
|
@ -329,6 +292,276 @@ CreateRoot(nsCOMPtr<mozIStorageConnection>& aDBConn,
|
|||
|
||||
} // Anonymous namespace
|
||||
|
||||
/**
|
||||
* An AsyncShutdown blocker in charge of shutting down places
|
||||
*/
|
||||
class DatabaseShutdown final:
|
||||
public nsIAsyncShutdownBlocker,
|
||||
public nsIAsyncShutdownCompletionCallback,
|
||||
public mozIStorageCompletionCallback
|
||||
{
|
||||
public:
|
||||
NS_DECL_THREADSAFE_ISUPPORTS
|
||||
NS_DECL_NSIASYNCSHUTDOWNBLOCKER
|
||||
NS_DECL_NSIASYNCSHUTDOWNCOMPLETIONCALLBACK
|
||||
NS_DECL_MOZISTORAGECOMPLETIONCALLBACK
|
||||
|
||||
explicit DatabaseShutdown(Database* aDatabase);
|
||||
|
||||
already_AddRefed<nsIAsyncShutdownClient> GetClient();
|
||||
|
||||
/**
|
||||
* `true` if we have not started shutdown, i.e. if
|
||||
* `BlockShutdown()` hasn't been called yet, false otherwise.
|
||||
*/
|
||||
bool IsStarted() const {
|
||||
return mIsStarted;
|
||||
}
|
||||
|
||||
private:
|
||||
nsCOMPtr<nsIAsyncShutdownBarrier> mBarrier;
|
||||
nsCOMPtr<nsIAsyncShutdownClient> mParentClient;
|
||||
|
||||
// The owning database.
|
||||
// The cycle is broken in method Complete(), once the connection
|
||||
// has been closed by mozStorage.
|
||||
nsRefPtr<Database> mDatabase;
|
||||
|
||||
// The current state, used both internally and for
|
||||
// forensics/debugging purposes.
|
||||
enum State {
|
||||
NOT_STARTED,
|
||||
|
||||
// Execution of `BlockShutdown` in progress
|
||||
// a. `BlockShutdown` is starting.
|
||||
RECEIVED_BLOCK_SHUTDOWN,
|
||||
// b. `BlockShutdown` is complete, waiting for clients.
|
||||
CALLED_WAIT_CLIENTS,
|
||||
|
||||
// Execution of `Done` in progress
|
||||
// a. `Done` is starting.
|
||||
RECEIVED_DONE,
|
||||
// b. We have notified observers that Places will close connection.
|
||||
NOTIFIED_OBSERVERS_PLACES_WILL_CLOSE_CONNECTION,
|
||||
// c. Execution of `Done` is complete, waiting for mozStorage shutdown.
|
||||
CALLED_STORAGESHUTDOWN,
|
||||
|
||||
// Execution of `Complete` in progress
|
||||
// a. `Complete` is starting.
|
||||
RECEIVED_STORAGESHUTDOWN_COMPLETE,
|
||||
// b. We have notified observers that Places as closed connection.
|
||||
NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED,
|
||||
};
|
||||
State mState;
|
||||
bool mIsStarted;
|
||||
|
||||
// As tests may resurrect a dead `Database`, we use a counter to
|
||||
// give the instances of `DatabaseShutdown` unique names.
|
||||
uint16_t mCounter;
|
||||
static uint16_t sCounter;
|
||||
|
||||
~DatabaseShutdown() {}
|
||||
};
|
||||
uint16_t DatabaseShutdown::sCounter = 0;
|
||||
|
||||
DatabaseShutdown::DatabaseShutdown(Database* aDatabase)
|
||||
: mDatabase(aDatabase)
|
||||
, mState(NOT_STARTED)
|
||||
, mIsStarted(false)
|
||||
, mCounter(sCounter++)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc = services::GetAsyncShutdown();
|
||||
MOZ_ASSERT(asyncShutdownSvc);
|
||||
|
||||
if (asyncShutdownSvc) {
|
||||
DebugOnly<nsresult> rv = asyncShutdownSvc->MakeBarrier(
|
||||
NS_LITERAL_STRING("Places Database shutdown"),
|
||||
getter_AddRefs(mBarrier)
|
||||
);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
}
|
||||
}
|
||||
|
||||
already_AddRefed<nsIAsyncShutdownClient>
|
||||
DatabaseShutdown::GetClient()
|
||||
{
|
||||
nsCOMPtr<nsIAsyncShutdownClient> client;
|
||||
if (mBarrier) {
|
||||
DebugOnly<nsresult> rv = mBarrier->GetClient(getter_AddRefs(client));
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
}
|
||||
return client.forget();
|
||||
}
|
||||
|
||||
// nsIAsyncShutdownBlocker::GetName
|
||||
NS_IMETHODIMP
|
||||
DatabaseShutdown::GetName(nsAString& aName)
|
||||
{
|
||||
if (mCounter > 0) {
|
||||
// During tests, we can end up with the Database singleton being resurrected.
|
||||
// Make sure that each instance of DatabaseShutdown has a unique name.
|
||||
nsPrintfCString name("Places DatabaseShutdown: Blocking profile-before-change (%x)", this);
|
||||
aName = NS_ConvertUTF8toUTF16(name);
|
||||
} else {
|
||||
aName = NS_LITERAL_STRING("Places DatabaseShutdown: Blocking profile-before-change");
|
||||
}
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// nsIAsyncShutdownBlocker::GetState
|
||||
NS_IMETHODIMP DatabaseShutdown::GetState(nsIPropertyBag** aState)
|
||||
{
|
||||
nsresult rv;
|
||||
nsCOMPtr<nsIWritablePropertyBag2> bag =
|
||||
do_CreateInstance("@mozilla.org/hash-property-bag;1", &rv);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) return rv;
|
||||
|
||||
// Put `mState` in field `progress`
|
||||
nsCOMPtr<nsIWritableVariant> progress =
|
||||
do_CreateInstance(NS_VARIANT_CONTRACTID, &rv);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) return rv;
|
||||
|
||||
rv = progress->SetAsUint8(mState);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) return rv;
|
||||
|
||||
rv = bag->SetPropertyAsInterface(NS_LITERAL_STRING("progress"), progress);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) return rv;
|
||||
|
||||
// Put `mBarrier`'s state in field `barrier`, if possible
|
||||
if (!mBarrier) {
|
||||
return NS_OK;
|
||||
}
|
||||
nsCOMPtr<nsIPropertyBag> barrierState;
|
||||
rv = mBarrier->GetState(getter_AddRefs(barrierState));
|
||||
if (NS_FAILED(rv)) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIWritableVariant> barrier =
|
||||
do_CreateInstance(NS_VARIANT_CONTRACTID, &rv);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) return rv;
|
||||
|
||||
rv = barrier->SetAsInterface(NS_GET_IID(nsIPropertyBag), barrierState);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) return rv;
|
||||
|
||||
rv = bag->SetPropertyAsInterface(NS_LITERAL_STRING("Barrier"), barrier);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) return rv;
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
||||
// nsIAsyncShutdownBlocker::BlockShutdown
|
||||
//
|
||||
// Step 1 in shutdown, called during profile-before-change.
|
||||
// As a `nsIAsyncShutdownBarrier`, we now need to wait until all clients
|
||||
// of `this` barrier have completed their own shutdown.
|
||||
//
|
||||
// See `Done()` for step 2.
|
||||
NS_IMETHODIMP
|
||||
DatabaseShutdown::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
|
||||
{
|
||||
mParentClient = aParentClient;
|
||||
mState = RECEIVED_BLOCK_SHUTDOWN;
|
||||
mIsStarted = true;
|
||||
|
||||
if (NS_WARN_IF(!mBarrier)) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
|
||||
// Wait until all clients have removed their blockers, then proceed
|
||||
// with own shutdown.
|
||||
DebugOnly<nsresult> rv = mBarrier->Wait(this);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
|
||||
mState = CALLED_WAIT_CLIENTS;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// nsIAsyncShutdownCompletionCallback::Done
|
||||
//
|
||||
// Step 2 in shutdown, called once all clients have removed their blockers.
|
||||
// We may now check sanity, inform observers, and close the database handler.
|
||||
//
|
||||
// See `Complete()` for step 3.
|
||||
NS_IMETHODIMP
|
||||
DatabaseShutdown::Done()
|
||||
{
|
||||
mState = RECEIVED_DONE;
|
||||
|
||||
// Fire internal shutdown notifications.
|
||||
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
|
||||
MOZ_ASSERT(os);
|
||||
if (os) {
|
||||
(void)os->NotifyObservers(nullptr, TOPIC_PLACES_WILL_CLOSE_CONNECTION, nullptr);
|
||||
}
|
||||
mState = NOTIFIED_OBSERVERS_PLACES_WILL_CLOSE_CONNECTION;
|
||||
|
||||
// At this stage, any use of this database is forbidden. Get rid of
|
||||
// `gDatabase`. Note, however, that the database could be
|
||||
// resurrected. This can happen in particular during tests.
|
||||
MOZ_ASSERT(Database::gDatabase == nullptr || Database::gDatabase == mDatabase);
|
||||
Database::gDatabase = nullptr;
|
||||
|
||||
mDatabase->Shutdown();
|
||||
mState = CALLED_STORAGESHUTDOWN;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
||||
// mozIStorageCompletionCallback::Complete
|
||||
//
|
||||
// Step 3 (and last step) of shutdown
|
||||
//
|
||||
// Called once the connection has been closed by mozStorage.
|
||||
// Inform observers of TOPIC_PLACES_CONNECTION_CLOSED.
|
||||
//
|
||||
NS_IMETHODIMP
|
||||
DatabaseShutdown::Complete(nsresult, nsISupports*)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
mState = RECEIVED_STORAGESHUTDOWN_COMPLETE;
|
||||
mDatabase = nullptr;
|
||||
|
||||
nsresult rv;
|
||||
if (mParentClient) {
|
||||
// mParentClient may be nullptr in tests
|
||||
rv = mParentClient->RemoveBlocker(this);
|
||||
if (NS_WARN_IF(NS_FAILED(rv))) return rv;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||||
MOZ_ASSERT(os);
|
||||
if (os) {
|
||||
rv = os->NotifyObservers(nullptr,
|
||||
TOPIC_PLACES_CONNECTION_CLOSED,
|
||||
nullptr);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
}
|
||||
mState = NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED;
|
||||
|
||||
if (NS_WARN_IF(!mBarrier)) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
}
|
||||
nsCOMPtr<nsIAsyncShutdownBarrier> barrier = mBarrier.forget();
|
||||
nsCOMPtr<nsIAsyncShutdownClient> parentClient = mParentClient.forget();
|
||||
nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
|
||||
MOZ_ASSERT(mainThread);
|
||||
|
||||
NS_ProxyRelease(mainThread, barrier);
|
||||
NS_ProxyRelease(mainThread, parentClient);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS(
|
||||
DatabaseShutdown
|
||||
, nsIAsyncShutdownBlocker
|
||||
, nsIAsyncShutdownCompletionCallback
|
||||
, mozIStorageCompletionCallback
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//// Database
|
||||
|
||||
|
@ -345,26 +578,87 @@ Database::Database()
|
|||
, mAsyncThreadStatements(mMainConn)
|
||||
, mDBPageSize(0)
|
||||
, mDatabaseStatus(nsINavHistoryService::DATABASE_STATUS_OK)
|
||||
, mShuttingDown(false)
|
||||
, mClosed(false)
|
||||
, mConnectionShutdown(new DatabaseShutdown(this))
|
||||
{
|
||||
MOZ_ASSERT(XRE_GetProcessType() != GeckoProcessType_Content,
|
||||
"Cannot instantiate Places in the content process");
|
||||
// Attempting to create two instances of the service?
|
||||
MOZ_ASSERT(!gDatabase);
|
||||
gDatabase = this;
|
||||
|
||||
// Prepare async shutdown
|
||||
nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetShutdownPhase();
|
||||
MOZ_ASSERT(shutdownPhase);
|
||||
|
||||
if (shutdownPhase) {
|
||||
DebugOnly<nsresult> rv = shutdownPhase->AddBlocker(
|
||||
static_cast<nsIAsyncShutdownBlocker*>(mConnectionShutdown.get()),
|
||||
NS_LITERAL_STRING(__FILE__),
|
||||
__LINE__,
|
||||
NS_LITERAL_STRING(""));
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
}
|
||||
}
|
||||
|
||||
already_AddRefed<nsIAsyncShutdownClient>
|
||||
Database::GetShutdownPhase()
|
||||
{
|
||||
nsCOMPtr<nsIAsyncShutdownService> asyncShutdownSvc = services::GetAsyncShutdown();
|
||||
MOZ_ASSERT(asyncShutdownSvc);
|
||||
if (NS_WARN_IF(!asyncShutdownSvc)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase;
|
||||
DebugOnly<nsresult> rv = asyncShutdownSvc->
|
||||
GetProfileBeforeChange(getter_AddRefs(shutdownPhase));
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
return shutdownPhase.forget();
|
||||
}
|
||||
|
||||
Database::~Database()
|
||||
{
|
||||
// Check to make sure it's us, in case somebody wrongly creates an extra
|
||||
// instance of this singleton class.
|
||||
MOZ_ASSERT(gDatabase == this);
|
||||
}
|
||||
|
||||
// Remove the static reference to the service.
|
||||
if (gDatabase == this) {
|
||||
gDatabase = nullptr;
|
||||
bool
|
||||
Database::IsShutdownStarted() const
|
||||
{
|
||||
if (!mConnectionShutdown) {
|
||||
// We have already broken the cycle between `this` and `mConnectionShutdown`.
|
||||
return true;
|
||||
}
|
||||
return mConnectionShutdown->IsStarted();
|
||||
}
|
||||
|
||||
already_AddRefed<mozIStorageAsyncStatement>
|
||||
Database::GetAsyncStatement(const nsACString& aQuery) const
|
||||
{
|
||||
if (IsShutdownStarted()) {
|
||||
return nullptr;
|
||||
}
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
return mMainThreadAsyncStatements.GetCachedStatement(aQuery);
|
||||
}
|
||||
|
||||
already_AddRefed<mozIStorageStatement>
|
||||
Database::GetStatement(const nsACString& aQuery) const
|
||||
{
|
||||
if (IsShutdownStarted()) {
|
||||
return nullptr;
|
||||
}
|
||||
if (NS_IsMainThread()) {
|
||||
return mMainThreadStatements.GetCachedStatement(aQuery);
|
||||
}
|
||||
return mAsyncThreadStatements.GetCachedStatement(aQuery);
|
||||
}
|
||||
|
||||
already_AddRefed<nsIAsyncShutdownClient>
|
||||
Database::GetConnectionShutdown()
|
||||
{
|
||||
MOZ_ASSERT(mConnectionShutdown);
|
||||
|
||||
return mConnectionShutdown->GetClient();
|
||||
}
|
||||
|
||||
nsresult
|
||||
|
@ -438,7 +732,6 @@ Database::Init()
|
|||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||||
if (os) {
|
||||
(void)os->AddObserver(this, TOPIC_PROFILE_CHANGE_TEARDOWN, true);
|
||||
(void)os->AddObserver(this, TOPIC_PROFILE_BEFORE_CHANGE, true);
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
|
@ -1607,25 +1900,87 @@ Database::MigrateV28Up() {
|
|||
void
|
||||
Database::Shutdown()
|
||||
{
|
||||
|
||||
// As the last step in the shutdown path, finalize the database handle.
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(!mShuttingDown);
|
||||
MOZ_ASSERT(!mClosed);
|
||||
|
||||
mShuttingDown = true;
|
||||
// Break cycle
|
||||
nsCOMPtr<mozIStorageCompletionCallback> closeListener = mConnectionShutdown.forget();
|
||||
|
||||
if (!mMainConn) {
|
||||
// The connection has never been initialized. Just mark it
|
||||
// as closed.
|
||||
mClosed = true;
|
||||
(void)closeListener->Complete(NS_OK, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
{ // Sanity check for missing guids.
|
||||
bool haveNullGuids = false;
|
||||
nsCOMPtr<mozIStorageStatement> stmt;
|
||||
|
||||
nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
|
||||
"SELECT 1 "
|
||||
"FROM moz_places "
|
||||
"WHERE guid IS NULL "
|
||||
), getter_AddRefs(stmt));
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
rv = stmt->ExecuteStep(&haveNullGuids);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
MOZ_ASSERT(!haveNullGuids && "Found a page without a GUID!");
|
||||
|
||||
rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
|
||||
"SELECT 1 "
|
||||
"FROM moz_bookmarks "
|
||||
"WHERE guid IS NULL "
|
||||
), getter_AddRefs(stmt));
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
rv = stmt->ExecuteStep(&haveNullGuids);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
MOZ_ASSERT(!haveNullGuids && "Found a bookmark without a GUID!");
|
||||
|
||||
rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
|
||||
"SELECT 1 "
|
||||
"FROM moz_favicons "
|
||||
"WHERE guid IS NULL "
|
||||
), getter_AddRefs(stmt));
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
rv = stmt->ExecuteStep(&haveNullGuids);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
MOZ_ASSERT(!haveNullGuids && "Found a favicon without a GUID!");
|
||||
}
|
||||
|
||||
{ // Sanity check for unrounded dateAdded and lastModified values (bug
|
||||
// 1107308).
|
||||
bool hasUnroundedDates = false;
|
||||
nsCOMPtr<mozIStorageStatement> stmt;
|
||||
|
||||
nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
|
||||
"SELECT 1 "
|
||||
"FROM moz_bookmarks "
|
||||
"WHERE dateAdded % 1000 > 0 OR lastModified % 1000 > 0 LIMIT 1"
|
||||
), getter_AddRefs(stmt));
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
rv = stmt->ExecuteStep(&hasUnroundedDates);
|
||||
MOZ_ASSERT(NS_SUCCEEDED(rv));
|
||||
MOZ_ASSERT(!hasUnroundedDates && "Found unrounded dates!");
|
||||
}
|
||||
#endif
|
||||
|
||||
mMainThreadStatements.FinalizeStatements();
|
||||
mMainThreadAsyncStatements.FinalizeStatements();
|
||||
|
||||
nsRefPtr< FinalizeStatementCacheProxy<mozIStorageStatement> > event =
|
||||
new FinalizeStatementCacheProxy<mozIStorageStatement>(
|
||||
mAsyncThreadStatements, NS_ISUPPORTS_CAST(nsIObserver*, this)
|
||||
mAsyncThreadStatements,
|
||||
NS_ISUPPORTS_CAST(nsIObserver*, this)
|
||||
);
|
||||
DispatchToAsyncThread(event);
|
||||
|
||||
mClosed = true;
|
||||
|
||||
nsRefPtr<ConnectionCloseCallback> closeListener =
|
||||
new ConnectionCloseCallback();
|
||||
(void)mMainConn->AsyncClose(closeListener);
|
||||
}
|
||||
|
||||
|
@ -1638,10 +1993,10 @@ Database::Observe(nsISupports *aSubject,
|
|||
const char16_t *aData)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
if (strcmp(aTopic, TOPIC_PROFILE_CHANGE_TEARDOWN) == 0) {
|
||||
if (strcmp(aTopic, TOPIC_PROFILE_CHANGE_TEARDOWN) == 0 ||
|
||||
strcmp(aTopic, TOPIC_SIMULATE_PLACES_MUST_CLOSE_1) == 0) {
|
||||
// Tests simulating shutdown may cause multiple notifications.
|
||||
if (mShuttingDown) {
|
||||
if (IsShutdownStarted()) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -1668,79 +2023,25 @@ Database::Observe(nsISupports *aSubject,
|
|||
|
||||
// Notify all Places users that we are about to shutdown.
|
||||
(void)os->NotifyObservers(nullptr, TOPIC_PLACES_SHUTDOWN, nullptr);
|
||||
}
|
||||
|
||||
else if (strcmp(aTopic, TOPIC_PROFILE_BEFORE_CHANGE) == 0) {
|
||||
} else if (strcmp(aTopic, TOPIC_SIMULATE_PLACES_MUST_CLOSE_2) == 0) {
|
||||
// Tests simulating shutdown may cause re-entrance.
|
||||
if (mShuttingDown) {
|
||||
if (IsShutdownStarted()) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Fire internal shutdown notifications.
|
||||
nsCOMPtr<nsIObserverService> os = services::GetObserverService();
|
||||
if (os) {
|
||||
(void)os->NotifyObservers(nullptr, TOPIC_PLACES_WILL_CLOSE_CONNECTION, nullptr);
|
||||
// Since we are going through shutdown of Database,
|
||||
// we don't need to block actual shutdown anymore.
|
||||
nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetShutdownPhase();
|
||||
if (shutdownPhase) {
|
||||
shutdownPhase->RemoveBlocker(mConnectionShutdown.get());
|
||||
}
|
||||
|
||||
#ifdef DEBUG
|
||||
{ // Sanity check for missing guids.
|
||||
bool haveNullGuids = false;
|
||||
nsCOMPtr<mozIStorageStatement> stmt;
|
||||
|
||||
nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
|
||||
"SELECT 1 "
|
||||
"FROM moz_places "
|
||||
"WHERE guid IS NULL "
|
||||
), getter_AddRefs(stmt));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
rv = stmt->ExecuteStep(&haveNullGuids);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
MOZ_ASSERT(!haveNullGuids && "Found a page without a GUID!");
|
||||
|
||||
rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
|
||||
"SELECT 1 "
|
||||
"FROM moz_bookmarks "
|
||||
"WHERE guid IS NULL "
|
||||
), getter_AddRefs(stmt));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
rv = stmt->ExecuteStep(&haveNullGuids);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
MOZ_ASSERT(!haveNullGuids && "Found a bookmark without a GUID!");
|
||||
|
||||
rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
|
||||
"SELECT 1 "
|
||||
"FROM moz_favicons "
|
||||
"WHERE guid IS NULL "
|
||||
), getter_AddRefs(stmt));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
rv = stmt->ExecuteStep(&haveNullGuids);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
MOZ_ASSERT(!haveNullGuids && "Found a favicon without a GUID!");
|
||||
}
|
||||
|
||||
{ // Sanity check for unrounded dateAdded and lastModified values (bug
|
||||
// 1107308).
|
||||
bool hasUnroundedDates = false;
|
||||
nsCOMPtr<mozIStorageStatement> stmt;
|
||||
|
||||
nsresult rv = mMainConn->CreateStatement(NS_LITERAL_CSTRING(
|
||||
"SELECT 1 "
|
||||
"FROM moz_bookmarks "
|
||||
"WHERE dateAdded % 1000 > 0 OR lastModified % 1000 > 0 LIMIT 1"
|
||||
), getter_AddRefs(stmt));
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
rv = stmt->ExecuteStep(&hasUnroundedDates);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
MOZ_ASSERT(!hasUnroundedDates && "Found unrounded dates!");
|
||||
}
|
||||
#endif
|
||||
|
||||
// As the last step in the shutdown path, finalize the database handle.
|
||||
Shutdown();
|
||||
return mConnectionShutdown->BlockShutdown(nullptr);
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
||||
|
||||
} // namespace places
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include "nsWeakReference.h"
|
||||
#include "nsIInterfaceRequestorUtils.h"
|
||||
#include "nsIObserver.h"
|
||||
#include "nsIAsyncShutdown.h"
|
||||
#include "mozilla/storage.h"
|
||||
#include "mozilla/storage/StatementCache.h"
|
||||
#include "mozilla/Attributes.h"
|
||||
|
@ -26,11 +27,6 @@
|
|||
// initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
|
||||
// Any shutdown work that requires the Places APIs should happen here.
|
||||
#define TOPIC_PROFILE_CHANGE_TEARDOWN "profile-change-teardown"
|
||||
// This topic is received just before the profile is lost. Places begins
|
||||
// shutting down the connection and notifies TOPIC_PLACES_WILL_CLOSE_CONNECTION
|
||||
// to all listeners. Only critical database cleanups should happen here,
|
||||
// some APIs may bail out already.
|
||||
#define TOPIC_PROFILE_BEFORE_CHANGE "profile-before-change"
|
||||
// Fired when Places is shutting down. Any code should stop accessing Places
|
||||
// APIs after this notification. If you need to listen for Places shutdown
|
||||
// you should only use this notification, next ones are intended only for
|
||||
|
@ -43,6 +39,14 @@
|
|||
// Fired when the connection has gone, nothing will work from now on.
|
||||
#define TOPIC_PLACES_CONNECTION_CLOSED "places-connection-closed"
|
||||
|
||||
// Simulate profile-before-change. This topic may only be used by
|
||||
// calling `observe` directly on the database. Used for testing only.
|
||||
#define TOPIC_SIMULATE_PLACES_MUST_CLOSE_1 "test-simulate-places-shutdown-phase-1"
|
||||
|
||||
// Simulate profile-before-change. This topic may only be used by
|
||||
// calling `observe` directly on the database. Used for testing only.
|
||||
#define TOPIC_SIMULATE_PLACES_MUST_CLOSE_2 "test-simulate-places-shutdown-phase-2"
|
||||
|
||||
class nsIRunnable;
|
||||
|
||||
namespace mozilla {
|
||||
|
@ -60,6 +64,8 @@ enum JournalMode {
|
|||
, JOURNAL_WAL
|
||||
};
|
||||
|
||||
class DatabaseShutdown;
|
||||
|
||||
class Database final : public nsIObserver
|
||||
, public nsSupportsWeakReference
|
||||
{
|
||||
|
@ -79,10 +85,9 @@ public:
|
|||
nsresult Init();
|
||||
|
||||
/**
|
||||
* Finalizes the cached statements and closes the database connection.
|
||||
* A TOPIC_PLACES_CONNECTION_CLOSED notification is fired when done.
|
||||
* The AsyncShutdown client used by clients of this API to be informed of shutdown.
|
||||
*/
|
||||
void Shutdown();
|
||||
already_AddRefed<nsIAsyncShutdownClient> GetConnectionShutdown();
|
||||
|
||||
/**
|
||||
* Getter to use when instantiating the class.
|
||||
|
@ -161,17 +166,7 @@ public:
|
|||
* @note Always null check the result.
|
||||
* @note Always use a scoper to reset the statement.
|
||||
*/
|
||||
already_AddRefed<mozIStorageStatement>
|
||||
GetStatement(const nsACString& aQuery) const
|
||||
{
|
||||
if (mShuttingDown) {
|
||||
return nullptr;
|
||||
}
|
||||
if (NS_IsMainThread()) {
|
||||
return mMainThreadStatements.GetCachedStatement(aQuery);
|
||||
}
|
||||
return mAsyncThreadStatements.GetCachedStatement(aQuery);
|
||||
}
|
||||
already_AddRefed<mozIStorageStatement> GetStatement(const nsACString& aQuery) const;
|
||||
|
||||
/**
|
||||
* Gets a cached asynchronous statement.
|
||||
|
@ -199,17 +194,17 @@ public:
|
|||
* @note Always null check the result.
|
||||
* @note AsyncStatements are automatically reset on execution.
|
||||
*/
|
||||
already_AddRefed<mozIStorageAsyncStatement>
|
||||
GetAsyncStatement(const nsACString& aQuery) const
|
||||
{
|
||||
if (mShuttingDown) {
|
||||
return nullptr;
|
||||
}
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
return mMainThreadAsyncStatements.GetCachedStatement(aQuery);
|
||||
}
|
||||
already_AddRefed<mozIStorageAsyncStatement> GetAsyncStatement(const nsACString& aQuery) const;
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Finalizes the cached statements and closes the database connection.
|
||||
* A TOPIC_PLACES_CONNECTION_CLOSED notification is fired when done.
|
||||
*/
|
||||
void Shutdown();
|
||||
|
||||
bool IsShutdownStarted() const;
|
||||
|
||||
/**
|
||||
* Initializes the database file. If the database does not exist or is
|
||||
* corrupt, a new one is created. In case of corruption it also creates a
|
||||
|
@ -253,7 +248,7 @@ protected:
|
|||
|
||||
/**
|
||||
* Initializes triggers defined in nsPlacesTriggers.h
|
||||
*/
|
||||
*/
|
||||
nsresult InitTempTriggers();
|
||||
|
||||
/**
|
||||
|
@ -278,6 +273,8 @@ protected:
|
|||
|
||||
nsresult UpdateBookmarkRootTitles();
|
||||
|
||||
friend class DatabaseShutdown;
|
||||
|
||||
private:
|
||||
~Database();
|
||||
|
||||
|
@ -296,8 +293,22 @@ private:
|
|||
|
||||
int32_t mDBPageSize;
|
||||
uint16_t mDatabaseStatus;
|
||||
bool mShuttingDown;
|
||||
bool mClosed;
|
||||
|
||||
/**
|
||||
* Determine at which shutdown phase we need to start shutting down
|
||||
* the Database.
|
||||
*/
|
||||
already_AddRefed<nsIAsyncShutdownClient> GetShutdownPhase();
|
||||
|
||||
/**
|
||||
* A companion object in charge of shutting down the mozStorage
|
||||
* connection once all clients have disconnected.
|
||||
*
|
||||
* Cycles between `this` and `mConnectionShutdown` are broken
|
||||
* in `Shutdown()`.
|
||||
*/
|
||||
nsRefPtr<DatabaseShutdown> mConnectionShutdown;
|
||||
};
|
||||
|
||||
} // namespace places
|
||||
|
|
|
@ -90,54 +90,6 @@ Cu.importGlobalProperties(["URL"]);
|
|||
const NOTIFICATION_CHUNK_SIZE = 300;
|
||||
const ONRESULT_CHUNK_SIZE = 300;
|
||||
|
||||
/**
|
||||
* Private shutdown barrier blocked by ongoing operations.
|
||||
*/
|
||||
XPCOMUtils.defineLazyGetter(this, "operationsBarrier", () =>
|
||||
new AsyncShutdown.Barrier("History.jsm: wait until all connections are closed")
|
||||
);
|
||||
|
||||
/**
|
||||
* Shared connection
|
||||
*/
|
||||
XPCOMUtils.defineLazyGetter(this, "DBConnPromised", () =>
|
||||
Task.spawn(function*() {
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
try {
|
||||
Sqlite.shutdown.addBlocker(
|
||||
"Places History.jsm: Closing database wrapper",
|
||||
Task.async(function*() {
|
||||
yield operationsBarrier.wait();
|
||||
gIsClosed = true;
|
||||
yield db.close();
|
||||
}),
|
||||
() => ({
|
||||
fetchState: () => ({
|
||||
isClosed: gIsClosed,
|
||||
operations: operationsBarrier.state,
|
||||
})
|
||||
})
|
||||
);
|
||||
} catch (ex) {
|
||||
// It's too late to block shutdown of Sqlite, so close the connection
|
||||
// immediately.
|
||||
db.close();
|
||||
throw ex;
|
||||
}
|
||||
return db;
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* `true` once this module has been shutdown.
|
||||
*/
|
||||
let gIsClosed = false;
|
||||
function ensureModuleIsOpen() {
|
||||
if (gIsClosed) {
|
||||
throw new Error("History.jsm has been shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a bookmarks notification through the given observers.
|
||||
*
|
||||
|
@ -262,8 +214,6 @@ this.History = Object.freeze({
|
|||
* is an empty array.
|
||||
*/
|
||||
remove: function (pages, onResult = null) {
|
||||
ensureModuleIsOpen();
|
||||
|
||||
// Normalize and type-check arguments
|
||||
if (Array.isArray(pages)) {
|
||||
if (pages.length == 0) {
|
||||
|
@ -294,29 +244,8 @@ this.History = Object.freeze({
|
|||
throw new TypeError("Invalid function: " + onResult);
|
||||
}
|
||||
|
||||
return Task.spawn(function*() {
|
||||
let promise = remove(normalizedPages, onResult);
|
||||
|
||||
operationsBarrier.client.addBlocker(
|
||||
"History.remove",
|
||||
promise,
|
||||
{
|
||||
// In case of crash, we do not want to upload information on
|
||||
// which urls are being cleared, for privacy reasons. GUIDs
|
||||
// are safe wrt privacy, but useless.
|
||||
fetchState: () => ({
|
||||
guids: guids.length,
|
||||
urls: normalizedPages.urls.map(u => u.protocol),
|
||||
})
|
||||
});
|
||||
|
||||
try {
|
||||
return (yield promise);
|
||||
} finally {
|
||||
// Cleanup the barrier.
|
||||
operationsBarrier.client.removeBlocker(promise);
|
||||
}
|
||||
});
|
||||
return PlacesUtils.withConnectionWrapper("History.jsm: remove",
|
||||
db => remove(db, normalizedPages, onResult));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -349,8 +278,6 @@ this.History = Object.freeze({
|
|||
* particular if the `object` is empty.
|
||||
*/
|
||||
removeVisitsByFilter: function(filter, onResult = null) {
|
||||
ensureModuleIsOpen();
|
||||
|
||||
if (!filter || typeof filter != "object") {
|
||||
throw new TypeError("Expected a filter");
|
||||
}
|
||||
|
@ -374,21 +301,9 @@ this.History = Object.freeze({
|
|||
throw new TypeError("Invalid function: " + onResult);
|
||||
}
|
||||
|
||||
return Task.spawn(function*() {
|
||||
let promise = removeVisitsByFilter(filter, onResult);
|
||||
|
||||
operationsBarrier.client.addBlocker(
|
||||
"History.removeVisitsByFilter",
|
||||
promise
|
||||
);
|
||||
|
||||
try {
|
||||
return (yield promise);
|
||||
} finally {
|
||||
// Cleanup the barrier.
|
||||
operationsBarrier.client.removeBlocker(promise);
|
||||
}
|
||||
});
|
||||
return PlacesUtils.withConnectionWrapper("History.jsm: removeVisitsByFilter",
|
||||
db => removeVisitsByFilter(db, filter, onResult)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -418,19 +333,9 @@ this.History = Object.freeze({
|
|||
* A promise resolved once the operation is complete.
|
||||
*/
|
||||
clear() {
|
||||
ensureModuleIsOpen();
|
||||
|
||||
return Task.spawn(function* () {
|
||||
let promise = clear();
|
||||
operationsBarrier.client.addBlocker("History.clear", promise);
|
||||
|
||||
try {
|
||||
return (yield promise);
|
||||
} finally {
|
||||
// Cleanup the barrier.
|
||||
operationsBarrier.client.removeBlocker(promise);
|
||||
}
|
||||
});
|
||||
return PlacesUtils.withConnectionWrapper("History.jsm: clear",
|
||||
clear
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -557,9 +462,7 @@ let invalidateFrecencies = Task.async(function*(db, idList) {
|
|||
});
|
||||
|
||||
// Inner implementation of History.clear().
|
||||
let clear = Task.async(function* () {
|
||||
let db = yield DBConnPromised;
|
||||
|
||||
let clear = Task.async(function* (db) {
|
||||
// Remove all history.
|
||||
yield db.execute("DELETE FROM moz_historyvisits");
|
||||
|
||||
|
@ -708,9 +611,7 @@ let notifyOnResult = Task.async(function*(data, onResult) {
|
|||
});
|
||||
|
||||
// Inner implementation of History.removeVisitsByFilter.
|
||||
let removeVisitsByFilter = Task.async(function*(filter, onResult = null) {
|
||||
let db = yield DBConnPromised;
|
||||
|
||||
let removeVisitsByFilter = Task.async(function*(db, filter, onResult = null) {
|
||||
// 1. Determine visits that took place during the interval. Note
|
||||
// that the database uses microseconds, while JS uses milliseconds,
|
||||
// so we need to *1000 one way and /1000 the other way.
|
||||
|
@ -797,8 +698,7 @@ let removeVisitsByFilter = Task.async(function*(filter, onResult = null) {
|
|||
|
||||
|
||||
// Inner implementation of History.remove.
|
||||
let remove = Task.async(function*({guids, urls}, onResult = null) {
|
||||
let db = yield DBConnPromised;
|
||||
let remove = Task.async(function*(db, {guids, urls}, onResult = null) {
|
||||
// 1. Find out what needs to be removed
|
||||
let query =
|
||||
`SELECT id, url, guid, foreign_count, title, frecency FROM moz_places
|
||||
|
|
|
@ -49,6 +49,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Bookmarks",
|
|||
"resource://gre/modules/Bookmarks.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "History",
|
||||
"resource://gre/modules/History.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
|
||||
"resource://gre/modules/AsyncShutdown.jsm");
|
||||
|
||||
// The minimum amount of transactions before starting a batch. Usually we do
|
||||
// do incremental updates, a batch will cause views to completely
|
||||
|
@ -1174,6 +1176,207 @@ this.PlacesUtils = {
|
|||
return urls;
|
||||
},
|
||||
|
||||
/**
|
||||
* Serializes the given node (and all its descendents) as JSON
|
||||
* and writes the serialization to the given output stream.
|
||||
*
|
||||
* @param aNode
|
||||
* An nsINavHistoryResultNode
|
||||
* @param aStream
|
||||
* An nsIOutputStream. NOTE: it only uses the write(str, len)
|
||||
* method of nsIOutputStream. The caller is responsible for
|
||||
* closing the stream.
|
||||
*/
|
||||
_serializeNodeAsJSONToOutputStream: function (aNode, aStream) {
|
||||
function addGenericProperties(aPlacesNode, aJSNode) {
|
||||
aJSNode.title = aPlacesNode.title;
|
||||
aJSNode.id = aPlacesNode.itemId;
|
||||
let guid = aPlacesNode.bookmarkGuid;
|
||||
if (guid) {
|
||||
aJSNode.itemGuid = guid;
|
||||
var parent = aPlacesNode.parent;
|
||||
if (parent)
|
||||
aJSNode.parent = parent.itemId;
|
||||
|
||||
var dateAdded = aPlacesNode.dateAdded;
|
||||
if (dateAdded)
|
||||
aJSNode.dateAdded = dateAdded;
|
||||
var lastModified = aPlacesNode.lastModified;
|
||||
if (lastModified)
|
||||
aJSNode.lastModified = lastModified;
|
||||
|
||||
// XXX need a hasAnnos api
|
||||
var annos = [];
|
||||
try {
|
||||
annos = PlacesUtils.getAnnotationsForItem(aJSNode.id).filter(function(anno) {
|
||||
// XXX should whitelist this instead, w/ a pref for
|
||||
// backup/restore of non-whitelisted annos
|
||||
// XXX causes JSON encoding errors, so utf-8 encode
|
||||
//anno.value = unescape(encodeURIComponent(anno.value));
|
||||
if (anno.name == PlacesUtils.LMANNO_FEEDURI)
|
||||
aJSNode.livemark = 1;
|
||||
return true;
|
||||
});
|
||||
} catch(ex) {}
|
||||
if (annos.length != 0)
|
||||
aJSNode.annos = annos;
|
||||
}
|
||||
// XXXdietrich - store annos for non-bookmark items
|
||||
}
|
||||
|
||||
function addURIProperties(aPlacesNode, aJSNode) {
|
||||
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
|
||||
aJSNode.uri = aPlacesNode.uri;
|
||||
if (aJSNode.id && aJSNode.id != -1) {
|
||||
// harvest bookmark-specific properties
|
||||
var keyword = PlacesUtils.bookmarks.getKeywordForBookmark(aJSNode.id);
|
||||
if (keyword)
|
||||
aJSNode.keyword = keyword;
|
||||
}
|
||||
|
||||
if (aPlacesNode.tags)
|
||||
aJSNode.tags = aPlacesNode.tags;
|
||||
|
||||
// last character-set
|
||||
var uri = PlacesUtils._uri(aPlacesNode.uri);
|
||||
try {
|
||||
var lastCharset = PlacesUtils.annotations.getPageAnnotation(
|
||||
uri, PlacesUtils.CHARSET_ANNO);
|
||||
aJSNode.charset = lastCharset;
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function addSeparatorProperties(aPlacesNode, aJSNode) {
|
||||
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR;
|
||||
}
|
||||
|
||||
function addContainerProperties(aPlacesNode, aJSNode) {
|
||||
var concreteId = PlacesUtils.getConcreteItemId(aPlacesNode);
|
||||
if (concreteId != -1) {
|
||||
// This is a bookmark or a tag container.
|
||||
if (PlacesUtils.nodeIsQuery(aPlacesNode) ||
|
||||
concreteId != aPlacesNode.itemId) {
|
||||
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
|
||||
aJSNode.uri = aPlacesNode.uri;
|
||||
// folder shortcut
|
||||
aJSNode.concreteId = concreteId;
|
||||
}
|
||||
else { // Bookmark folder or a shortcut we should convert to folder.
|
||||
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER;
|
||||
|
||||
// Mark root folders.
|
||||
if (aJSNode.id == PlacesUtils.placesRootId)
|
||||
aJSNode.root = "placesRoot";
|
||||
else if (aJSNode.id == PlacesUtils.bookmarksMenuFolderId)
|
||||
aJSNode.root = "bookmarksMenuFolder";
|
||||
else if (aJSNode.id == PlacesUtils.tagsFolderId)
|
||||
aJSNode.root = "tagsFolder";
|
||||
else if (aJSNode.id == PlacesUtils.unfiledBookmarksFolderId)
|
||||
aJSNode.root = "unfiledBookmarksFolder";
|
||||
else if (aJSNode.id == PlacesUtils.toolbarFolderId)
|
||||
aJSNode.root = "toolbarFolder";
|
||||
}
|
||||
}
|
||||
else {
|
||||
// This is a grouped container query, generated on the fly.
|
||||
aJSNode.type = PlacesUtils.TYPE_X_MOZ_PLACE;
|
||||
aJSNode.uri = aPlacesNode.uri;
|
||||
}
|
||||
}
|
||||
|
||||
function appendConvertedComplexNode(aNode, aSourceNode, aArray) {
|
||||
var repr = {};
|
||||
|
||||
for (let [name, value] in Iterator(aNode))
|
||||
repr[name] = value;
|
||||
|
||||
// write child nodes
|
||||
var children = repr.children = [];
|
||||
if (!aNode.livemark) {
|
||||
asContainer(aSourceNode);
|
||||
var wasOpen = aSourceNode.containerOpen;
|
||||
if (!wasOpen)
|
||||
aSourceNode.containerOpen = true;
|
||||
var cc = aSourceNode.childCount;
|
||||
for (var i = 0; i < cc; ++i) {
|
||||
var childNode = aSourceNode.getChild(i);
|
||||
appendConvertedNode(aSourceNode.getChild(i), i, children);
|
||||
}
|
||||
if (!wasOpen)
|
||||
aSourceNode.containerOpen = false;
|
||||
}
|
||||
|
||||
aArray.push(repr);
|
||||
return true;
|
||||
}
|
||||
|
||||
function appendConvertedNode(bNode, aIndex, aArray) {
|
||||
var node = {};
|
||||
|
||||
// set index in order received
|
||||
// XXX handy shortcut, but are there cases where we don't want
|
||||
// to export using the sorting provided by the query?
|
||||
if (aIndex)
|
||||
node.index = aIndex;
|
||||
|
||||
addGenericProperties(bNode, node);
|
||||
|
||||
var parent = bNode.parent;
|
||||
var grandParent = parent ? parent.parent : null;
|
||||
if (grandParent)
|
||||
node.grandParentId = grandParent.itemId;
|
||||
|
||||
if (PlacesUtils.nodeIsURI(bNode)) {
|
||||
// Tag root accept only folder nodes
|
||||
if (parent && parent.itemId == PlacesUtils.tagsFolderId)
|
||||
return false;
|
||||
|
||||
// Check for url validity, since we can't halt while writing a backup.
|
||||
// This will throw if we try to serialize an invalid url and it does
|
||||
// not make sense saving a wrong or corrupt uri node.
|
||||
try {
|
||||
PlacesUtils._uri(bNode.uri);
|
||||
} catch (ex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
addURIProperties(bNode, node);
|
||||
}
|
||||
else if (PlacesUtils.nodeIsContainer(bNode)) {
|
||||
// Tag containers accept only uri nodes
|
||||
if (grandParent && grandParent.itemId == PlacesUtils.tagsFolderId)
|
||||
return false;
|
||||
|
||||
addContainerProperties(bNode, node);
|
||||
}
|
||||
else if (PlacesUtils.nodeIsSeparator(bNode)) {
|
||||
// Tag root accept only folder nodes
|
||||
// Tag containers accept only uri nodes
|
||||
if ((parent && parent.itemId == PlacesUtils.tagsFolderId) ||
|
||||
(grandParent && grandParent.itemId == PlacesUtils.tagsFolderId))
|
||||
return false;
|
||||
|
||||
addSeparatorProperties(bNode, node);
|
||||
}
|
||||
|
||||
if (!node.feedURI && node.type == PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER)
|
||||
return appendConvertedComplexNode(node, bNode, aArray);
|
||||
|
||||
aArray.push(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
// serialize to stream
|
||||
var array = [];
|
||||
if (appendConvertedNode(aNode, null, array)) {
|
||||
var json = JSON.stringify(array[0]);
|
||||
aStream.write(json, json.length);
|
||||
}
|
||||
else {
|
||||
throw Cr.NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the shared Sqlite.jsm readonly connection to the Places database.
|
||||
* This is intended to be used mostly internally, and by other Places modules.
|
||||
|
@ -1184,12 +1387,39 @@ this.PlacesUtils = {
|
|||
promiseDBConnection: () => gAsyncDBConnPromised,
|
||||
|
||||
/**
|
||||
* Perform a read/write operation on the Places database.
|
||||
*
|
||||
* Gets a Sqlite.jsm wrapped connection to the Places database.
|
||||
* This is intended to be used mostly internally, and by other Places modules.
|
||||
* Keep in mind the Places DB schema is by no means frozen or even stable.
|
||||
* Your custom queries can - and will - break overtime.
|
||||
*
|
||||
* As all operations on the Places database are asynchronous, if shutdown
|
||||
* is initiated while an operation is pending, this could cause dataloss.
|
||||
* Using `withConnectionWrapper` ensures that shutdown waits until all
|
||||
* operations are complete before proceeding.
|
||||
*
|
||||
* Example:
|
||||
* yield withConnectionWrapper("Bookmarks: Remove a bookmark", Task.async(function*(db) {
|
||||
* // Proceed with the db, asynchronously.
|
||||
* // Shutdown will not interrupt operations that take place here.
|
||||
* }));
|
||||
*
|
||||
* @param {string} name The name of the operation. Used for debugging, logging
|
||||
* and crash reporting.
|
||||
* @param {function(db)} task A function that takes as argument a Sqlite.jsm
|
||||
* connection and returns a Promise. Shutdown is guaranteed to not interrupt
|
||||
* execution of `task`.
|
||||
*/
|
||||
promiseWrappedConnection: () => gAsyncDBWrapperPromised,
|
||||
withConnectionWrapper: (name, task) => {
|
||||
if (!name) {
|
||||
throw new TypeError("Expecting a user-readable name");
|
||||
}
|
||||
return Task.spawn(function*() {
|
||||
let db = yield gAsyncDBWrapperPromised;
|
||||
return db.executeBeforeShutdown(name, task);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Given a uri returns list of itemIds associated to it.
|
||||
|
@ -1979,49 +2209,49 @@ let Keywords = {
|
|||
// This also checks href for validity
|
||||
url = new URL(url);
|
||||
|
||||
return Task.spawn(function* () {
|
||||
let cache = yield gKeywordsCachePromise;
|
||||
return PlacesUtils.withConnectionWrapper("Keywords.insert", Task.async(function*(db) {
|
||||
let cache = yield gKeywordsCachePromise;
|
||||
|
||||
// Trying to set the same keyword is a no-op.
|
||||
let oldEntry = cache.get(keyword);
|
||||
if (oldEntry && oldEntry.url.href == url.href &&
|
||||
oldEntry.postData == keywordEntry.postData) {
|
||||
return;
|
||||
}
|
||||
// Trying to set the same keyword is a no-op.
|
||||
let oldEntry = cache.get(keyword);
|
||||
if (oldEntry && oldEntry.url.href == url.href &&
|
||||
oldEntry.postData == keywordEntry.postData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A keyword can only be associated to a single page.
|
||||
// If another page is using the new keyword, we must update the keyword
|
||||
// entry.
|
||||
// Note we cannot use INSERT OR REPLACE cause it wouldn't invoke the delete
|
||||
// trigger.
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
if (oldEntry) {
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_keywords
|
||||
SET place_id = (SELECT id FROM moz_places WHERE url = :url),
|
||||
post_data = :post_data
|
||||
WHERE keyword = :keyword
|
||||
`, { url: url.href, keyword: keyword, post_data: postData });
|
||||
yield notifyKeywordChange(oldEntry.url.href, "");
|
||||
} else {
|
||||
// An entry for the given page could be missing, in such a case we need to
|
||||
// create it.
|
||||
yield db.executeCached(
|
||||
`INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid)
|
||||
VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
|
||||
`, { url: url.href, rev_host: PlacesUtils.getReversedHost(url),
|
||||
frecency: url.protocol == "place:" ? 0 : -1 });
|
||||
yield db.executeCached(
|
||||
`INSERT INTO moz_keywords (keyword, place_id, post_data)
|
||||
VALUES (:keyword, (SELECT id FROM moz_places WHERE url = :url), :post_data)
|
||||
`, { url: url.href, keyword: keyword, post_data: postData });
|
||||
}
|
||||
// A keyword can only be associated to a single page.
|
||||
// If another page is using the new keyword, we must update the keyword
|
||||
// entry.
|
||||
// Note we cannot use INSERT OR REPLACE cause it wouldn't invoke the delete
|
||||
// trigger.
|
||||
if (oldEntry) {
|
||||
yield db.executeCached(
|
||||
`UPDATE moz_keywords
|
||||
SET place_id = (SELECT id FROM moz_places WHERE url = :url),
|
||||
post_data = :post_data
|
||||
WHERE keyword = :keyword
|
||||
`, { url: url.href, keyword: keyword, post_data: postData });
|
||||
yield notifyKeywordChange(oldEntry.url.href, "");
|
||||
} else {
|
||||
// An entry for the given page could be missing, in such a case we need to
|
||||
// create it.
|
||||
yield db.executeCached(
|
||||
`INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid)
|
||||
VALUES (:url, :rev_host, 0, :frecency, GENERATE_GUID())
|
||||
`, { url: url.href, rev_host: PlacesUtils.getReversedHost(url),
|
||||
frecency: url.protocol == "place:" ? 0 : -1 });
|
||||
yield db.executeCached(
|
||||
`INSERT INTO moz_keywords (keyword, place_id, post_data)
|
||||
VALUES (:keyword, (SELECT id FROM moz_places WHERE url = :url), :post_data)
|
||||
`, { url: url.href, keyword: keyword, post_data: postData });
|
||||
}
|
||||
|
||||
cache.set(keyword, { keyword, url, postData });
|
||||
cache.set(keyword, { keyword, url, postData });
|
||||
|
||||
// In any case, notify about the new keyword.
|
||||
yield notifyKeywordChange(url.href, keyword);
|
||||
}.bind(this));
|
||||
// In any case, notify about the new keyword.
|
||||
yield notifyKeywordChange(url.href, keyword);
|
||||
}.bind(this))
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -2036,20 +2266,19 @@ let Keywords = {
|
|||
if (!keyword || typeof(keyword) != "string")
|
||||
throw new Error("Invalid keyword");
|
||||
keyword = keyword.trim().toLowerCase();
|
||||
return Task.spawn(function* () {
|
||||
return PlacesUtils.withConnectionWrapper("Keywords.remove", Task.async(function*(db) {
|
||||
let cache = yield gKeywordsCachePromise;
|
||||
if (!cache.has(keyword))
|
||||
return;
|
||||
let { url } = cache.get(keyword);
|
||||
cache.delete(keyword);
|
||||
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
yield db.execute(`DELETE FROM moz_keywords WHERE keyword = :keyword`,
|
||||
{ keyword });
|
||||
|
||||
// Notify bookmarks about the removal.
|
||||
yield notifyKeywordChange(url.href, "");
|
||||
}.bind(this));
|
||||
}.bind(this))) ;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -2057,94 +2286,96 @@ let Keywords = {
|
|||
// Once the old API will be gone, we can remove this and stop observing.
|
||||
let gIgnoreKeywordNotifications = false;
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gKeywordsCachePromise", Task.async(function* () {
|
||||
let cache = new Map();
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
let rows = yield db.execute(
|
||||
`SELECT keyword, url, post_data
|
||||
FROM moz_keywords k
|
||||
JOIN moz_places h ON h.id = k.place_id
|
||||
`);
|
||||
for (let row of rows) {
|
||||
let keyword = row.getResultByName("keyword");
|
||||
let entry = { keyword,
|
||||
url: new URL(row.getResultByName("url")),
|
||||
postData: row.getResultByName("post_data") };
|
||||
cache.set(keyword, entry);
|
||||
}
|
||||
XPCOMUtils.defineLazyGetter(this, "gKeywordsCachePromise", () =>
|
||||
PlacesUtils.withConnectionWrapper("PlacesUtils: gKeywordsCachePromise",
|
||||
Task.async(function*(db) {
|
||||
let cache = new Map();
|
||||
let rows = yield db.execute(
|
||||
`SELECT keyword, url, post_data
|
||||
FROM moz_keywords k
|
||||
JOIN moz_places h ON h.id = k.place_id
|
||||
`);
|
||||
for (let row of rows) {
|
||||
let keyword = row.getResultByName("keyword");
|
||||
let entry = { keyword,
|
||||
url: new URL(row.getResultByName("url")),
|
||||
postData: row.getResultByName("post_data") };
|
||||
cache.set(keyword, entry);
|
||||
}
|
||||
|
||||
// Helper to get a keyword from an href.
|
||||
function keywordsForHref(href) {
|
||||
let keywords = [];
|
||||
for (let [ key, val ] of cache) {
|
||||
if (val.url.href == href)
|
||||
keywords.push(key);
|
||||
}
|
||||
return keywords;
|
||||
}
|
||||
|
||||
// Start observing changes to bookmarks. For now we are going to keep that
|
||||
// relation for backwards compatibility reasons, but mostly because we are
|
||||
// lacking a UI to manage keywords directly.
|
||||
let observer = {
|
||||
QueryInterface: XPCOMUtils.generateQI(Ci.nsINavBookmarkObserver),
|
||||
onBeginUpdateBatch() {},
|
||||
onEndUpdateBatch() {},
|
||||
onItemAdded() {},
|
||||
onItemVisited() {},
|
||||
onItemMoved() {},
|
||||
|
||||
onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid) {
|
||||
if (itemType != PlacesUtils.bookmarks.TYPE_BOOKMARK)
|
||||
return;
|
||||
|
||||
let keywords = keywordsForHref(uri.spec);
|
||||
// This uri has no keywords associated, so there's nothing to do.
|
||||
if (keywords.length == 0)
|
||||
return;
|
||||
|
||||
Task.spawn(function* () {
|
||||
// If the uri is not bookmarked anymore, we can remove this keyword.
|
||||
let bookmark = yield PlacesUtils.bookmarks.fetch({ url: uri });
|
||||
if (!bookmark) {
|
||||
for (let keyword of keywords) {
|
||||
yield PlacesUtils.keywords.remove(keyword);
|
||||
}
|
||||
// Helper to get a keyword from an href.
|
||||
function keywordsForHref(href) {
|
||||
let keywords = [];
|
||||
for (let [ key, val ] of cache) {
|
||||
if (val.url.href == href)
|
||||
keywords.push(key);
|
||||
}
|
||||
}).catch(Cu.reportError);
|
||||
},
|
||||
return keywords;
|
||||
}
|
||||
|
||||
onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid) {
|
||||
if (gIgnoreKeywordNotifications ||
|
||||
prop != "keyword")
|
||||
return;
|
||||
// Start observing changes to bookmarks. For now we are going to keep that
|
||||
// relation for backwards compatibility reasons, but mostly because we are
|
||||
// lacking a UI to manage keywords directly.
|
||||
let observer = {
|
||||
QueryInterface: XPCOMUtils.generateQI(Ci.nsINavBookmarkObserver),
|
||||
onBeginUpdateBatch() {},
|
||||
onEndUpdateBatch() {},
|
||||
onItemAdded() {},
|
||||
onItemVisited() {},
|
||||
onItemMoved() {},
|
||||
|
||||
Task.spawn(function* () {
|
||||
let bookmark = yield PlacesUtils.bookmarks.fetch(guid);
|
||||
// By this time the bookmark could have gone, there's nothing we can do.
|
||||
if (!bookmark)
|
||||
return;
|
||||
onItemRemoved(id, parentId, index, itemType, uri, guid, parentGuid) {
|
||||
if (itemType != PlacesUtils.bookmarks.TYPE_BOOKMARK)
|
||||
return;
|
||||
|
||||
if (val.length == 0) {
|
||||
// We are removing a keyword.
|
||||
let keywords = keywordsForHref(bookmark.url.href)
|
||||
for (let keyword of keywords) {
|
||||
cache.delete(keyword);
|
||||
}
|
||||
} else {
|
||||
// We are adding a new keyword.
|
||||
cache.set(val, { keyword: val, url: bookmark.url });
|
||||
let keywords = keywordsForHref(uri.spec);
|
||||
// This uri has no keywords associated, so there's nothing to do.
|
||||
if (keywords.length == 0)
|
||||
return;
|
||||
|
||||
Task.spawn(function* () {
|
||||
// If the uri is not bookmarked anymore, we can remove this keyword.
|
||||
let bookmark = yield PlacesUtils.bookmarks.fetch({ url: uri });
|
||||
if (!bookmark) {
|
||||
for (let keyword of keywords) {
|
||||
yield PlacesUtils.keywords.remove(keyword);
|
||||
}
|
||||
}
|
||||
}).catch(Cu.reportError);
|
||||
},
|
||||
|
||||
onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid) {
|
||||
if (gIgnoreKeywordNotifications ||
|
||||
prop != "keyword")
|
||||
return;
|
||||
|
||||
Task.spawn(function* () {
|
||||
let bookmark = yield PlacesUtils.bookmarks.fetch(guid);
|
||||
// By this time the bookmark could have gone, there's nothing we can do.
|
||||
if (!bookmark)
|
||||
return;
|
||||
|
||||
if (val.length == 0) {
|
||||
// We are removing a keyword.
|
||||
let keywords = keywordsForHref(bookmark.url.href)
|
||||
for (let keyword of keywords) {
|
||||
cache.delete(keyword);
|
||||
}
|
||||
} else {
|
||||
// We are adding a new keyword.
|
||||
cache.set(val, { keyword: val, url: bookmark.url });
|
||||
}
|
||||
}).catch(Cu.reportError);
|
||||
}
|
||||
}).catch(Cu.reportError);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
PlacesUtils.bookmarks.addObserver(observer, false);
|
||||
PlacesUtils.registerShutdownFunction(() => {
|
||||
PlacesUtils.bookmarks.removeObserver(observer);
|
||||
});
|
||||
return cache;
|
||||
}));
|
||||
PlacesUtils.bookmarks.addObserver(observer, false);
|
||||
PlacesUtils.registerShutdownFunction(() => {
|
||||
PlacesUtils.bookmarks.removeObserver(observer);
|
||||
});
|
||||
return cache;
|
||||
})
|
||||
));
|
||||
|
||||
// Sometime soon, likely as part of the transition to mozIAsyncBookmarks,
|
||||
// itemIds will be deprecated in favour of GUIDs, which play much better
|
||||
|
|
|
@ -297,7 +297,7 @@ nsNavHistory::Init()
|
|||
***
|
||||
*** Nothing after these add observer calls should return anything but NS_OK.
|
||||
*** If a failure code is returned, this nsNavHistory object will be held onto
|
||||
*** by the observer service and the preference service.
|
||||
*** by the observer service and the preference service.
|
||||
****************************************************************************/
|
||||
|
||||
// Observe preferences changes.
|
||||
|
@ -774,7 +774,7 @@ nsNavHistory::GetUpdateRequirements(const nsCOMArray<nsNavHistoryQuery>& aQuerie
|
|||
nsINavHistoryQueryOptions::RESULTS_AS_TAG_QUERY)
|
||||
return QUERYUPDATE_COMPLEX_WITH_BOOKMARKS;
|
||||
|
||||
// Whenever there is a maximum number of results,
|
||||
// Whenever there is a maximum number of results,
|
||||
// and we are not a bookmark query we must requery. This
|
||||
// is because we can't generally know if any given addition/change causes
|
||||
// the item to be in the top N items in the database.
|
||||
|
@ -1276,7 +1276,7 @@ bool IsOptimizableHistoryQuery(const nsCOMArray<nsNavHistoryQuery>& aQueries,
|
|||
return false;
|
||||
|
||||
nsNavHistoryQuery *aQuery = aQueries[0];
|
||||
|
||||
|
||||
if (aOptions->QueryType() != nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY)
|
||||
return false;
|
||||
|
||||
|
@ -1298,25 +1298,25 @@ bool IsOptimizableHistoryQuery(const nsCOMArray<nsNavHistoryQuery>& aQueries,
|
|||
if (aQuery->MinVisits() != -1 || aQuery->MaxVisits() != -1)
|
||||
return false;
|
||||
|
||||
if (aQuery->BeginTime() || aQuery->BeginTimeReference())
|
||||
if (aQuery->BeginTime() || aQuery->BeginTimeReference())
|
||||
return false;
|
||||
|
||||
if (aQuery->EndTime() || aQuery->EndTimeReference())
|
||||
if (aQuery->EndTime() || aQuery->EndTimeReference())
|
||||
return false;
|
||||
|
||||
if (!aQuery->SearchTerms().IsEmpty())
|
||||
if (!aQuery->SearchTerms().IsEmpty())
|
||||
return false;
|
||||
|
||||
if (aQuery->OnlyBookmarked())
|
||||
if (aQuery->OnlyBookmarked())
|
||||
return false;
|
||||
|
||||
if (aQuery->DomainIsHost() || !aQuery->Domain().IsEmpty())
|
||||
return false;
|
||||
|
||||
if (aQuery->AnnotationIsNot() || !aQuery->Annotation().IsEmpty())
|
||||
if (aQuery->AnnotationIsNot() || !aQuery->Annotation().IsEmpty())
|
||||
return false;
|
||||
|
||||
if (aQuery->UriIsPrefix() || aQuery->Uri())
|
||||
if (aQuery->UriIsPrefix() || aQuery->Uri())
|
||||
return false;
|
||||
|
||||
if (aQuery->Folders().Length() > 0)
|
||||
|
@ -1332,7 +1332,7 @@ bool IsOptimizableHistoryQuery(const nsCOMArray<nsNavHistoryQuery>& aQueries,
|
|||
}
|
||||
|
||||
static
|
||||
bool NeedToFilterResultSet(const nsCOMArray<nsNavHistoryQuery>& aQueries,
|
||||
bool NeedToFilterResultSet(const nsCOMArray<nsNavHistoryQuery>& aQueries,
|
||||
nsNavHistoryQueryOptions *aOptions)
|
||||
{
|
||||
uint16_t resultType = aOptions->ResultType();
|
||||
|
@ -1390,8 +1390,8 @@ private:
|
|||
};
|
||||
|
||||
PlacesSQLQueryBuilder::PlacesSQLQueryBuilder(
|
||||
const nsCString& aConditions,
|
||||
nsNavHistoryQueryOptions* aOptions,
|
||||
const nsCString& aConditions,
|
||||
nsNavHistoryQueryOptions* aOptions,
|
||||
bool aUseLimit,
|
||||
nsNavHistory::StringHash& aAddParams,
|
||||
bool aHasSearchTerms)
|
||||
|
@ -1855,7 +1855,7 @@ PlacesSQLQueryBuilder::SelectAsTag()
|
|||
|
||||
// This allows sorting by date fields what is not possible with
|
||||
// other history queries.
|
||||
mHasDateColumns = true;
|
||||
mHasDateColumns = true;
|
||||
|
||||
mQueryString = nsPrintfCString(
|
||||
"SELECT null, 'place:folder=' || id || '&queryType=%d&type=%ld', "
|
||||
|
@ -1905,7 +1905,7 @@ PlacesSQLQueryBuilder::Where()
|
|||
mQueryString.ReplaceSubstring("{QUERY_OPTIONS_PLACES}",
|
||||
additionalPlacesConditions.get());
|
||||
|
||||
// If we used WHERE already, we inject the conditions
|
||||
// If we used WHERE already, we inject the conditions
|
||||
// in place of {ADDITIONAL_CONDITIONS}
|
||||
if (mQueryString.Find("{ADDITIONAL_CONDITIONS}", 0) != kNotFound) {
|
||||
nsAutoCString innerCondition;
|
||||
|
@ -2056,15 +2056,15 @@ PlacesSQLQueryBuilder::Limit()
|
|||
nsresult
|
||||
nsNavHistory::ConstructQueryString(
|
||||
const nsCOMArray<nsNavHistoryQuery>& aQueries,
|
||||
nsNavHistoryQueryOptions* aOptions,
|
||||
nsCString& queryString,
|
||||
nsNavHistoryQueryOptions* aOptions,
|
||||
nsCString& queryString,
|
||||
bool& aParamsPresent,
|
||||
nsNavHistory::StringHash& aAddParams)
|
||||
{
|
||||
// For information about visit_type see nsINavHistoryService.idl.
|
||||
// visitType == 0 is undefined (see bug #375777 for details).
|
||||
// Some sites, especially Javascript-heavy ones, load things in frames to
|
||||
// display them, resulting in a lot of these entries. This is the reason
|
||||
// Some sites, especially Javascript-heavy ones, load things in frames to
|
||||
// display them, resulting in a lot of these entries. This is the reason
|
||||
// why such visits are filtered out.
|
||||
nsresult rv;
|
||||
aParamsPresent = false;
|
||||
|
@ -2151,7 +2151,7 @@ nsNavHistory::ConstructQueryString(
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
PLDHashOperator BindAdditionalParameter(nsNavHistory::StringHash::KeyType aParamName,
|
||||
PLDHashOperator BindAdditionalParameter(nsNavHistory::StringHash::KeyType aParamName,
|
||||
nsCString aParamValue,
|
||||
void* aStatement)
|
||||
{
|
||||
|
@ -2233,7 +2233,7 @@ nsNavHistory::GetQueryResults(nsNavHistoryQueryResultNode *aResultNode,
|
|||
} else {
|
||||
rv = ResultsAsList(statement, aOptions, aResults);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
@ -2980,6 +2980,17 @@ nsNavHistory::GetDBConnection(mozIStorageConnection **_DBConnection)
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsNavHistory::GetShutdownClient(nsIAsyncShutdownClient **_shutdownClient)
|
||||
{
|
||||
NS_ENSURE_ARG_POINTER(_shutdownClient);
|
||||
nsRefPtr<nsIAsyncShutdownClient> client = mDB->GetConnectionShutdown();
|
||||
MOZ_ASSERT(client);
|
||||
client.forget(_shutdownClient);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsNavHistory::AsyncExecuteLegacyQueries(nsINavHistoryQuery** aQueries,
|
||||
uint32_t aQueryCount,
|
||||
|
@ -3077,9 +3088,10 @@ nsNavHistory::Observe(nsISupports *aSubject, const char *aTopic,
|
|||
const char16_t *aData)
|
||||
{
|
||||
NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
|
||||
|
||||
if (strcmp(aTopic, TOPIC_PROFILE_TEARDOWN) == 0 ||
|
||||
strcmp(aTopic, TOPIC_PROFILE_CHANGE) == 0) {
|
||||
strcmp(aTopic, TOPIC_PROFILE_CHANGE) == 0 ||
|
||||
strcmp(aTopic, TOPIC_SIMULATE_PLACES_MUST_CLOSE_1) == 0 ||
|
||||
strcmp(aTopic, TOPIC_SIMULATE_PLACES_MUST_CLOSE_2) == 0) {
|
||||
// These notifications are used by tests to simulate a Places shutdown.
|
||||
// They should just be forwarded to the Database handle.
|
||||
mDB->Observe(aSubject, aTopic, aData);
|
||||
|
@ -3219,10 +3231,10 @@ nsNavHistory::DecayFrecency()
|
|||
|
||||
// Helper class for QueryToSelectClause
|
||||
//
|
||||
// This class helps to build part of the WHERE clause. It supports
|
||||
// multiple queries by appending the query index to the parameter name.
|
||||
// This class helps to build part of the WHERE clause. It supports
|
||||
// multiple queries by appending the query index to the parameter name.
|
||||
// For the query with index 0 the parameter name is not altered what
|
||||
// allows using this parameter in other situations (see SelectAsSite).
|
||||
// allows using this parameter in other situations (see SelectAsSite).
|
||||
|
||||
class ConditionBuilder
|
||||
{
|
||||
|
@ -3259,7 +3271,7 @@ public:
|
|||
return *this;
|
||||
}
|
||||
|
||||
void GetClauseString(nsCString& aResult)
|
||||
void GetClauseString(nsCString& aResult)
|
||||
{
|
||||
aResult = mClause;
|
||||
}
|
||||
|
@ -3294,7 +3306,7 @@ nsNavHistory::QueryToSelectClause(nsNavHistoryQuery* aQuery, // const
|
|||
clause.Condition("EXISTS (SELECT 1 FROM moz_historyvisits "
|
||||
"WHERE place_id = h.id");
|
||||
// begin time
|
||||
if (NS_SUCCEEDED(aQuery->GetHasBeginTime(&hasIt)) && hasIt)
|
||||
if (NS_SUCCEEDED(aQuery->GetHasBeginTime(&hasIt)) && hasIt)
|
||||
clause.Condition("visit_date >=").Param(":begin_time");
|
||||
// end time
|
||||
if (NS_SUCCEEDED(aQuery->GetHasEndTime(&hasIt)) && hasIt)
|
||||
|
@ -3325,7 +3337,7 @@ nsNavHistory::QueryToSelectClause(nsNavHistoryQuery* aQuery, // const
|
|||
|
||||
if (aQuery->MaxVisits() >= 0)
|
||||
clause.Condition("h.visit_count <=").Param(":max_visits");
|
||||
|
||||
|
||||
// only bookmarked, has no affect on bookmarks-only queries
|
||||
if (aOptions->QueryType() != nsINavHistoryQueryOptions::QUERY_TYPE_BOOKMARKS &&
|
||||
aQuery->OnlyBookmarked())
|
||||
|
@ -3629,7 +3641,7 @@ const int64_t UNDEFINED_URN_VALUE = -1;
|
|||
// urn:places-persist:place:group=0&group=1&sort=1&type=1,,%28local%20files%29)
|
||||
// to be used to persist the open state of this container
|
||||
nsresult
|
||||
CreatePlacesPersistURN(nsNavHistoryQueryResultNode *aResultNode,
|
||||
CreatePlacesPersistURN(nsNavHistoryQueryResultNode *aResultNode,
|
||||
int64_t aValue, const nsCString& aTitle, nsCString& aURN)
|
||||
{
|
||||
nsAutoCString uri;
|
||||
|
@ -3658,12 +3670,12 @@ int64_t
|
|||
nsNavHistory::GetTagsFolder()
|
||||
{
|
||||
// cache our tags folder
|
||||
// note, we can't do this in nsNavHistory::Init(),
|
||||
// note, we can't do this in nsNavHistory::Init(),
|
||||
// as getting the bookmarks service would initialize it.
|
||||
if (mTagsFolder == -1) {
|
||||
nsNavBookmarks *bookmarks = nsNavBookmarks::GetBookmarksService();
|
||||
NS_ENSURE_TRUE(bookmarks, -1);
|
||||
|
||||
|
||||
nsresult rv = bookmarks->GetTagsFolder(&mTagsFolder);
|
||||
NS_ENSURE_SUCCESS(rv, -1);
|
||||
}
|
||||
|
@ -3676,7 +3688,7 @@ nsNavHistory::GetTagsFolder()
|
|||
// - searching on title, url and tags
|
||||
// - limit count
|
||||
//
|
||||
// Note: changes to filtering in FilterResultSet()
|
||||
// Note: changes to filtering in FilterResultSet()
|
||||
// may require changes to NeedToFilterResultSet()
|
||||
|
||||
nsresult
|
||||
|
@ -4033,7 +4045,7 @@ nsNavHistory::QueryRowToResult(int64_t itemId,
|
|||
resultNode->mBookmarkGuid = aBookmarkGuid;
|
||||
resultNode->GetAsFolder()->mTargetFolderGuid = targetFolderGuid;
|
||||
|
||||
// Use the query item title, unless it's void (in that case use the
|
||||
// Use the query item title, unless it's void (in that case use the
|
||||
// concrete folder title).
|
||||
if (!aTitle.IsVoid()) {
|
||||
resultNode->mTitle = aTitle;
|
||||
|
|
|
@ -11,13 +11,14 @@ interface nsINavHistoryQuery;
|
|||
interface nsINavHistoryQueryOptions;
|
||||
interface mozIStorageStatementCallback;
|
||||
interface mozIStoragePendingStatement;
|
||||
interface nsIAsyncShutdownClient;
|
||||
|
||||
/**
|
||||
* This is a private interface used by Places components to get access to the
|
||||
* database. If outside consumers wish to use this, they should only read from
|
||||
* the database so they do not break any internal invariants.
|
||||
*/
|
||||
[scriptable, uuid(6eb7ed3d-13ca-450b-b370-15c75e2f3dab)]
|
||||
[scriptable, uuid(366ee63e-a413-477d-9ad6-8d6863e89401)]
|
||||
interface nsPIPlacesDatabase : nsISupports
|
||||
{
|
||||
/**
|
||||
|
@ -42,4 +43,10 @@ interface nsPIPlacesDatabase : nsISupports
|
|||
in unsigned long aQueryCount,
|
||||
in nsINavHistoryQueryOptions aOptions,
|
||||
in mozIStorageStatementCallback aCallback);
|
||||
|
||||
/**
|
||||
* Hook for clients who need to perform actions during/by the end of
|
||||
* the shutdown of the database.
|
||||
*/
|
||||
readonly attribute nsIAsyncShutdownClient shutdownClient;
|
||||
};
|
||||
|
|
|
@ -8,6 +8,7 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
|||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
|
||||
"resource://gre/modules/PlacesUtils.jsm");
|
||||
|
@ -32,8 +33,8 @@ this.PlacesTestUtils = Object.freeze({
|
|||
* @resolves When all visits have been added successfully.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
addVisits(placeInfo) {
|
||||
return new Promise((resolve, reject) => {
|
||||
addVisits: Task.async(function*(placeInfo) {
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
let places = [];
|
||||
if (placeInfo instanceof Ci.nsIURI) {
|
||||
places.push({ uri: placeInfo });
|
||||
|
@ -73,7 +74,8 @@ this.PlacesTestUtils = Object.freeze({
|
|||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
return (yield promise);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Clear all history.
|
||||
|
|
|
@ -367,12 +367,20 @@ function promiseTopicObserved(aTopic)
|
|||
/**
|
||||
* Simulates a Places shutdown.
|
||||
*/
|
||||
function shutdownPlaces(aKeepAliveConnection)
|
||||
{
|
||||
let shutdownPlaces = function() {
|
||||
do_print("shutdownPlaces: starting");
|
||||
let promise = new Promise(resolve => {
|
||||
Services.obs.addObserver(resolve, "places-connection-closed", false);
|
||||
});
|
||||
let hs = PlacesUtils.history.QueryInterface(Ci.nsIObserver);
|
||||
hs.observe(null, "profile-change-teardown", null);
|
||||
hs.observe(null, "profile-before-change", null);
|
||||
}
|
||||
hs.observe(null, "test-simulate-places-shutdown-phase-1", null);
|
||||
do_print("shutdownPlaces: sent test-simulate-places-shutdown-phase-1");
|
||||
hs.observe(null, "test-simulate-places-shutdown-phase-2", null);
|
||||
do_print("shutdownPlaces: sent test-simulate-places-shutdown-phase-2");
|
||||
return promise.then(() => {
|
||||
do_print("shutdownPlaces: complete");
|
||||
});
|
||||
};
|
||||
|
||||
const FILENAME_BOOKMARKS_HTML = "bookmarks.html";
|
||||
const FILENAME_BOOKMARKS_JSON = "bookmarks-" +
|
||||
|
|
|
@ -221,7 +221,7 @@ add_task(function* test_remove_many() {
|
|||
Assert.ok(origin);
|
||||
Assert.ok(origin.hasBookmark, "Observing onFrecencyChanged on a page with a bookmark");
|
||||
origin.onFrecencyChangedCalled = true;
|
||||
// We do not make sure that `origin.onFrecencyChangedCalled` is `false`, as
|
||||
// We do not make sure that `origin.onFrecencyChangedCalled` is `false`, as
|
||||
},
|
||||
onManyFrecenciesChanged: function() {
|
||||
Assert.ok(false, "Observing onManyFrecenciesChanges, this is most likely correct but not covered by this test");
|
||||
|
|
|
@ -10,9 +10,10 @@ add_task(function* () {
|
|||
PlacesUtils.invalidateCachedGuidFor(9999);
|
||||
|
||||
do_print("Change the GUID.");
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
yield db.execute("UPDATE moz_bookmarks SET guid = :guid WHERE id = :id",
|
||||
{ guid: "123456789012", id});
|
||||
yield PlacesUtils.withConnectionWrapper("test", Task.async(function*(db) {
|
||||
yield db.execute("UPDATE moz_bookmarks SET guid = :guid WHERE id = :id",
|
||||
{ guid: "123456789012", id});
|
||||
}));
|
||||
// The cache should still point to the wrong id.
|
||||
Assert.equal((yield PlacesUtils.promiseItemGuid(id)), bm.guid);
|
||||
|
||||
|
|
|
@ -31,9 +31,10 @@ add_task(function* test_corrupt_database() {
|
|||
let corruptBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
|
||||
url: "http://test.mozilla.org",
|
||||
title: "We love belugas" });
|
||||
let db = yield PlacesUtils.promiseWrappedConnection();
|
||||
yield db.execute("UPDATE moz_bookmarks SET fk = NULL WHERE guid = :guid",
|
||||
{ guid: corruptBookmark.guid });
|
||||
let db = yield PlacesUtils.withConnectionWrapper("test", Task.async(function*(db) {
|
||||
yield db.execute("UPDATE moz_bookmarks SET fk = NULL WHERE guid = :guid",
|
||||
{ guid: corruptBookmark.guid });
|
||||
}));
|
||||
|
||||
let bookmarksFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.html");
|
||||
if ((yield OS.File.exists(bookmarksFile)))
|
||||
|
|
|
@ -51,11 +51,9 @@ function run_test() {
|
|||
dbConn.executeSimpleSQL("PRAGMA USER_VERSION = 1");
|
||||
|
||||
// Try to create history service while the db is locked
|
||||
try {
|
||||
var hs1 = Cc["@mozilla.org/browser/nav-history-service;1"].
|
||||
getService(Ci.nsINavHistoryService);
|
||||
do_throw("Creating an instance of history service on a locked db should throw");
|
||||
} catch (ex) {}
|
||||
Assert.throws(() => Cc["@mozilla.org/browser/nav-history-service;1"].
|
||||
getService(Ci.nsINavHistoryService),
|
||||
/NS_ERROR_XPC_GS_RETURNED_FAILURE/);
|
||||
|
||||
// Close our connection and try to cleanup the file (could fail on Windows)
|
||||
dbConn.close();
|
||||
|
@ -65,6 +63,11 @@ function run_test() {
|
|||
} catch(e) { dump("Unable to remove dummy places.sqlite"); }
|
||||
}
|
||||
|
||||
// Make sure that the incorrectly opened service is closed before
|
||||
// we make another attempt. Otherwise, there will be a conflict between
|
||||
// the two services (and an assertion failure).
|
||||
yield shutdownPlaces();
|
||||
|
||||
// Create history service correctly
|
||||
try {
|
||||
var hs2 = Cc["@mozilla.org/browser/nav-history-service;1"].
|
||||
|
|
|
@ -442,7 +442,7 @@ add_task(function* test_getLivemark_removeItem_contention() {
|
|||
PlacesUtils.livemarks.addLivemark({ title: "test"
|
||||
, parentGuid: PlacesUtils.bookmarks.unfiledGuid
|
||||
, feedURI: FEED_URI
|
||||
});
|
||||
}).catch(() => {/* swallow errors*/});
|
||||
yield PlacesUtils.bookmarks.eraseEverything();
|
||||
let livemark = yield PlacesUtils.livemarks.addLivemark(
|
||||
{ title: "test"
|
||||
|
|
|
@ -18,52 +18,62 @@ XPCOMUtils.defineLazyGetter(this, "Sanitizer", function () {
|
|||
* be removed when the user sanitizes their history.
|
||||
*/
|
||||
function runTests() {
|
||||
dontExpireThumbnailURLs([URL, URL_COPY]);
|
||||
yield Task.spawn(function*() {
|
||||
dontExpireThumbnailURLs([URL, URL_COPY]);
|
||||
|
||||
yield clearHistory();
|
||||
yield addVisitsAndRepopulateNewTabLinks(URL, next);
|
||||
yield createThumbnail();
|
||||
yield promiseClearHistory();
|
||||
yield promiseAddVisitsAndRepopulateNewTabLinks(URL);
|
||||
yield promiseCreateThumbnail();
|
||||
|
||||
// Make sure Storage.copy() updates an existing file.
|
||||
yield PageThumbsStorage.copy(URL, URL_COPY);
|
||||
let copy = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY));
|
||||
let mtime = copy.lastModifiedTime -= 60;
|
||||
// Make sure Storage.copy() updates an existing file.
|
||||
yield PageThumbsStorage.copy(URL, URL_COPY);
|
||||
let copy = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY));
|
||||
let mtime = copy.lastModifiedTime -= 60;
|
||||
|
||||
yield PageThumbsStorage.copy(URL, URL_COPY);
|
||||
isnot(new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY)).lastModifiedTime, mtime,
|
||||
"thumbnail file was updated");
|
||||
yield PageThumbsStorage.copy(URL, URL_COPY);
|
||||
isnot(new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY)).lastModifiedTime, mtime,
|
||||
"thumbnail file was updated");
|
||||
|
||||
let file = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL));
|
||||
let fileCopy = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY));
|
||||
let file = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL));
|
||||
let fileCopy = new FileUtils.File(PageThumbsStorage.getFilePathForURL(URL_COPY));
|
||||
|
||||
// Clear the browser history. Retry until the files are gone because Windows
|
||||
// locks them sometimes.
|
||||
while (file.exists() || fileCopy.exists()) {
|
||||
yield clearHistory();
|
||||
}
|
||||
// Clear the browser history. Retry until the files are gone because Windows
|
||||
// locks them sometimes.
|
||||
info("Clearing history");
|
||||
while (file.exists() || fileCopy.exists()) {
|
||||
yield promiseClearHistory();
|
||||
}
|
||||
info("History is clear");
|
||||
|
||||
yield addVisitsAndRepopulateNewTabLinks(URL, next);
|
||||
yield createThumbnail();
|
||||
info("Repopulating");
|
||||
yield promiseAddVisitsAndRepopulateNewTabLinks(URL);
|
||||
yield promiseCreateThumbnail();
|
||||
|
||||
// Clear the last 10 minutes of browsing history.
|
||||
yield clearHistory(true);
|
||||
info("Clearing the last 10 minutes of browsing history");
|
||||
// Clear the last 10 minutes of browsing history.
|
||||
yield promiseClearHistory(true);
|
||||
|
||||
// Retry until the file is gone because Windows locks it sometimes.
|
||||
clearFile(file, URL);
|
||||
info("Attempt to clear file");
|
||||
// Retry until the file is gone because Windows locks it sometimes.
|
||||
yield promiseClearFile(file, URL);
|
||||
|
||||
info("Done");
|
||||
});
|
||||
}
|
||||
|
||||
function clearFile(aFile, aURL) {
|
||||
if (aFile.exists()) {
|
||||
// Re-add our URL to the history so that history observer's onDeleteURI()
|
||||
// is called again.
|
||||
PlacesTestUtils.addVisits(makeURI(aURL)).then(() => {
|
||||
// Try again...
|
||||
clearHistory(true, () => clearFile(aFile, aURL));
|
||||
});
|
||||
let promiseClearFile = Task.async(function*(aFile, aURL) {
|
||||
if (!aFile.exists()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Re-add our URL to the history so that history observer's onDeleteURI()
|
||||
// is called again.
|
||||
yield PlacesTestUtils.addVisits(makeURI(aURL));
|
||||
yield promiseClearHistory(true);
|
||||
// Then retry.
|
||||
return promiseClearFile(aFile, aURL);
|
||||
});
|
||||
|
||||
function clearHistory(aUseRange, aCallback = next) {
|
||||
function promiseClearHistory(aUseRange) {
|
||||
let s = new Sanitizer();
|
||||
s.prefDomain = "privacy.cpd.";
|
||||
|
||||
|
@ -84,18 +94,19 @@ function clearHistory(aUseRange, aCallback = next) {
|
|||
s.ignoreTimespan = false;
|
||||
}
|
||||
|
||||
s.sanitize();
|
||||
s.range = null;
|
||||
s.ignoreTimespan = true;
|
||||
|
||||
executeSoon(aCallback);
|
||||
return s.sanitize().then(() => {
|
||||
s.range = null;
|
||||
s.ignoreTimespan = true;
|
||||
});
|
||||
}
|
||||
|
||||
function createThumbnail() {
|
||||
addTab(URL, function () {
|
||||
whenFileExists(URL, function () {
|
||||
gBrowser.removeTab(gBrowser.selectedTab);
|
||||
next();
|
||||
function promiseCreateThumbnail() {
|
||||
return new Promise(resolve => {
|
||||
addTab(URL, function () {
|
||||
whenFileExists(URL, function () {
|
||||
gBrowser.removeTab(gBrowser.selectedTab);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -210,6 +210,9 @@ function addVisitsAndRepopulateNewTabLinks(aPlaceInfo, aCallback) {
|
|||
NewTabUtils.links.populateCache(aCallback, true);
|
||||
});
|
||||
}
|
||||
function promiseAddVisitsAndRepopulateNewTabLinks(aPlaceInfo) {
|
||||
return new Promise(resolve => addVisitsAndRepopulateNewTabLinks(aPlaceInfo, resolve));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls a given callback when the thumbnail for a given URL has been found
|
||||
|
|
Загрузка…
Ссылка в новой задаче