Bug 1043863 - Use AsyncShutdown to shutdown Places. r=mak

--HG--
extra : rebase_source : 3a593651ac1fc995e01d00af037aaac8b81c7c32
This commit is contained in:
David Rajchenbach-Teller 2015-05-05 12:44:16 +02:00
Родитель 945cfe86e7
Коммит 38a8ed9399
17 изменённых файлов: 1428 добавлений и 947 удалений

Просмотреть файл

@ -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