"use strict"; ChromeUtils.defineESModuleGetters(this, { Preferences: "resource://gre/modules/Preferences.sys.mjs", }); async function check_keyword(aExpectExists, aHref, aKeyword, aPostData = null) { // Check case-insensitivity. aKeyword = aKeyword.toUpperCase(); let entry = await PlacesUtils.keywords.fetch(aKeyword); Assert.deepEqual( entry, await PlacesUtils.keywords.fetch({ keyword: aKeyword }) ); if (aExpectExists) { Assert.ok(!!entry, "A keyword should exist"); Assert.equal(entry.url.href, aHref); Assert.equal(entry.postData, aPostData); Assert.deepEqual( entry, await PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref }) ); let entries = []; await PlacesUtils.keywords.fetch({ url: aHref }, e => entries.push(e)); Assert.ok( entries.some( e => e.url.href == aHref && e.keyword == aKeyword.toLowerCase() ) ); } else { Assert.ok( !entry || entry.url.href != aHref, "The given keyword entry should not exist" ); if (aHref) { Assert.equal( null, await PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref }) ); } else { Assert.equal( null, await PlacesUtils.keywords.fetch({ keyword: aKeyword }) ); } } } /** * Polls the keywords cache waiting for the given keyword entry. */ async function promiseKeyword(keyword, expectedHref) { let href = null; do { await new Promise(resolve => do_timeout(100, resolve)); let entry = await PlacesUtils.keywords.fetch(keyword); if (entry) { href = entry.url.href; } } while (href != expectedHref); } async function check_no_orphans() { let db = await PlacesUtils.promiseDBConnection(); let rows = await db.executeCached( `SELECT id FROM moz_keywords k WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id) ` ); Assert.equal(rows.length, 0); } function expectBookmarkNotifications() { const observer = { notifications: [], _start() { this._handle = this._handle.bind(this); PlacesUtils.observers.addListener( ["bookmark-keyword-changed"], this._handle ); }, _handle(events) { for (const event of events) { this.notifications.push({ type: event.type, id: event.id, itemType: event.itemType, url: event.url, guid: event.guid, parentGuid: event.parentGuid, keyword: event.keyword, lastModified: new Date(event.lastModified), source: event.source, isTagging: event.isTagging, }); } }, check(expected) { PlacesUtils.observers.removeListener( ["bookmark-keyword-changed"], this._handle ); Assert.deepEqual(this.notifications, expected); }, }; observer._start(); return observer; } add_task(async function test_invalid_input() { Assert.throws(() => PlacesUtils.keywords.fetch(null), /Invalid keyword/); Assert.throws(() => PlacesUtils.keywords.fetch(5), /Invalid keyword/); Assert.throws(() => PlacesUtils.keywords.fetch(undefined), /Invalid keyword/); Assert.throws( () => PlacesUtils.keywords.fetch({ keyword: null }), /Invalid keyword/ ); Assert.throws( () => PlacesUtils.keywords.fetch({ keyword: {} }), /Invalid keyword/ ); Assert.throws( () => PlacesUtils.keywords.fetch({ keyword: 5 }), /Invalid keyword/ ); Assert.throws( () => PlacesUtils.keywords.fetch({}), /At least keyword or url must be provided/ ); Assert.throws( () => PlacesUtils.keywords.fetch({ keyword: "test" }, "test"), /onResult callback must be a valid function/ ); Assert.throws( () => PlacesUtils.keywords.fetch({ url: "test" }), /is not a valid URL/ ); Assert.throws( () => PlacesUtils.keywords.fetch({ url: {} }), /is not a valid URL/ ); Assert.throws( () => PlacesUtils.keywords.fetch({ url: null }), /is not a valid URL/ ); Assert.throws( () => PlacesUtils.keywords.fetch({ url: "" }), /is not a valid URL/ ); Assert.throws( () => PlacesUtils.keywords.insert(null), /Input should be a valid object/ ); Assert.throws( () => PlacesUtils.keywords.insert("test"), /Input should be a valid object/ ); Assert.throws( () => PlacesUtils.keywords.insert(undefined), /Input should be a valid object/ ); Assert.throws(() => PlacesUtils.keywords.insert({}), /Invalid keyword/); Assert.throws( () => PlacesUtils.keywords.insert({ keyword: null }), /Invalid keyword/ ); Assert.throws( () => PlacesUtils.keywords.insert({ keyword: 5 }), /Invalid keyword/ ); Assert.throws( () => PlacesUtils.keywords.insert({ keyword: "" }), /Invalid keyword/ ); Assert.throws( () => PlacesUtils.keywords.insert({ keyword: "test", postData: 5 }), /Invalid POST data/ ); Assert.throws( () => PlacesUtils.keywords.insert({ keyword: "test", postData: {} }), /Invalid POST data/ ); Assert.throws( () => PlacesUtils.keywords.insert({ keyword: "test" }), /is not a valid URL/ ); Assert.throws( () => PlacesUtils.keywords.insert({ keyword: "test", url: 5 }), /is not a valid URL/ ); Assert.throws( () => PlacesUtils.keywords.insert({ keyword: "test", url: "" }), /is not a valid URL/ ); Assert.throws( () => PlacesUtils.keywords.insert({ keyword: "test", url: null }), /is not a valid URL/ ); Assert.throws( () => PlacesUtils.keywords.insert({ keyword: "test", url: "mozilla" }), /is not a valid URL/ ); Assert.throws(() => PlacesUtils.keywords.remove(null), /Invalid keyword/); Assert.throws(() => PlacesUtils.keywords.remove(""), /Invalid keyword/); Assert.throws(() => PlacesUtils.keywords.remove(5), /Invalid keyword/); }); add_task(async function test_addKeyword() { await check_keyword(false, "http://example.com/", "keyword"); let fc = await foreign_count("http://example.com/"); let observer = expectBookmarkNotifications(); await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", }); observer.check([]); await check_keyword(true, "http://example.com/", "keyword"); Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 keyword // Now remove the keyword. observer = expectBookmarkNotifications(); await PlacesUtils.keywords.remove("keyword"); observer.check([]); await check_keyword(false, "http://example.com/", "keyword"); Assert.equal(await foreign_count("http://example.com/"), fc); // -1 keyword // Check using URL. await PlacesUtils.keywords.insert({ keyword: "keyword", url: new URL("http://example.com/"), }); await check_keyword(true, "http://example.com/", "keyword"); await PlacesUtils.keywords.remove("keyword"); await check_keyword(false, "http://example.com/", "keyword"); await check_no_orphans(); }); add_task(async function test_addBookmarkAndKeyword() { let timerPrecision = Preferences.get("privacy.reduceTimerPrecision"); Preferences.set("privacy.reduceTimerPrecision", false); registerCleanupFunction(function () { Preferences.set("privacy.reduceTimerPrecision", timerPrecision); }); await check_keyword(false, "http://example.com/", "keyword"); let fc = await foreign_count("http://example.com/"); let bookmark = await PlacesUtils.bookmarks.insert({ url: "http://example.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }); let observer = expectBookmarkNotifications(); await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", }); observer.check([ { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark.guid), itemType: bookmark.type, url: bookmark.url, guid: bookmark.guid, parentGuid: bookmark.parentGuid, keyword: "keyword", lastModified: new Date(bookmark.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, ]); await check_keyword(true, "http://example.com/", "keyword"); Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark +1 keyword // Now remove the keyword. observer = expectBookmarkNotifications(); await PlacesUtils.keywords.remove("keyword"); observer.check([ { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark.guid), itemType: bookmark.type, url: bookmark.url, guid: bookmark.guid, parentGuid: bookmark.parentGuid, keyword: "", lastModified: new Date(bookmark.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, ]); await check_keyword(false, "http://example.com/", "keyword"); Assert.equal(await foreign_count("http://example.com/"), fc + 1); // -1 keyword // Add again the keyword, then remove the bookmark. await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", }); observer = expectBookmarkNotifications(); await PlacesUtils.bookmarks.remove(bookmark.guid); // the notification is synchronous but the removal process is async. // Unfortunately there's nothing explicit we can wait for. // eslint-disable-next-line no-empty while (await foreign_count("http://example.com/")) {} // We don't get any itemChanged notification since the bookmark has been // removed already. observer.check([]); await check_keyword(false, "http://example.com/", "keyword"); await check_no_orphans(); }); add_task(async function test_addKeywordToURIHavingKeyword() { await check_keyword(false, "http://example.com/", "keyword"); let fc = await foreign_count("http://example.com/"); let observer = expectBookmarkNotifications(); await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", }); observer.check([]); await check_keyword(true, "http://example.com/", "keyword"); Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 keyword await PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/", postData: "test=1", }); await check_keyword(true, "http://example.com/", "keyword"); await check_keyword(true, "http://example.com/", "keyword2", "test=1"); Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 keyword let entries = []; let entry = await PlacesUtils.keywords.fetch( { url: "http://example.com/" }, e => entries.push(e) ); Assert.equal(entries.length, 2); Assert.deepEqual(entries[0], entry); // Now remove the keywords. observer = expectBookmarkNotifications(); await PlacesUtils.keywords.remove("keyword"); await PlacesUtils.keywords.remove("keyword2"); observer.check([]); await check_keyword(false, "http://example.com/", "keyword"); await check_keyword(false, "http://example.com/", "keyword2"); Assert.equal(await foreign_count("http://example.com/"), fc); // -1 keyword await check_no_orphans(); }); add_task(async function test_addBookmarkToURIHavingKeyword() { await check_keyword(false, "http://example.com/", "keyword"); let fc = await foreign_count("http://example.com/"); let observer = expectBookmarkNotifications(); await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", }); observer.check([]); await check_keyword(true, "http://example.com/", "keyword"); Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 keyword observer = expectBookmarkNotifications(); let bookmark = await PlacesUtils.bookmarks.insert({ url: "http://example.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }); Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark observer.check([]); observer = expectBookmarkNotifications(); await PlacesUtils.bookmarks.remove(bookmark.guid); // the notification is synchronous but the removal process is async. // Unfortunately there's nothing explicit we can wait for. // eslint-disable-next-line no-empty while (await foreign_count("http://example.com/")) {} // We don't get any itemChanged notification since the bookmark has been // removed already. observer.check([]); await check_keyword(false, "http://example.com/", "keyword"); await check_no_orphans(); }); add_task(async function test_sameKeywordDifferentURL() { let fc1 = await foreign_count("http://example1.com/"); let bookmark1 = await PlacesUtils.bookmarks.insert({ url: "http://example1.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }); let fc2 = await foreign_count("http://example2.com/"); let bookmark2 = await PlacesUtils.bookmarks.insert({ url: "http://example2.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }); await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example1.com/", }); await check_keyword(true, "http://example1.com/", "keyword"); Assert.equal(await foreign_count("http://example1.com/"), fc1 + 2); // +1 bookmark +1 keyword await check_keyword(false, "http://example2.com/", "keyword"); Assert.equal(await foreign_count("http://example2.com/"), fc2 + 1); // +1 bookmark // Assign the same keyword to another url. let observer = expectBookmarkNotifications(); await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example2.com/", }); observer.check([ { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark1.guid), itemType: bookmark1.type, url: bookmark1.url, guid: bookmark1.guid, parentGuid: bookmark1.parentGuid, keyword: "", lastModified: new Date(bookmark1.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark2.guid), itemType: bookmark2.type, url: bookmark2.url, guid: bookmark2.guid, parentGuid: bookmark2.parentGuid, keyword: "keyword", lastModified: new Date(bookmark2.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, ]); await check_keyword(false, "http://example1.com/", "keyword"); Assert.equal(await foreign_count("http://example1.com/"), fc1 + 1); // -1 keyword await check_keyword(true, "http://example2.com/", "keyword"); Assert.equal(await foreign_count("http://example2.com/"), fc2 + 2); // +1 keyword // Now remove the keyword. observer = expectBookmarkNotifications(); await PlacesUtils.keywords.remove("keyword"); observer.check([ { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark2.guid), itemType: bookmark2.type, url: bookmark2.url, guid: bookmark2.guid, parentGuid: bookmark2.parentGuid, keyword: "", lastModified: new Date(bookmark2.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, ]); await check_keyword(false, "http://example1.com/", "keyword"); await check_keyword(false, "http://example2.com/", "keyword"); Assert.equal(await foreign_count("http://example1.com/"), fc1 + 1); Assert.equal(await foreign_count("http://example2.com/"), fc2 + 1); // -1 keyword await PlacesUtils.bookmarks.remove(bookmark1); await PlacesUtils.bookmarks.remove(bookmark2); Assert.equal(await foreign_count("http://example1.com/"), fc1); // -1 bookmark // eslint-disable-next-line no-empty while (await foreign_count("http://example2.com/")) {} // -1 keyword await check_no_orphans(); }); add_task(async function test_sameURIDifferentKeyword() { let fc = await foreign_count("http://example.com/"); let observer = expectBookmarkNotifications(); let bookmark = await PlacesUtils.bookmarks.insert({ url: "http://example.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }); await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", }); await check_keyword(true, "http://example.com/", "keyword"); Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark +1 keyword observer.check([ { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark.guid), itemType: bookmark.type, url: bookmark.url, guid: bookmark.guid, parentGuid: bookmark.parentGuid, keyword: "keyword", lastModified: new Date(bookmark.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, ]); observer = expectBookmarkNotifications(); await PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/", }); await check_keyword(false, "http://example.com/", "keyword"); await check_keyword(true, "http://example.com/", "keyword2"); Assert.equal(await foreign_count("http://example.com/"), fc + 2); // -1 keyword +1 keyword observer.check([ { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark.guid), itemType: bookmark.type, url: bookmark.url, guid: bookmark.guid, parentGuid: bookmark.parentGuid, keyword: "keyword2", lastModified: new Date(bookmark.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, ]); // Now remove the bookmark. await PlacesUtils.bookmarks.remove(bookmark); // eslint-disable-next-line no-empty while (await foreign_count("http://example.com/")) {} await check_keyword(false, "http://example.com/", "keyword"); await check_keyword(false, "http://example.com/", "keyword2"); await check_no_orphans(); }); add_task(async function test_deleteKeywordMultipleBookmarks() { let fc = await foreign_count("http://example.com/"); let observer = expectBookmarkNotifications(); let bookmark1 = await PlacesUtils.bookmarks.insert({ url: "http://example.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }); let bookmark2 = await PlacesUtils.bookmarks.insert({ url: "http://example.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }); await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", }); await check_keyword(true, "http://example.com/", "keyword"); Assert.equal(await foreign_count("http://example.com/"), fc + 3); // +2 bookmark +1 keyword observer.check([ { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark2.guid), itemType: bookmark2.type, url: bookmark2.url, guid: bookmark2.guid, parentGuid: bookmark2.parentGuid, keyword: "keyword", lastModified: new Date(bookmark2.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark1.guid), itemType: bookmark1.type, url: bookmark1.url, guid: bookmark1.guid, parentGuid: bookmark1.parentGuid, keyword: "keyword", lastModified: new Date(bookmark1.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, ]); observer = expectBookmarkNotifications(); await PlacesUtils.keywords.remove("keyword"); await check_keyword(false, "http://example.com/", "keyword"); Assert.equal(await foreign_count("http://example.com/"), fc + 2); // -1 keyword observer.check([ { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark2.guid), itemType: bookmark2.type, url: bookmark2.url, guid: bookmark2.guid, parentGuid: bookmark2.parentGuid, keyword: "", lastModified: new Date(bookmark2.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, { type: "bookmark-keyword-changed", id: await PlacesUtils.promiseItemId(bookmark1.guid), itemType: bookmark1.type, url: bookmark1.url, guid: bookmark1.guid, parentGuid: bookmark1.parentGuid, keyword: "", lastModified: new Date(bookmark1.lastModified), source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, isTagging: false, }, ]); // Now remove the bookmarks. await PlacesUtils.bookmarks.remove(bookmark1); await PlacesUtils.bookmarks.remove(bookmark2); Assert.equal(await foreign_count("http://example.com/"), fc); // -2 bookmarks await check_no_orphans(); }); add_task(async function test_multipleKeywordsSamePostData() { await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", postData: "postData1", }); await check_keyword(true, "http://example.com/", "keyword", "postData1"); // Add another keyword with same postData, should fail. await PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/", postData: "postData1", }); await check_keyword(false, "http://example.com/", "keyword", "postData1"); await check_keyword(true, "http://example.com/", "keyword2", "postData1"); await PlacesUtils.keywords.remove("keyword2"); await check_no_orphans(); }); add_task(async function test_bookmarkURLChange() { let fc1 = await foreign_count("http://example1.com/"); let fc2 = await foreign_count("http://example2.com/"); let bookmark = await PlacesUtils.bookmarks.insert({ url: "http://example1.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }); await PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example1.com/", }); await check_keyword(true, "http://example1.com/", "keyword"); Assert.equal(await foreign_count("http://example1.com/"), fc1 + 2); // +1 bookmark +1 keyword await PlacesUtils.bookmarks.update({ guid: bookmark.guid, url: "http://example2.com/", }); await promiseKeyword("keyword", "http://example2.com/"); await check_keyword(false, "http://example1.com/", "keyword"); await check_keyword(true, "http://example2.com/", "keyword"); Assert.equal(await foreign_count("http://example1.com/"), fc1); // -1 bookmark -1 keyword Assert.equal(await foreign_count("http://example2.com/"), fc2 + 2); // +1 bookmark +1 keyword }); add_task(async function test_tagDoesntPreventKeywordRemoval() { await check_keyword(false, "http://example.com/", "example"); let fc = await foreign_count("http://example.com/"); let httpBookmark = await PlacesUtils.bookmarks.insert({ url: "http://example.com/", parentGuid: PlacesUtils.bookmarks.unfiledGuid, }); Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 bookmark PlacesUtils.tagging.tagURI(uri("http://example.com/"), ["example_tag"]); Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark +1 tag await PlacesUtils.keywords.insert({ keyword: "example", url: "http://example.com/", }); Assert.equal(await foreign_count("http://example.com/"), fc + 3); // +1 bookmark +1 tag +1 keyword await check_keyword(true, "http://example.com/", "example"); await PlacesUtils.bookmarks.remove(httpBookmark); await TestUtils.waitForCondition( async () => !(await PlacesUtils.bookmarks.fetch({ url: "http://example.com/" })), "Wait for bookmark to be removed" ); await check_keyword(false, "http://example.com/", "example"); Assert.equal(await foreign_count("http://example.com/"), fc); // bookmark, keyword, and tag should all have been removed await check_no_orphans(); });