Bug 1477996 - Move getURIsForTag to the bookmarking API. r=Standard8,lina

The tagging API is being merged into the bookmarking API. This is part of it.

Differential Revision: https://phabricator.services.mozilla.com/D2450

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Marco Bonardo 2018-07-27 15:16:57 +00:00
parent 3bda10d3f6
commit 23cf556fe1
12 changed files with 143 additions and 89 deletions

View File

@ -810,8 +810,9 @@ PlacesController.prototype = {
// child of the "Tags" query in the library, in all other places we
// must only remove the query node.
let tag = node.title;
let URIs = PlacesUtils.tagging.getURIsForTag(tag);
transactions.push(PlacesTransactions.Untag({ tag, urls: URIs }));
let urls = new Set();
await PlacesUtils.bookmarks.fetch({tags: [tag]}, b => urls.add(b.url));
transactions.push(PlacesTransactions.Untag({ tag, urls: Array.from(urls) }));
} else if (PlacesUtils.nodeIsURI(node) &&
PlacesUtils.nodeIsQuery(node.parent) &&
PlacesUtils.asQuery(node.parent).queryOptions.queryType ==

View File

@ -87,7 +87,7 @@ add_task(async function test_tags() {
}
// The tag should now not exist.
Assert.deepEqual(PlacesUtils.tagging.getURIsForTag("test"), [],
Assert.equal(await PlacesUtils.bookmarks.fetch({tags: ["test"]}), null,
"There should be no URIs remaining for the tag");
tagsNode.containerOpen = false;

View File

@ -421,9 +421,10 @@ add_task(async function test_bookmark_change_during_sync() {
"Folder 1 should have 3 children after first sync");
await assertChildGuids(folder2_guid, [bmk4_guid, tagQuery_guid],
"Folder 2 should have 2 children after first sync");
let taggedURIs = PlacesUtils.tagging.getURIsForTag("taggy");
let taggedURIs = [];
await PlacesUtils.bookmarks.fetch({tags: ["taggy"]}, b => taggedURIs.push(b.url));
equal(taggedURIs.length, 1, "Should have 1 tagged URI");
equal(taggedURIs[0].spec, "https://example.org/",
equal(taggedURIs[0].href, "https://example.org/",
"Synced tagged bookmark should appear in tagged URI list");
changes = await PlacesSyncUtils.bookmarks.pullChanges();

View File

@ -780,15 +780,12 @@ var Bookmarks = Object.freeze({
// If we're updating a tag, we must notify all the tagged bookmarks
// about the change.
if (isTagging) {
let URIs = PlacesUtils.tagging.getURIsForTag(updatedItem.title);
for (let uri of URIs) {
for (let entry of (await fetchBookmarksByURL({ url: new URL(uri.spec) }, true))) {
notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
PlacesUtils.toPRTime(entry.lastModified),
entry.type, entry._parentId,
entry.guid, entry.parentGuid,
"", updatedItem.source ]);
}
for (let entry of (await fetchBookmarksByTags({ tags: [updatedItem.title] }, true))) {
notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
PlacesUtils.toPRTime(entry.lastModified),
entry.type, entry._parentId,
entry.guid, entry.parentGuid,
"", updatedItem.source ]);
}
}
}
@ -1200,6 +1197,13 @@ var Bookmarks = Object.freeze({
* retrieves the most recent item with the specified guid prefix.
* To retrieve ALL of the bookmarks for that guid prefix, you must pass
* in an onResult callback, that will be invoked once for each bookmark.
* - tags
* Retrieves the most recent item with all the specified tags.
* The tags are matched in a case-insensitive way.
* To retrieve ALL of the bookmarks having these tags, pass in an
* onResult callback, that will be invoked once for each bookmark.
* Note, there can be multiple bookmarks for the same url, if you need
* unique tagged urls you can filter duplicates by accumulating in a Set.
*
* @param guidOrInfo
* The globally unique identifier of the item to fetch, or an
@ -1235,15 +1239,18 @@ var Bookmarks = Object.freeze({
info = { guid: guidOrInfo };
} else if (Object.keys(info).length == 1) {
// Just a faster code path.
if (!["url", "guid", "parentGuid", "index", "guidPrefix"].includes(Object.keys(info)[0]))
if (!["url", "guid", "parentGuid",
"index", "guidPrefix", "tags"].includes(Object.keys(info)[0])) {
throw new Error(`Unexpected number of conditions provided: 0`);
}
} else {
// Only one condition at a time can be provided.
let conditionsCount = [
v => v.hasOwnProperty("guid"),
v => v.hasOwnProperty("parentGuid") && v.hasOwnProperty("index"),
v => v.hasOwnProperty("url"),
v => v.hasOwnProperty("guidPrefix")
v => v.hasOwnProperty("guidPrefix"),
v => v.hasOwnProperty("tags")
].reduce((old, fn) => old + fn(info) | 0, 0);
if (conditionsCount != 1)
throw new Error(`Unexpected number of conditions provided: ${conditionsCount}`);
@ -1274,6 +1281,9 @@ var Bookmarks = Object.freeze({
results = await fetchBookmarkByPosition(fetchInfo, options && options.concurrent);
else if (fetchInfo.hasOwnProperty("guidPrefix"))
results = await fetchBookmarksByGUIDPrefix(fetchInfo, options && options.concurrent);
else if (fetchInfo.hasOwnProperty("tags")) {
results = await fetchBookmarksByTags(fetchInfo, options && options.concurrent);
}
if (!results)
return null;
@ -2054,6 +2064,42 @@ async function fetchBookmarkByPosition(info, concurrent) {
query);
}
async function fetchBookmarksByTags(info, concurrent) {
let query = async function(db) {
let rows = await db.executeCached(
`SELECT b.guid, IFNULL(p.guid, "") AS parentGuid, b.position AS 'index',
b.dateAdded, b.lastModified, b.type, IFNULL(b.title, "") AS title,
h.url AS url, b.id AS _id, b.parent AS _parentId,
NULL AS _childCount,
p.parent AS _grandParentId, b.syncStatus AS _syncStatus
FROM moz_bookmarks b
JOIN moz_bookmarks p ON p.id = b.parent
JOIN moz_bookmarks g ON g.id = p.parent
JOIN moz_places h ON h.id = b.fk
WHERE g.guid <> ? AND b.fk IN (
SELECT b2.fk FROM moz_bookmarks b2
JOIN moz_bookmarks p2 ON p2.id = b2.parent
JOIN moz_bookmarks g2 ON g2.id = p2.parent
WHERE g2.guid = ?
AND lower(p2.title) IN (
${new Array(info.tags.length).fill("?").join(",")}
)
GROUP BY b2.fk HAVING count(*) = ${info.tags.length}
)
ORDER BY b.lastModified DESC
`, [Bookmarks.tagsGuid, Bookmarks.tagsGuid].concat(info.tags.map(t => t.toLowerCase())));
return rows.length ? rowsToItemsArray(rows) : null;
};
if (concurrent) {
let db = await PlacesUtils.promiseDBConnection();
return query(db);
}
return PlacesUtils.withConnectionWrapper("Bookmarks.jsm: fetchBookmarksByTags",
query);
}
async function fetchBookmarksByGUIDPrefix(info, concurrent) {
let query = async function(db) {
let rows = await db.executeCached(

View File

@ -1597,8 +1597,10 @@ PT.RenameTag.prototype = {
// For now this is implemented by untagging and tagging all the bookmarks.
// We should create a specialized bookmarking API to just rename the tag.
let onUndo = [], onRedo = [];
let urls = PlacesUtils.tagging.getURIsForTag(oldTag);
if (urls.length > 0) {
let urls = new Set();
await PlacesUtils.bookmarks.fetch({tags: [oldTag]}, b => urls.add(b.url));
if (urls.size > 0) {
urls = Array.from(urls);
let tagTxn = TransactionsHistory.getRawTransaction(
PT.Tag({ urls, tags: [tag] })
);

View File

@ -252,7 +252,8 @@ const BOOKMARK_VALIDATORS = Object.freeze({
keyword: simpleValidateFunc(v => (typeof(v) == "string") && v.length),
charset: simpleValidateFunc(v => (typeof(v) == "string") && v.length),
postData: simpleValidateFunc(v => (typeof(v) == "string") && v.length),
tags: simpleValidateFunc(v => Array.isArray(v) && v.length),
tags: simpleValidateFunc(v => Array.isArray(v) && v.length &&
v.every(item => item && typeof item == "string")),
});
// Sync bookmark records can contain additional properties.

View File

@ -48,15 +48,6 @@ interface nsITaggingService : nsISupports
in nsIVariant aTags,
[optional] in unsigned short aSource);
/**
* Retrieves all URLs tagged with the given tag.
*
* @param aTag
* tag name
* @returns Array of uris tagged with aTag.
*/
nsIVariant getURIsForTag(in AString aTag);
/**
* Retrieves all tags set for the given URL.
*

View File

@ -229,42 +229,6 @@ TaggingService.prototype = {
}
},
// nsITaggingService
getURIsForTag: function TS_getURIsForTag(aTagName) {
if (!aTagName || aTagName.length == 0) {
throw Components.Exception("Invalid tag name", Cr.NS_ERROR_INVALID_ARG);
}
if (/^\s|\s$/.test(aTagName)) {
throw Components.Exception("Tag passed to getURIsForTag was not trimmed",
Cr.NS_ERROR_INVALID_ARG);
}
let uris = [];
let tagId = this._getItemIdForTag(aTagName);
if (tagId == -1)
return uris;
let db = PlacesUtils.history.DBConnection;
let stmt = db.createStatement(
`SELECT h.url FROM moz_places h
JOIN moz_bookmarks b ON b.fk = h.id
WHERE b.parent = :tag_id`
);
stmt.params.tag_id = tagId;
try {
while (stmt.executeStep()) {
try {
uris.push(Services.io.newURI(stmt.row.url));
} catch (ex) {}
}
} finally {
stmt.finalize();
}
return uris;
},
// nsITaggingService
getTagsForURI: function TS_getTagsForURI(aURI, aCount) {
if (!aURI) {

View File

@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
add_task(async function() {
add_task(async function test_fetchTags() {
let tags = await PlacesUtils.bookmarks.fetchTags();
Assert.deepEqual(tags, []);
@ -34,3 +34,55 @@ add_task(async function() {
{ name: "3", count: 1 },
]);
});
add_task(async function test_fetch_by_tags() {
Assert.throws(() => PlacesUtils.bookmarks.fetch({ tags: "" }),
/Invalid value for property 'tags'/);
Assert.throws(() => PlacesUtils.bookmarks.fetch({ tags: [] }),
/Invalid value for property 'tags'/);
Assert.throws(() => PlacesUtils.bookmarks.fetch({ tags: null }),
/Invalid value for property 'tags'/);
Assert.throws(() => PlacesUtils.bookmarks.fetch({ tags: [""] }),
/Invalid value for property 'tags'/);
Assert.throws(() => PlacesUtils.bookmarks.fetch({ tags: ["valid", null] }),
/Invalid value for property 'tags'/);
info("Add bookmarks with tags.");
let bm1 = await PlacesUtils.bookmarks.insert({
url: "http://bacon.org/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
PlacesUtils.tagging.tagURI(Services.io.newURI(bm1.url.href), ["egg", "ratafià"]);
let bm2 = await PlacesUtils.bookmarks.insert({
url: "http://mushroom.org/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
PlacesUtils.tagging.tagURI(Services.io.newURI(bm2.url.href), ["egg"]);
info("Fetch a single tag.");
let bms = [];
Assert.equal((await PlacesUtils.bookmarks.fetch({tags: ["egg"]}, b => bms.push(b))).guid,
bm2.guid, "Found the expected recent bookmark");
Assert.deepEqual(bms.map(b => b.guid), [bm2.guid, bm1.guid],
"Found the expected bookmarks");
info("Fetch multiple tags.");
bms = [];
Assert.equal((await PlacesUtils.bookmarks.fetch({tags: ["egg", "ratafià"]}, b => bms.push(b))).guid,
bm1.guid, "Found the expected recent bookmark");
Assert.deepEqual(bms.map(b => b.guid), [bm1.guid],
"Found the expected bookmarks");
info("Fetch a nonexisting tag.");
bms = [];
Assert.equal(await PlacesUtils.bookmarks.fetch({tags: ["egg", "tomato"]}, b => bms.push(b)),
null, "Should not find any bookmark");
Assert.deepEqual(bms, [], "Should not find any bookmark");
info("Check case insensitive");
bms = [];
Assert.equal((await PlacesUtils.bookmarks.fetch({tags: ["eGg", "raTafiÀ"]}, b => bms.push(b))).guid,
bm1.guid, "Found the expected recent bookmark");
Assert.deepEqual(bms.map(b => b.guid), [bm1.guid],
"Found the expected bookmarks");
});

View File

@ -961,12 +961,12 @@ add_task(async function test_rewrite_tag_queries() {
deepEqual(changesToUpload, {}, "Should not reupload any local records");
let urisWithTaggy = PlacesUtils.tagging.getURIsForTag("taggy");
deepEqual(urisWithTaggy.map(uri => uri.spec).sort(), ["http://example.com/e"],
let bmWithTaggy = await PlacesUtils.bookmarks.fetch({tags: ["taggy"]});
equal(bmWithTaggy.url.href, "http://example.com/e",
"Should insert bookmark with new tag");
let urisWithKitty = PlacesUtils.tagging.getURIsForTag("kitty");
deepEqual(urisWithKitty.map(uri => uri.spec).sort(), ["http://example.com/d"],
let bmWithKitty = await PlacesUtils.bookmarks.fetch({tags: ["kitty"]});
equal(bmWithKitty.url.href, "http://example.com/d",
"Should retain existing tag");
let { root: toolbarContainer } = PlacesUtils.getFolderContents(

View File

@ -37,9 +37,11 @@ function shuffle(array) {
return results;
}
function assertTagForURLs(tag, urls, message) {
let taggedURLs = PlacesUtils.tagging.getURIsForTag(tag).map(uri => uri.spec);
deepEqual(taggedURLs.sort(compareAscending), urls.sort(compareAscending), message);
async function assertTagForURLs(tag, urls, message) {
let taggedURLs = new Set();
await PlacesUtils.bookmarks.fetch({tags: [tag]}, b => taggedURLs.add(b.url.href));
deepEqual(Array.from(taggedURLs).sort(compareAscending),
urls.sort(compareAscending), message);
}
function assertURLHasTags(url, tags, message) {
@ -581,7 +583,7 @@ add_task(async function test_update_tags() {
deepEqual(updatedItem.tags, ["foo", "baz"], "Should return updated tags");
assertURLHasTags("https://mozilla.org", ["baz", "foo"],
"Should update tags for URL");
assertTagForURLs("bar", [], "Should remove existing tag");
await assertTagForURLs("bar", [], "Should remove existing tag");
}
info("Tags with whitespace");
@ -666,7 +668,7 @@ add_task(async function test_pullChanges_tags() {
deepEqual(Object.keys(changes).sort(),
[firstItem.recordId, secondItem.recordId, taggedItem.recordId].sort(),
"Should include tagged bookmarks after changing case");
assertTagForURLs("TaGgY", ["https://example.org/", "https://mozilla.org/"],
await assertTagForURLs("TaGgY", ["https://example.org/", "https://mozilla.org/"],
"Should add tag for new URL");
await setChangesSynced(changes);
}
@ -715,17 +717,17 @@ add_task(async function test_pullChanges_tags() {
deepEqual(Object.keys(changes).sort(),
[firstItem.recordId, secondItem.recordId, untaggedItem.recordId].sort(),
"Should include tagged bookmarks after changing tag entry URI");
assertTagForURLs("tricky", ["https://bugzilla.org/", "https://mozilla.org/"],
await assertTagForURLs("tricky", ["https://bugzilla.org/", "https://mozilla.org/"],
"Should remove tag entry for old URI");
await setChangesSynced(changes);
bm.url = "https://example.com/";
bm.url = "https://example.org/";
await PlacesUtils.bookmarks.update(bm);
changes = await PlacesSyncUtils.bookmarks.pullChanges();
deepEqual(Object.keys(changes).sort(),
[untaggedItem.recordId].sort(),
[firstItem.recordId, secondItem.recordId, untaggedItem.recordId].sort(),
"Should include tagged bookmarks after changing tag entry URL");
assertTagForURLs("tricky", ["https://example.com/", "https://mozilla.org/"],
await assertTagForURLs("tricky", ["https://example.org/", "https://mozilla.org/"],
"Should remove tag entry for old URL");
await setChangesSynced(changes);
}
@ -1313,13 +1315,13 @@ add_task(async function test_insert_tags() {
title: "bar",
}].map(info => PlacesSyncUtils.bookmarks.insert(info)));
assertTagForURLs("foo", ["https://example.com/", "https://example.org/"],
await assertTagForURLs("foo", ["https://example.com/", "https://example.org/"],
"2 URLs with new tag");
assertTagForURLs("bar", ["https://example.com/"], "1 URL with existing tag");
assertTagForURLs("baz", ["https://example.org/",
await assertTagForURLs("bar", ["https://example.com/"], "1 URL with existing tag");
await assertTagForURLs("baz", ["https://example.org/",
"place:queryType=1&sort=12&maxResults=10"],
"Should support tagging URLs and tag queries");
assertTagForURLs("qux", ["place:queryType=1&sort=12&maxResults=10"],
await assertTagForURLs("qux", ["place:queryType=1&sort=12&maxResults=10"],
"Should support tagging tag queries");
await PlacesUtils.bookmarks.eraseEverything();
@ -1353,7 +1355,7 @@ add_task(async function test_insert_tags_whitespace() {
assertURLHasTags("https://example.net/", ["taggy"],
"Should ignore dupes when setting tags");
assertTagForURLs("taggy", ["https://example.net/", "https://example.org/"],
await assertTagForURLs("taggy", ["https://example.net/", "https://example.org/"],
"Should exclude falsy tags");
PlacesUtils.tagging.untagURI(uri("https://example.org"), ["untrimmed", "taggy"]);

View File

@ -58,12 +58,6 @@ function run_test() {
Assert.equal(uri2tags.length, 1);
Assert.equal(uri2tags[0], "Tag 1");
// test getURIsForTag
var tag1uris = tagssvc.getURIsForTag("tag 1");
Assert.equal(tag1uris.length, 2);
Assert.ok(tag1uris[0].equals(uri1));
Assert.ok(tag1uris[1].equals(uri2));
// test untagging
tagssvc.untagURI(uri1, ["tag 1"]);
Assert.equal(tag1node.childCount, 1);