--- a/browser/components/places/content/editBookmarkOverlay.js
+++ b/browser/components/places/content/editBookmarkOverlay.js
@@ -126,24 +126,28 @@ var gEditItemOverlay = {
_initDescriptionField() {
if (!this._paneInfo.isItem)
throw new Error("_initDescriptionField called unexpectedly");
this._initTextField(this._descriptionField,
PlacesUIUtils.getItemDescription(this._paneInfo.itemId));
},
- _initKeywordField: Task.async(function* (aNewKeyword) {
- if (!this._paneInfo.isBookmark)
+ _initKeywordField: Task.async(function* (newKeyword = "") {
+ if (!this._paneInfo.isBookmark) {
throw new Error("_initKeywordField called unexpectedly");
+ }
- let newKeyword = aNewKeyword;
- if (newKeyword === undefined) {
- let itemId = this._paneInfo.itemId;
- newKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId);
+ if (!newKeyword) {
+ let entries = [];
+ yield PlacesUtils.keywords.fetch({ url: this._paneInfo.uri.spec },
+ e => entries.push(e));
+ if (entries.length > 0) {
+ this._keyword = newKeyword = entries[0].keyword;
+ }
}
this._initTextField(this._keywordField, newKeyword);
}),
_initLoadInSidebar: Task.async(function* () {
if (!this._paneInfo.isBookmark)
throw new Error("_initLoadInSidebar called unexpectedly");
@@ -210,17 +214,17 @@ var gEditItemOverlay = {
// hide the description field for
if (showOrCollapse("descriptionRow", isItem && !this.readOnly,
"description")) {
this._initDescriptionField();
this._descriptionField.readOnly = this.readOnly;
}
if (showOrCollapse("keywordRow", isBookmark, "keyword")) {
- this._initKeywordField();
+ this._initKeywordField().catch(Components.utils.reportError);
this._keywordField.readOnly = this.readOnly;
}
// Collapse the tag selector if the item does not accept tags.
if (showOrCollapse("tagsRow", isURI || bulkTagging, "tags"))
this._initTagsField().catch(Components.utils.reportError);
else if (!this._element("tagsSelectorRow").collapsed)
this.toggleTagsSelector().catch(Components.utils.reportError);
@@ -563,17 +567,17 @@ var gEditItemOverlay = {
return;
}
Task.spawn(function* () {
let guid = this._paneInfo.isTag
? (yield PlacesUtils.promiseItemGuid(this._paneInfo.itemId))
: this._paneInfo.itemGuid;
PlacesTransactions.EditTitle({ guid, title: newTitle })
.transact().catch(Components.utils.reportError);
- }).catch(Cu.reportError);
+ }).catch(Components.utils.reportError);
}
},
onDescriptionFieldChange() {
if (this.readOnly || !this._paneInfo.isItem)
return;
let itemId = this._paneInfo.itemId;
@@ -620,24 +624,29 @@ var gEditItemOverlay = {
.transact().catch(Components.utils.reportError);
},
onKeywordFieldChange() {
if (this.readOnly || !this._paneInfo.isBookmark)
return;
let itemId = this._paneInfo.itemId;
- let newKeyword = this._keywordField.value;
+ let oldKeyword = this._keyword;
+ let keyword = this._keyword = this._keywordField.value;
+ let postData = this._paneInfo.postData;
if (!PlacesUIUtils.useAsyncTransactions) {
- let txn = new PlacesEditBookmarkKeywordTransaction(itemId, newKeyword, this._paneInfo.postData);
+ let txn = new PlacesEditBookmarkKeywordTransaction(itemId,
+ keyword,
+ postData,
+ oldKeyword);
PlacesUtils.transactionManager.doTransaction(txn);
return;
}
let guid = this._paneInfo.itemGuid;
- PlacesTransactions.EditKeyword({ guid, keyword: newKeyword })
+ PlacesTransactions.EditKeyword({ guid, keyword, postData, oldKeyword })
.transact().catch(Components.utils.reportError);
},
onLoadInSidebarCheckboxCommand() {
if (!this.initialized || !this._paneInfo.isBookmark)
return;
let annotation = { name : PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO };
@@ -1080,17 +1089,17 @@ var gEditItemOverlay = {
if (this._paneInfo.visibleRows.has("tagsRow")) {
delete this._paneInfo._cachedCommonTags;
this._onTagsChange(aItemId);
}
}
break;
case "keyword":
if (this._paneInfo.visibleRows.has("keywordRow"))
- this._initKeywordField(aValue);
+ this._initKeywordField(aValue).catch(Components.utils.reportError);
break;
case PlacesUIUtils.DESCRIPTION_ANNO:
if (this._paneInfo.visibleRows.has("descriptionRow"))
this._initDescriptionField();
break;
case PlacesUIUtils.LOAD_IN_SIDEBAR_ANNO:
if (this._paneInfo.visibleRows.has("loadInSidebarCheckbox"))
this._initLoadInSidebar();
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -849,16 +849,23 @@ Database::InitSchema(bool* aDatabaseMigr
if (currentSchemaVersion < 33) {
rv = MigrateV33Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 50 uses schema version 33.
+ if (currentSchemaVersion < 34) {
+ rv = MigrateV34Up();
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ // Firefox 51 uses schema version 34.
+
// Schema Upgrades must add migration code here.
rv = UpdateBookmarkRootTitles();
// We don't want a broken localization to cause us to think
// the database is corrupt and needs to be replaced.
MOZ_ASSERT(NS_SUCCEEDED(rv));
}
}
@@ -1808,16 +1815,31 @@ Database::MigrateV33Up() {
// Create an index on url_hash.
rv = mMainConn->ExecuteSimpleSQL(CREATE_IDX_MOZ_PLACES_URL_HASH);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
+nsresult
+Database::MigrateV34Up() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
+ "DELETE FROM moz_keywords WHERE id IN ( "
+ "SELECT id FROM moz_keywords k "
+ "WHERE NOT EXISTS (SELECT 1 FROM moz_places h WHERE k.place_id = h.id) "
+ ")"
+ ));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+}
+
void
Database::Shutdown()
{
// As the last step in the shutdown path, finalize the database handle.
MOZ_ASSERT(NS_IsMainThread());
MOZ_ASSERT(!mClosed);
// Break cycles with the shutdown blockers.
--- a/toolkit/components/places/Database.h
+++ b/toolkit/components/places/Database.h
@@ -13,17 +13,17 @@
#include "mozilla/storage.h"
#include "mozilla/storage/StatementCache.h"
#include "mozilla/Attributes.h"
#include "nsIEventTarget.h"
#include "Shutdown.h"
// This is the schema version. Update it at any schema change and add a
// corresponding migrateVxx method below.
-#define DATABASE_SCHEMA_VERSION 33
+#define DATABASE_SCHEMA_VERSION 34
// Fired after Places inited.
#define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
// Fired when initialization fails due to a locked database.
#define TOPIC_DATABASE_LOCKED "places-database-locked"
// This topic is received when the profile is about to be lost. Places does
// initial shutdown work and notifies TOPIC_PLACES_SHUTDOWN to all listeners.
// Any shutdown work that requires the Places APIs should happen here.
@@ -264,16 +264,17 @@ protected:
nsresult MigrateV25Up();
nsresult MigrateV26Up();
nsresult MigrateV27Up();
nsresult MigrateV28Up();
nsresult MigrateV30Up();
nsresult MigrateV31Up();
nsresult MigrateV32Up();
nsresult MigrateV33Up();
+ nsresult MigrateV34Up();
nsresult UpdateBookmarkRootTitles();
friend class ConnectionShutdownBlocker;
private:
~Database();
--- a/toolkit/components/places/PlacesTransactions.jsm
+++ b/toolkit/components/places/PlacesTransactions.jsm
@@ -880,17 +880,17 @@ function (aInput, aRequiredProps = [], a
// Update the documentation at the top of this module if you add or
// remove properties.
DefineTransaction.defineInputProps(["url", "feedUrl", "siteUrl"],
DefineTransaction.urlValidate, null);
DefineTransaction.defineInputProps(["guid", "parentGuid", "newParentGuid"],
DefineTransaction.guidValidate);
DefineTransaction.defineInputProps(["title"],
DefineTransaction.strOrNullValidate, null);
-DefineTransaction.defineInputProps(["keyword", "postData", "tag",
+DefineTransaction.defineInputProps(["keyword", "oldKeyword", "postData", "tag",
"excludingAnnotation"],
DefineTransaction.strValidate, "");
DefineTransaction.defineInputProps(["index", "newIndex"],
DefineTransaction.indexValidate,
PlacesUtils.bookmarks.DEFAULT_INDEX);
DefineTransaction.defineInputProps(["annotation"],
DefineTransaction.annotationObjectValidate);
DefineTransaction.defineArrayInputProp("guids", "guid");
@@ -995,18 +995,22 @@ function* createItemsFromBookmarksTree(a
let guid = aRestoring ? aItem.guid : undefined;
let parentId = yield PlacesUtils.promiseItemId(aParentGuid);
let annos = aItem.annos ? [...aItem.annos] : [];
switch (aItem.type) {
case PlacesUtils.TYPE_X_MOZ_PLACE: {
let uri = NetUtil.newURI(aItem.uri);
itemId = PlacesUtils.bookmarks.insertBookmark(
parentId, uri, aIndex, aItem.title, guid);
- if ("keyword" in aItem)
- PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aItem.keyword);
+ if ("keyword" in aItem) {
+ yield PlacesUtils.keywords.insert({
+ keyword: aItem.keyword,
+ url: uri.spec
+ });
+ }
if ("tags" in aItem) {
PlacesUtils.tagging.tagURI(uri, aItem.tags.split(","));
}
break;
}
case PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER: {
// Either a folder or a livemark
let [feedURI, siteURI] = extractLivemarkDetails(annos);
@@ -1079,36 +1083,42 @@ var PT = PlacesTransactions;
*/
PT.NewBookmark = DefineTransaction(["parentGuid", "url"],
["index", "title", "keyword", "postData",
"annotations", "tags"]);
PT.NewBookmark.prototype = Object.seal({
execute: function (aParentGuid, aURI, aIndex, aTitle,
aKeyword, aPostData, aAnnos, aTags) {
return ExecuteCreateItem(this, aParentGuid,
- function (parentId, guidToRestore = "") {
+ function* (parentId, guidToRestore = "") {
let itemId = PlacesUtils.bookmarks.insertBookmark(
parentId, aURI, aIndex, aTitle, guidToRestore);
- if (aKeyword)
- PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword);
- if (aPostData)
- PlacesUtils.setPostDataForBookmark(itemId, aPostData);
- if (aAnnos.length)
+
+ if (aKeyword) {
+ yield PlacesUtils.keywords.insert({
+ url: aURI.spec,
+ keyword: aKeyword,
+ postData: aPostData
+ });
+ }
+ if (aAnnos.length) {
PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
+ }
if (aTags.length > 0) {
let currentTags = PlacesUtils.tagging.getTagsForURI(aURI);
aTags = aTags.filter(t => !currentTags.includes(t));
PlacesUtils.tagging.tagURI(aURI, aTags);
}
return itemId;
},
function _additionalOnUndo() {
- if (aTags.length > 0)
+ if (aTags.length > 0) {
PlacesUtils.tagging.untagURI(aURI, aTags);
+ }
});
}
});
/**
* Transaction for creating a folder.
*
* Required Input Properties: title, parentGuid.
@@ -1116,17 +1126,17 @@ PT.NewBookmark.prototype = Object.seal({
*
* When this transaction is executed, it's resolved to the new folder's GUID.
*/
PT.NewFolder = DefineTransaction(["parentGuid", "title"],
["index", "annotations"]);
PT.NewFolder.prototype = Object.seal({
execute: function (aParentGuid, aTitle, aIndex, aAnnos) {
return ExecuteCreateItem(this, aParentGuid,
- function(parentId, guidToRestore = "") {
+ function* (parentId, guidToRestore = "") {
let itemId = PlacesUtils.bookmarks.createFolder(
parentId, aTitle, aIndex, guidToRestore);
if (aAnnos.length > 0)
PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
return itemId;
});
}
});
@@ -1139,17 +1149,17 @@ PT.NewFolder.prototype = Object.seal({
*
* When this transaction is executed, it's resolved to the new separator's
* GUID.
*/
PT.NewSeparator = DefineTransaction(["parentGuid"], ["index"]);
PT.NewSeparator.prototype = Object.seal({
execute: function (aParentGuid, aIndex) {
return ExecuteCreateItem(this, aParentGuid,
- function (parentId, guidToRestore = "") {
+ function* (parentId, guidToRestore = "") {
let itemId = PlacesUtils.bookmarks.insertSeparator(
parentId, aIndex, guidToRestore);
return itemId;
});
}
});
/**
@@ -1331,24 +1341,46 @@ PT.Annotate.prototype = {
}
};
/**
* Transaction for setting the keyword for a bookmark.
*
* Required Input Properties: guid, keyword.
*/
-PT.EditKeyword = DefineTransaction(["guid", "keyword"]);
+PT.EditKeyword = DefineTransaction(["guid", "keyword"],
+ ["postData", "oldKeyword"]);
PT.EditKeyword.prototype = Object.seal({
- execute: function* (aGuid, aKeyword) {
- let itemId = yield PlacesUtils.promiseItemId(aGuid),
- oldKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId);
- PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword);
- this.undo = () => {
- PlacesUtils.bookmarks.setKeywordForBookmark(itemId, oldKeyword);
+ execute: function* (aGuid, aKeyword, aPostData, aOldKeyword) {
+ let url;
+ let oldKeywordEntry;
+ if (aOldKeyword) {
+ oldKeywordEntry = yield PlacesUtils.keywords.fetch(aOldKeyword);
+ url = oldKeywordEntry.url;
+ yield PlacesUtils.keywords.remove(aOldKeyword);
+ }
+
+ if (aKeyword) {
+ if (!url) {
+ url = (yield PlacesUtils.bookmarks.fetch(aGuid)).url;
+ }
+ yield PlacesUtils.keywords.insert({
+ url: url,
+ keyword: aKeyword,
+ postData: aPostData || (oldKeywordEntry ? oldKeywordEntry.postData : "")
+ });
+ }
+
+ this.undo = function* () {
+ if (aKeyword) {
+ yield PlacesUtils.keywords.remove(aKeyword);
+ }
+ if (oldKeywordEntry) {
+ yield PlacesUtils.keywords.insert(oldKeywordEntry);
+ }
};
}
});
/**
* Transaction for sorting a folder by name.
*
* Required Input Properties: guid.
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -1822,17 +1822,18 @@ this.PlacesUtils = {
default:
Cu.reportError("Unexpected bookmark type");
break;
}
return item;
}.bind(this);
const QUERY_STR =
- `WITH RECURSIVE
+ `/* do not warn (bug no): cannot use an index */
+ WITH RECURSIVE
descendants(fk, level, type, id, guid, parent, parentGuid, position,
title, dateAdded, lastModified) AS (
SELECT b1.fk, 0, b1.type, b1.id, b1.guid, b1.parent,
(SELECT guid FROM moz_bookmarks WHERE id = b1.parent),
b1.position, b1.title, b1.dateAdded, b1.lastModified
FROM moz_bookmarks b1 WHERE b1.guid=:item_guid
UNION ALL
SELECT b2.fk, level + 1, b2.type, b2.id, b2.guid, b2.parent,
@@ -2351,39 +2352,64 @@ XPCOMUtils.defineLazyGetter(this, "gKeyw
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")
+ onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid,
+ parentGuid, oldVal) {
+ if (gIgnoreKeywordNotifications) {
+ return;
+ }
+
+ if (prop == "keyword") {
+ this._onKeywordChanged(guid, val).catch(Cu.reportError);
+ } else if (prop == "uri") {
+ this._onUrlChanged(guid, val, oldVal).catch(Cu.reportError);
+ }
+ },
+
+ _onKeywordChanged: Task.async(function* (guid, keyword) {
+ let bookmark = yield PlacesUtils.bookmarks.fetch(guid);
+ // Due to mixed sync/async operations, by this time the bookmark could
+ // have disappeared and we already handle removals in onItemRemoved.
+ if (!bookmark) {
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 });
+ }
+
+ if (keyword.length == 0) {
+ // We are removing a keyword.
+ let keywords = keywordsForHref(bookmark.url.href)
+ for (let kw of keywords) {
+ cache.delete(kw);
}
- }).catch(Cu.reportError);
- }
+ } else {
+ // We are adding a new keyword.
+ cache.set(keyword, { keyword, url: bookmark.url });
+ }
+ }),
+
+ _onUrlChanged: Task.async(function* (guid, url, oldUrl) {
+ // Check if the old url is associated with keywords.
+ let entries = [];
+ yield PlacesUtils.keywords.fetch({ url: oldUrl }, e => entries.push(e));
+ if (entries.length == 0) {
+ return;
+ }
+
+ // Move the keywords to the new url.
+ for (let entry of entries) {
+ yield PlacesUtils.keywords.remove(entry.keyword);
+ entry.url = new URL(url);
+ yield PlacesUtils.keywords.insert(entry);
+ }
+ }),
};
PlacesUtils.bookmarks.addObserver(observer, false);
PlacesUtils.registerShutdownFunction(() => {
PlacesUtils.bookmarks.removeObserver(observer);
});
return cache;
})
@@ -3418,50 +3444,87 @@ PlacesSetPageAnnotationTransaction.proto
* Transaction for editing a bookmark's keyword.
*
* @param aItemId
* id of the bookmark to edit
* @param aNewKeyword
* new keyword for the bookmark
* @param aNewPostData [optional]
* new keyword's POST data, if available
+ * @param aOldKeyword [optional]
+ * old keyword of the bookmark
*
* @return nsITransaction object
*/
this.PlacesEditBookmarkKeywordTransaction =
- function PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword, aNewPostData)
-{
+ function PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword,
+ aNewPostData, aOldKeyword) {
this.item = new TransactionItemCache();
this.item.id = aItemId;
+ this.item.keyword = aOldKeyword;
+ this.item.href = (PlacesUtils.bookmarks.getBookmarkURI(aItemId)).spec;
this.new = new TransactionItemCache();
this.new.keyword = aNewKeyword;
this.new.postData = aNewPostData
}
PlacesEditBookmarkKeywordTransaction.prototype = {
__proto__: BaseTransaction.prototype,
doTransaction: function EBKTXN_doTransaction()
{
- // Store the current values.
- this.item.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
- if (this.item.keyword)
- this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id);
-
- // Update the keyword.
- PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.new.keyword);
- if (this.new.keyword && this.new.postData)
- PlacesUtils.setPostDataForBookmark(this.item.id, this.new.postData);
+ let done = false;
+ Task.spawn(function* () {
+ if (this.item.keyword) {
+ let oldEntry = yield PlacesUtils.keywords.fetch(this.item.keyword);
+ this.item.postData = oldEntry.postData;
+ yield PlacesUtils.keywords.remove(this.item.keyword);
+ }
+
+ if (this.new.keyword) {
+ yield PlacesUtils.keywords.insert({
+ url: this.item.href,
+ keyword: this.new.keyword,
+ postData: this.new.postData || this.item.postData
+ });
+ }
+ }.bind(this)).catch(Cu.reportError)
+ .then(() => done = true);
+ // TODO: Until we can move to PlacesTransactions.jsm, we must spin the
+ // events loop :(
+ let thread = Services.tm.currentThread;
+ while (!done) {
+ thread.processNextEvent(true);
+ }
},
undoTransaction: function EBKTXN_undoTransaction()
{
- PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.item.keyword);
- if (this.item.postData)
- PlacesUtils.setPostDataForBookmark(this.item.id, this.item.postData);
+
+ let done = false;
+ Task.spawn(function* () {
+ if (this.new.keyword) {
+ yield PlacesUtils.keywords.remove(this.new.keyword);
+ }
+
+ if (this.item.keyword) {
+ yield PlacesUtils.keywords.insert({
+ url: this.item.href,
+ keyword: this.item.keyword,
+ postData: this.item.postData
+ });
+ }
+ }.bind(this)).catch(Cu.reportError)
+ .then(() => done = true);
+ // TODO: Until we can move to PlacesTransactions.jsm, we must spin the
+ // events loop :(
+ let thread = Services.tm.currentThread;
+ while (!done) {
+ thread.processNextEvent(true);
+ }
}
};
/**
* Transaction for editing the post data associated with a bookmark.
*
* @param aItemId
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -1,14 +1,14 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-const CURRENT_SCHEMA_VERSION = 33;
+const CURRENT_SCHEMA_VERSION = 34;
const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
const NS_APP_USER_PROFILE_50_DIR = "ProfD";
const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
// Shortcuts to transitions type.
const TRANSITION_LINK = Ci.nsINavHistoryService.TRANSITION_LINK;
const TRANSITION_TYPED = Ci.nsINavHistoryService.TRANSITION_TYPED;
new file mode 100644
index 0000000000000000000000000000000000000000..474628996f90b85655edcf9a5dface1d122b1088
GIT binary patch
literal 1146880
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
z000000000000000000000000000000008*^RZpsP_t3ffrbm>OmWIQ{LrRMlXEL4X
zBWr59ySqoWCs!ug+F~P<sYNyMjuG?c*0dy2@s{NBn(jnLbFw=<VpMHScf74B*&eSs
zwky-#m`--3n&L-qDmG$aDqAR?IxLsf9&b)`weK*iDcO;UcVu#T=}fAt<M`e((P7b3
zdi#lvsvQ}f9Z$8yn=;W%a#1|9IG!3A{mq)oqBDBOxG0qz8Eb1xtW36#%niL{VW_BX
z_~`o&+R!|`ZqD%yE0?t_s_PiG*FMi5aP5?m%Fs!n-AhYLL#3f1WyPIsv8MQuxxY4l
z^w`7F$yDa(QKJuQk1d}QPj|It(nn`IKV{7P^yJAcvu4g-G9nauZS?#T9vOagab;-Y
zcFpALkKd;HtcI4R^og@(*H2$rpQ}Fhmp6a*l%mSe{LqMO^>9J9`g=@#Y+<r1Q?u<=
z7KPTfO<FLyzIOJM6}1yr4BP87f12EQdSPX#@$YUpH}|Q;!mdnwyVeUsYfn6FT*vs+
zR?MA0ruC#@d$s&@-Pui%%24~?-Fn}8Y3fR6lI@9=+jkNPt?kN89Fb@`Io8rSdDgJK
zjwma-=*)u3P}4u$$>Mlh=XUKEgw`e|bT_p(&K}p8Zdo>LugHmaeC@1oWhjwvzw95m
zi0m3o#oD&(BD8kQwDBXB%w2I@YT3%uvORq9r$<&~H&;0LV<`Ir000000000000000
z000000000000000000000002*6MKY9LZ#v2veME}QQh#-_aC&OBA2{o7Y_>n00000
z000000000000000000000000000000004He^4wv~E*=B`00000000000000000000
z000000000000000008V_71>WXcs`W<0RR91000000000000000000000000000000
z0001h_k+E|#i2tAN<;0*m5H{t*vMpRQRoxp*^F@Tawz))00000000000000000000
z0000000000000000002*AC-|q%G#4F6K!p=k;&Aeid@n;Z-s(Kg3kwM1+}aHwE7#X
zgVhsP7p}U0)#a<sTy=Oh6951J0000000000000000000000000000000002^S9e6^
zfKWITF3X-47Y~UPgo^5h*KK%VL0>p=V%MpQ>ZVO?m@_9nreZ*-EF3O9Bwx0qU{1U#
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
zf6Xo)KL7v#00000000000000000000000000000000000>|zz!Pb4@x6kHZ8&ISMg
z000000000000000000000000000000000000Pv5ij_ei+heLx(!{OrMih?=urg%qY
zUTjgiFcK=N8(z2m-#7GyM-=3X9+EFQpkPKaojEy?PGsWEo63IXjA&oDBs?G#E(`Va
z7LH78ZjLTYCR^KMsn&EflZ<8-C(_ZDWLtAQm5Zk1@oZAOJ=&Z|cece=#G9kZj$BG~
z(&k$8lF7D(vDBu%x(D9W7gl61qjJ@0q2Q6=#-KYmF*tPf+p8a5ef8>vt7}(>R^7kq
z@@ysm000000000000000000000000000000000000Kor|BP$DQW>mE&S2iwAq%+CX
zie<UK_nzJ^p|P_q))a4!W#WmB=J@ha(d@taio%-X_WnESwxp8nebwp*{GFBh_ce02
z!o!c+FIQ&spP1h&(wNA^H}x<&S{JRWEiXL$*mo=68B4`GGJQ4bqoa;2D?EJed-c#3
zOJ~}X&54#oyt!X*^`(V}H|+Z^br*Fdnj5=15=*<{ebq({Eh!vyRDK@vGn?LW*4PqT
zmS{?L<cp4v)(t5x95iOfB^M{tnN7t;A68U2XxxsA<!7`h+0~KBma8r-9CY+fI!?u#
z;vG#ZHZ@s4GEz8b>W+)%rXt_$=G&BQdU!$MpyPI2e%mP+A01U4-swQPQf-ZkW9h|x
zMa#1*wKVuyD0nOQbMQ*=hv0X?lff^t2><{9000000000000000000000000000000
z00000@K3HN5)PM?=8i>0xnp5r?ih*Wjs*pgNLg87Bor>m9g54d<x7L^Q1E8(TJU6W
zfAF>7+Tat}1ONa400000000000000000000000000000000000_`h$T$cXTwRB~jj
ztu3)K**>znyL)8mjuMM@lvuc<#K?{k3-*qTDBG^h$nN2hCHZ4<d3G?R!Q-LeufglV
zE5QrFbHUTW#%uxr00000000000000000000000000000000000008_eD2$YaOA8~V
z;gZ~;ICm(@9SU=YNbXP&iIkR=XN#5wKMn<N1%D1+3Vt6v9sDYIB%1&L0000000000
z00000000000000000000000000095AiX#Q#(AuKjps+WH^ace*k%F>pUL+h|SXvk<
z4VUB&#pT&rrNKj?;IF~!!7ITF!E?dW!NzO?000000000000000000000000000000
z000000002|GaC>o4u^{BhL67gpbe#kkx;lKcPP#sigJg-+#!-X6htDSvhr+|aPUef
z`vU*~000000000000000000000000000000000000PiCeh2i3|(oj*|@X_}lw4pqk
z5)S?v%KiWV000000000000000000000000000000000000KofFBvM*do(;pnYoY89
z000000000000000000000000000000000000002Iuk02nE(;aa4Ih2~K^w}m@ycLb
zD0nk?HTXmDOt3L{G<Yz$FZgco&EWRn*5Jl$CIA2c0000000000000000000000000
z0000000000>|(`*;j(Z^MLsAVkPnJ>%Lj$!`5;o34+=_)3d^!ZiVH*GaHKLH6qNJ^
zMZG~`ZxHDX3W~BpSwVTWkILYhQ1Djpr{Kllx!|eb7s129{lUG#w}P()w*@z5GXVeq
z00000000000000000000000000000000000VAm=s42Q!dk==U3g7V_Ra9OydEE|-D
zOG@*xNO3+E8IUhiP*9RjEvU?=7E}}$mW9KSqI^(LSX5Y+t&u%uvkD7KOUuf$om2+n
zLcuG+Z-bu)KM1}NtPidUE(y*J(m``DKR7;{2><{90000000000000000000000000
z0000000000J62g3E)JK3it2{ft$+T(ihQ&<AH8hK;sN<+Q9io-@h5l7M+<wSSDjU!
zk4Ac<3*IQpM+@@N?q@fYRuq;MhkIk!Pb;n{3~h>Scws^1R<VKs*;rX$Y)jUv_PZln
zV$1JbSF$CxZ0x|I&9PC}-?y%?JbN*v!S$iwPr>uSlffgweZjYbuVfPd0000000000
z0000000000000000000000000007|sHv=NY;ZW$c(eqDuWOzwmWb7|*{%mo1q$oUW
zuh0Bxa$`~NY0FR7on6>_dPG^#MUmdqeV#wy+S0N}QQ5G)B47OJk-dqL6Yuz1L3y_M
z(qKg>cq#aG@Z;cH!Iy%of{U^V000000000000000000000000000000000000002+
z|H2-TA>ppf#1V<6lVdHNlV?S832l=WOs=n;J!M7h#1%!kgv5mIruN3!;~LW~%L;M{
z&C~1V9N(~VS<9ljj?!GhDP!iRCr@sfHFNfo5hb~VSq&{s=@VzouAjcLzBrd~;%VbL
z#-Fxg?))*WCl&4<8B#W8+V~Mm=B_v{wQS{S<=LTB278Bs7lVz#1Hm_gTY@WtwZV#D
zad1j-d@v?BG@A(k000000000000000000000000000000000000Kogv9+4s8?%5}&
zCnXb!*{zFD7?4Zo>S#%{#hV**igF2=bj!Sk+S=CEla{wu<P(y~wuP}&zC<LS5L=XP
z+@vs<ke)Dk(d@a&C8^rB3B|idhLoj}$xL5~T!Hp@M^|6GAeV4jX5Q#|i^nf%TfVrx
zB$sgVq~^@Tg)>uA;xk%y%Oy;l*mdfnx@l7z=FEv#<`O0}#p0)&vh?_dIpe03<r2nC
zORQ)+ZA5a(yt)bHxrDmTx&<?rw2ocb+`O=~Ja@%wcJYh@00000000000000000000
z000000000000000006))R-QYo*~NnZ00000000000000000000000000000000000
z004kptUUXP1iudjZw9Yq0{{R300000000000000000000000000000000000_!n1F
W7!HRk3L^P%KtVxKVYsX;^1lH_jT}M%
--- a/toolkit/components/places/tests/migration/xpcshell.ini
+++ b/toolkit/components/places/tests/migration/xpcshell.ini
@@ -15,16 +15,17 @@ support-files =
places_v25.sqlite
places_v26.sqlite
places_v27.sqlite
places_v28.sqlite
places_v30.sqlite
places_v31.sqlite
places_v32.sqlite
places_v33.sqlite
+ places_v34.sqlite
[test_current_from_downgraded.js]
[test_current_from_v6.js]
[test_current_from_v11.js]
[test_current_from_v19.js]
[test_current_from_v24.js]
[test_current_from_v25.js]
[test_current_from_v26.js]
--- a/toolkit/components/places/tests/unit/test_async_transactions.js
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -676,21 +676,22 @@ add_task(function* test_remove_folder()
yield ensureItemsRemoved(folder_level_2_info, folder_level_2_info);
observer.reset();
yield PT.clearTransactionsHistory();
ensureUndoState();
});
add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
- const testURI = NetUtil.newURI("http://add.remove.tag")
- , TAG_1 = "TestTag1", TAG_2 = "TestTag2"
- , KEYWORD = "test_keyword"
- , POST_DATA = "post_data"
- , ANNO = { name: "TestAnno", value: "TestAnnoValue" };
+ const testURI = NetUtil.newURI("http://add.remove.tag");
+ const TAG_1 = "TestTag1";
+ const TAG_2 = "TestTag2";
+ const KEYWORD = "test_keyword";
+ const POST_DATA = "post_data";
+ const ANNO = { name: "TestAnno", value: "TestAnnoValue" };
let folder_info = createTestFolderInfo();
folder_info.guid = yield PT.NewFolder(folder_info).transact();
let ensureTags = ensureTagsForURI.bind(null, testURI);
// Check that the NewBookmark transaction preserves tags.
observer.reset();
let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] };
@@ -744,28 +745,37 @@ add_task(function* test_add_and_remove_b
ensureTags([TAG_1, TAG_2]);
observer.reset();
yield PT.undo();
yield ensureItemsRemoved(b2_info);
ensureTags([TAG_1]);
// Check if Remove correctly restores keywords, tags and annotations.
+ // Since both bookmarks share the same uri, they also share the keyword that
+ // is not removed along with one of the bookmarks.
observer.reset();
yield PT.redo();
- ensureItemsChanged(...b2_post_creation_changes);
+ ensureItemsChanged({ guid: b2_info.guid
+ , isAnnoProperty: true
+ , property: ANNO.name
+ , newValue: ANNO.value });
ensureTags([TAG_1, TAG_2]);
// Test Remove for multiple items.
observer.reset();
yield PT.Remove(b1_info.guid).transact();
yield PT.Remove(b2_info.guid).transact();
yield PT.Remove(folder_info.guid).transact();
yield ensureItemsRemoved(b1_info, b2_info, folder_info);
ensureTags([]);
+ // There is no keyword removal notification cause all bookmarks are removed
+ // before the keyword itself, so there's no one to notify.
+ let entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry, null, "keyword has been removed");
observer.reset();
yield PT.undo();
yield ensureItemsAdded(folder_info);
ensureTags([]);
observer.reset();
yield PT.undo();
@@ -1016,38 +1026,108 @@ add_task(function* test_edit_keyword() {
ensureItemsChanged({ guid: bm_info.guid
, property: "keyword"
, newValue: aCurrentKeyword });
}
bm_info.guid = yield PT.NewBookmark(bm_info).transact();
observer.reset();
- yield PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD }).transact();
+ yield PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD, postData: "postData" }).transact();
ensureKeywordChange(KEYWORD);
+ let entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData");
observer.reset();
yield PT.undo();
ensureKeywordChange();
+ entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry, null);
observer.reset();
yield PT.redo();
ensureKeywordChange(KEYWORD);
+ entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData");
// Cleanup
observer.reset();
yield PT.undo();
ensureKeywordChange();
yield PT.undo();
ensureItemsRemoved(bm_info);
yield PT.clearTransactionsHistory();
ensureUndoState();
});
+add_task(function* test_edit_specific_keyword() {
+ let bm_info = { parentGuid: rootGuid
+ , url: NetUtil.newURI("http://test.edit.keyword") };
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "", aPreviousKeyword = "") {
+ ensureItemsChanged({ guid: bm_info.guid
+ , property: "keyword"
+ , newValue: aCurrentKeyword
+ });
+ }
+
+ yield PlacesUtils.keywords.insert({ keyword: "kw1", url: bm_info.url.spec, postData: "postData1" });
+ yield PlacesUtils.keywords.insert({ keyword: "kw2", url: bm_info.url.spec, postData: "postData2" });
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ yield PT.EditKeyword({ guid: bm_info.guid, keyword: "keyword", oldKeyword: "kw2" }).transact();
+ ensureKeywordChange("keyword", "kw2");
+ let entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData1");
+ entry = yield PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData2");
+ entry = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry, null);
+
+ observer.reset();
+ yield PT.undo();
+ ensureKeywordChange("kw2", "keyword");
+ entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData1");
+ entry = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData2");
+ entry = yield PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry, null);
+
+ observer.reset();
+ yield PT.redo();
+ ensureKeywordChange("keyword", "kw2");
+ entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData1");
+ entry = yield PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData2");
+ entry = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry, null);
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureKeywordChange("kw2");
+ yield PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
add_task(function* test_tag_uri() {
// This also tests passing uri specs.
let bm_info_a = { url: "http://bookmarked.uri"
, parentGuid: rootGuid };
let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
, parentGuid: rootGuid };
let unbookmarked_uri = NetUtil.newURI("http://un.bookmarked.uri");
--- a/toolkit/components/places/tests/unit/test_keywords.js
+++ b/toolkit/components/places/tests/unit/test_keywords.js
@@ -520,11 +520,29 @@ add_task(function* test_oldKeywordsAPI()
yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com" });
Assert.equal(PlacesUtils.bookmarks.getKeywordForBookmark(itemId), "keyword");
Assert.equal(PlacesUtils.bookmarks.getURIForKeyword("keyword").spec, "http://example.com/");
yield PlacesUtils.bookmarks.remove(bookmark);
check_no_orphans();
});
-function run_test() {
- run_next_test();
-}
+add_task(function* test_bookmarkURLChange() {
+ let fc1 = yield foreign_count("http://example1.com/");
+ let fc2 = yield foreign_count("http://example2.com/");
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example1.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword",
+ url: "http://example1.com/" });
+
+ yield check_keyword(true, "http://example1.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 2); // +1 bookmark +1 keyword
+
+ yield PlacesUtils.bookmarks.update({ guid: bookmark.guid,
+ url: "http://example2.com/"});
+ yield promiseKeyword("keyword", "http://example2.com/");
+
+ yield check_keyword(false, "http://example1.com/", "keyword");
+ yield check_keyword(true, "http://example2.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1); // -1 bookmark -1 keyword
+ Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 bookmark +1 keyword
+});
--- a/toolkit/components/places/tests/unit/test_placesTxn.js
+++ b/toolkit/components/places/tests/unit/test_placesTxn.js
@@ -550,16 +550,50 @@ add_task(function* test_edit_keyword() {
txn.undoTransaction();
do_check_eq(observer._itemChangedId, testBkmId);
do_check_eq(observer._itemChangedProperty, "keyword");
do_check_eq(observer._itemChangedValue, "");
do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), null);
});
+add_task(function* test_edit_specific_keyword() {
+ const KEYWORD = "keyword-test_edit_keyword2";
+
+ let testURI = NetUtil.newURI("http://test_edit_keyword2.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit keyword");
+ // Add multiple keyword to this uri.
+ yield PlacesUtils.keywords.insert({ keyword: "kw1", url: testURI.spec, postData: "postData1" });
+ yield PlacesUtils.keywords.insert({keyword: "kw2", url: testURI.spec, postData: "postData2" });
+
+ // Try to change only kw2.
+ let txn = new PlacesEditBookmarkKeywordTransaction(testBkmId, KEYWORD, "postData2", "kw2");
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "keyword");
+ do_check_eq(observer._itemChangedValue, KEYWORD);
+ let entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, testURI.spec);
+ Assert.equal(entry.postData, "postData1");
+ yield promiseKeyword(KEYWORD, testURI.spec, "postData2");
+ yield promiseKeyword("kw2", null);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "keyword");
+ do_check_eq(observer._itemChangedValue, "kw2");
+ do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), "postData1");
+ entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, testURI.spec);
+ Assert.equal(entry.postData, "postData1");
+ yield promiseKeyword("kw2", testURI.spec, "postData2");
+ yield promiseKeyword("keyword", null);
+});
+
add_task(function* test_LoadInSidebar_transaction() {
let testURI = NetUtil.newURI("http://test_LoadInSidebar_transaction.com");
let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test LoadInSidebar transaction");
const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
let anno = { name: LOAD_IN_SIDEBAR_ANNO,
type: Ci.nsIAnnotationService.TYPE_INT32,
flags: 0,