Bug 1150678 - Changing url of a bookmark with a keyword breaks the keyword forever. r=adw draft
authorMarco Bonardo <mbonardo@mozilla.com>
Tue, 30 Aug 2016 11:04:22 +0200
changeset 408124 4fc77a9e4c2241787041a57da10907f32250babd
parent 407818 506facea63169a29e04eb140663da1730052db64
child 530049 b68aeb7bed7aaa121b0ca97df2c029342f2833c6
push id28151
push usermak77@bonardo.net
push dateWed, 31 Aug 2016 18:43:57 +0000
reviewersadw
bugs1150678
milestone51.0a1
Bug 1150678 - Changing url of a bookmark with a keyword breaks the keyword forever. r=adw MozReview-Commit-ID: 89Od4PKpQse
browser/components/places/content/editBookmarkOverlay.js
toolkit/components/places/Database.cpp
toolkit/components/places/Database.h
toolkit/components/places/PlacesTransactions.jsm
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/migration/places_v34.sqlite
toolkit/components/places/tests/migration/xpcshell.ini
toolkit/components/places/tests/unit/test_async_transactions.js
toolkit/components/places/tests/unit/test_keywords.js
toolkit/components/places/tests/unit/test_placesTxn.js
--- 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,