Bug 1310737 - Urlbar doesn't properly handle %S and POST keywords. r=adw, a=gchang
authorMarco Bonardo <mbonardo@mozilla.com>
Wed, 19 Oct 2016 15:09:01 +0200
changeset 356500 476ff24916373b77e5cc0b1f2658188e3939406d
parent 356499 9c1ca21cf9c30f6056e9b4cef7280829a2b60ca7
child 356501 b8053319f3025d4b7a7c20ed9a38c9d950779c07
push id6570
push userraliiev@mozilla.com
push dateMon, 14 Nov 2016 12:26:13 +0000
treeherdermozilla-beta@f455459b2ae5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw, gchang
bugs1310737, 1315514
milestone51.0a2
Bug 1310737 - Urlbar doesn't properly handle %S and POST keywords. r=adw, a=gchang MozReview-Commit-ID: 3IEqahvOwVH * * * Bug 1315514 - Bookmark keywords are still broken for POST forms. r=adw MozReview-Commit-ID: 3lrRbZVtgii
browser/base/content/browser.js
browser/base/content/test/general/browser_addKeywordSearch.js
browser/base/content/test/urlbar/browser.ini
browser/base/content/test/urlbar/browser_action_keyword.js
browser/base/content/test/urlbar/browser_urlbarEnter.js
browser/base/content/test/urlbar/print_postdata.sjs
browser/base/content/urlbarBindings.xml
toolkit/components/places/PlacesUtils.jsm
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js
toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js
toolkit/components/places/tests/unifiedcomplete/test_searchEngine_alias.js
toolkit/modules/BrowserUtils.jsm
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2041,151 +2041,98 @@ function loadURI(uri, referrer, postData
                  referrerPolicy: referrerPolicy,
                  postData: postData,
                  allowThirdPartyFixup: allowThirdPartyFixup,
                  userContextId: userContextId });
   } catch (e) {}
 }
 
 /**
- * Given a urlbar value, discerns between URIs, keywords and aliases.
+ * Given a string, will generate a more appropriate urlbar value if a Places
+ * keyword or a search alias is found at the beginning of it.
  *
  * @param url
- *        The urlbar value.
- * @param callback (optional, deprecated)
- *        The callback function invoked when done. This parameter is
- *        deprecated, please use the Promise that is returned.
+ *        A string that may begin with a keyword or an alias.
  *
- * @return Promise<{ postData, url, mayInheritPrincipal }>
+ * @return {Promise}
+ * @resolves { url, postData, mayInheritPrincipal }. If it's not possible
+ *           to discern a keyword or an alias, url will be the input string.
  */
 function getShortcutOrURIAndPostData(url, callback = null) {
   if (callback) {
     Deprecated.warning("Please use the Promise returned by " +
                        "getShortcutOrURIAndPostData() instead of passing a " +
                        "callback",
                        "https://bugzilla.mozilla.org/show_bug.cgi?id=1100294");
   }
-
   return Task.spawn(function* () {
     let mayInheritPrincipal = false;
     let postData = null;
-    let shortcutURL = null;
-    let keyword = url;
-    let param = "";
-
-    let offset = url.indexOf(" ");
-    if (offset > 0) {
-      keyword = url.substr(0, offset);
-      param = url.substr(offset + 1);
+    // Split on the first whitespace.
+    let [keyword, param = ""] = url.trim().split(/\s(.+)/, 2);
+
+    if (!keyword) {
+      return { url, postData, mayInheritPrincipal };
     }
 
     let engine = Services.search.getEngineByAlias(keyword);
     if (engine) {
       let submission = engine.getSubmission(param, null, "keyword");
-      postData = submission.postData;
-      return { postData: submission.postData, url: submission.uri.spec,
+      return { url: submission.uri.spec,
+               postData: submission.postData,
                mayInheritPrincipal };
     }
 
     // A corrupt Places database could make this throw, breaking navigation
     // from the location bar.
+    let entry = null;
     try {
-      let entry = yield PlacesUtils.keywords.fetch(keyword);
-      if (entry) {
-        shortcutURL = entry.url.href;
-        postData = entry.postData;
-      }
+      entry = yield PlacesUtils.keywords.fetch(keyword);
     } catch (ex) {
-      Components.utils.reportError(`Unable to fetch data for Places keyword "${keyword}": ${ex}`);
-    }
-
-    if (!shortcutURL) {
-      return { postData, url, mayInheritPrincipal };
-    }
-
-    let escapedPostData = "";
-    if (postData)
-      escapedPostData = unescape(postData);
-
-    if (/%s/i.test(shortcutURL) || /%s/i.test(escapedPostData)) {
-      let charset = "";
-      const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/;
-      let matches = shortcutURL.match(re);
-
-      if (matches) {
-        [, shortcutURL, charset] = matches;
-      } else {
-        let uri;
-        try {
-          // makeURI() throws if URI is invalid.
-          uri = makeURI(shortcutURL);
-        } catch (ex) {}
-
-        if (uri) {
-          // Try to get the saved character-set.
-          // Will return an empty string if character-set is not found.
-          charset = yield PlacesUtils.getCharsetForURI(uri);
-        }
+      Cu.reportError(`Unable to fetch Places keyword "${keyword}": ${ex}`);
+    }
+    if (!entry || !entry.url) {
+      // This is not a Places keyword.
+      return { url, postData, mayInheritPrincipal };
+    }
+
+    try {
+      [url, postData] =
+        yield BrowserUtils.parseUrlAndPostData(entry.url.href,
+                                               entry.postData,
+                                               param);
+      if (postData) {
+        postData = getPostDataStream(postData);
       }
 
-      // encodeURIComponent produces UTF-8, and cannot be used for other charsets.
-      // escape() works in those cases, but it doesn't uri-encode +, @, and /.
-      // Therefore we need to manually replace these ASCII characters by their
-      // encodeURIComponent result, to match the behavior of nsEscape() with
-      // url_XPAlphas
-      let encodedParam = "";
-      if (charset && charset != "UTF-8")
-        encodedParam = escape(convertFromUnicode(charset, param)).
-                       replace(/[+@\/]+/g, encodeURIComponent);
-      else // Default charset is UTF-8
-        encodedParam = encodeURIComponent(param);
-
-      shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param);
-
-      if (/%s/i.test(escapedPostData)) // POST keyword
-        postData = getPostDataStream(escapedPostData, param, encodedParam,
-                                               "application/x-www-form-urlencoded");
-
-      // This URL came from a bookmark, so it's safe to let it inherit the current
-      // document's principal.
+      // Since this URL came from a bookmark, it's safe to let it inherit the
+      // current document's principal.
       mayInheritPrincipal = true;
 
-      return { postData, url: shortcutURL, mayInheritPrincipal };
-    }
-
-    if (param) {
-      // This keyword doesn't take a parameter, but one was provided. Just return
-      // the original URL.
-      postData = null;
-
-      return { postData, url, mayInheritPrincipal };
-    }
-
-    // This URL came from a bookmark, so it's safe to let it inherit the current
-    // document's principal.
-    mayInheritPrincipal = true;
-
-    return { postData, url: shortcutURL, mayInheritPrincipal };
+    } catch (ex) {
+      // It was not possible to bind the param, just use the original url value.
+    }
+
+    return { url, postData, mayInheritPrincipal };
   }).then(data => {
     if (callback) {
       callback(data);
     }
-
     return data;
   });
 }
 
-function getPostDataStream(aStringData, aKeyword, aEncKeyword, aType) {
-  var dataStream = Cc["@mozilla.org/io/string-input-stream;1"].
-                   createInstance(Ci.nsIStringInputStream);
-  aStringData = aStringData.replace(/%s/g, aEncKeyword).replace(/%S/g, aKeyword);
-  dataStream.data = aStringData;
-
-  var mimeStream = Cc["@mozilla.org/network/mime-input-stream;1"].
-                   createInstance(Ci.nsIMIMEInputStream);
+function getPostDataStream(aPostDataString,
+                           aType = "application/x-www-form-urlencoded") {
+  let dataStream = Cc["@mozilla.org/io/string-input-stream;1"]
+                     .createInstance(Ci.nsIStringInputStream);
+  dataStream.data = aPostDataString;
+
+  let mimeStream = Cc["@mozilla.org/network/mime-input-stream;1"]
+                     .createInstance(Ci.nsIMIMEInputStream);
   mimeStream.addHeader("Content-Type", aType);
   mimeStream.addContentLength = true;
   mimeStream.setData(dataStream);
   return mimeStream.QueryInterface(Ci.nsIInputStream);
 }
 
 function getLoadContext() {
   return window.QueryInterface(Ci.nsIInterfaceRequestor)
@@ -6392,30 +6339,16 @@ function AddKeywordForSearchField() {
                                                    , "loadInSidebar" ]
                                      }, window);
   }
   mm.addMessageListener("ContextMenu:SearchFieldBookmarkData:Result", onMessage);
 
   mm.sendAsyncMessage("ContextMenu:SearchFieldBookmarkData", {}, { target: gContextMenu.target });
 }
 
-function convertFromUnicode(charset, str)
-{
-  try {
-    var unicodeConverter = Components
-       .classes["@mozilla.org/intl/scriptableunicodeconverter"]
-       .createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
-    unicodeConverter.charset = charset;
-    str = unicodeConverter.ConvertFromUnicode(str);
-    return str + unicodeConverter.Finish();
-  } catch (ex) {
-    return null;
-  }
-}
-
 /**
  * Re-open a closed tab.
  * @param aIndex
  *        The index of the tab (via SessionStore.getClosedTabData)
  * @returns a reference to the reopened tab.
  */
 function undoCloseTab(aIndex) {
   // wallpaper patch to prevent an unnecessary blank tab (bug 343895)
--- a/browser/base/content/test/general/browser_addKeywordSearch.js
+++ b/browser/base/content/test/general/browser_addKeywordSearch.js
@@ -1,82 +1,81 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
 var testData = [
-  /* baseURI, field name, expected */
-  [ 'http://example.com/', 'q', 'http://example.com/?q=%s' ],
-  [ 'http://example.com/new-path-here/', 'q', 'http://example.com/new-path-here/?q=%s' ],
-  [ '', 'q', 'http://example.org/browser/browser/base/content/test/general/dummy_page.html?q=%s' ],
-  // Tests for proper behaviour when called on a form whose action contains a question mark.
-  [ 'http://example.com/search?oe=utf-8', 'q', 'http://example.com/search?oe=utf-8&q=%s' ],
+  { desc: "No path",
+    action: "http://example.com/",
+    param: "q",
+  },
+  { desc: "With path",
+    action: "http://example.com/new-path-here/",
+    param: "q",
+  },
+  { desc: "No action",
+    action: "",
+    param: "q",
+  },
+  { desc: "With Query String",
+    action: "http://example.com/search?oe=utf-8",
+    param: "q",
+  },
 ];
 
 add_task(function*() {
-  yield BrowserTestUtils.withNewTab({
-    gBrowser,
-    url: "http://example.org/browser/browser/base/content/test/general/dummy_page.html",
-  }, function* (browser) {
-    yield ContentTask.spawn(browser, null, function* () {
-      let doc = content.document;
-      let base = doc.createElement("base");
-      doc.head.appendChild(base);
-    });
-
-    var mm = browser.messageManager;
+  const TEST_URL = "http://example.org/browser/browser/base/content/test/general/dummy_page.html";
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
 
-    for (let [baseURI, fieldName, expected] of testData) {
-      let popupShownPromise = BrowserTestUtils.waitForEvent(document.getElementById("contentAreaContextMenu"),
-                                                            "popupshown");
+  let count = 0;
+  for (let method of ["GET", "POST"]) {
+    for (let {desc, action, param } of testData) {
+      info(`Running ${method} keyword test '${desc}'`);
+      let id = `keyword-form-${count++}`;
+      let contextMenu = document.getElementById("contentAreaContextMenu");
+      let contextMenuPromise =
+        BrowserTestUtils.waitForEvent(contextMenu, "popupshown")
+                        .then(() => gContextMenuContentData.popupNode);
 
-      yield ContentTask.spawn(browser, { baseURI, fieldName }, function* (args) {
+      yield ContentTask.spawn(tab.linkedBrowser,
+                              { action, param, method, id }, function* (args) {
         let doc = content.document;
-
-        let base = doc.querySelector('head > base');
-        base.href = args.baseURI;
-
         let form = doc.createElement("form");
-        form.id = "keyword-form";
+        form.id = args.id;
+        form.method = args.method;
+        form.action = args.action;
         let element = doc.createElement("input");
         element.setAttribute("type", "text");
-        element.setAttribute("name", args.fieldName);
+        element.setAttribute("name", args.param);
         form.appendChild(element);
         doc.body.appendChild(form);
-
-        /* Open context menu so chrome can access the element */
-        const domWindowUtils =
-          content.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
-                 .getInterface(Components.interfaces.nsIDOMWindowUtils);
-        let rect = element.getBoundingClientRect();
-        let left = rect.left + rect.width / 2;
-        let top = rect.top + rect.height / 2;
-        domWindowUtils.sendMouseEvent("contextmenu", left, top, 2,
-                                      1, 0, false, 0, 0, true);
       });
 
-      yield popupShownPromise;
+      yield BrowserTestUtils.synthesizeMouseAtCenter(`#${id} > input`,
+                                                     { type : "contextmenu", button : 2 },
+                                                     tab.linkedBrowser);
+      let target = yield contextMenuPromise;
 
-      let target = gContextMenuContentData.popupNode;
-
-      let urlCheck = new Promise((resolve, reject) => {
+      yield new Promise(resolve => {
+        let url = action || tab.linkedBrowser.currentURI.spec;
+        let mm = tab.linkedBrowser.messageManager;
         let onMessage = (message) => {
           mm.removeMessageListener("ContextMenu:SearchFieldBookmarkData:Result", onMessage);
-
-          is(message.data.spec, expected,
-             `Bookmark spec for search field named ${fieldName} and baseURI ${baseURI} incorrect`);
+          if (method == "GET") {
+            ok(message.data.spec.endsWith(`${param}=%s`),
+             `Check expected url for field named ${param} and action ${action}`);
+          } else {
+            is(message.data.spec, url,
+             `Check expected url for field named ${param} and action ${action}`);
+            is(message.data.postData, `${param}%3D%25s`,
+             `Check expected POST data for field named ${param} and action ${action}`);
+          }
           resolve();
         };
         mm.addMessageListener("ContextMenu:SearchFieldBookmarkData:Result", onMessage);
 
         mm.sendAsyncMessage("ContextMenu:SearchFieldBookmarkData", null, { target });
       });
 
-      yield urlCheck;
-
-      document.getElementById("contentAreaContextMenu").hidePopup();
+      let popupHiddenPromise = BrowserTestUtils.waitForEvent(contextMenu, "popuphidden");
+      contextMenu.hidePopup();
+      yield popupHiddenPromise;
+    }
+  }
 
-      yield ContentTask.spawn(browser, null, function* () {
-        let doc = content.document;
-        doc.body.removeChild(doc.getElementById("keyword-form"));
-      });
-    }
-  });
+  yield BrowserTestUtils.removeTab(tab);
 });
--- a/browser/base/content/test/urlbar/browser.ini
+++ b/browser/base/content/test/urlbar/browser.ini
@@ -2,16 +2,18 @@
 support-files =
   dummy_page.html
   head.js
 
 [browser_URLBarSetURI.js]
 skip-if = (os == "linux" || os == "mac") && debug # bug 970052, bug 970053
 [browser_action_keyword.js]
 skip-if = os == "linux" # Bug 1188154
+support-files =
+  print_postdata.sjs
 [browser_action_keyword_override.js]
 [browser_action_searchengine.js]
 [browser_action_searchengine_alias.js]
 [browser_autocomplete_a11y_label.js]
 [browser_autocomplete_autoselect.js]
 [browser_autocomplete_cursor.js]
 [browser_autocomplete_edit_completed.js]
 [browser_autocomplete_enter_race.js]
--- a/browser/base/content/test/urlbar/browser_action_keyword.js
+++ b/browser/base/content/test/urlbar/browser_action_keyword.js
@@ -1,81 +1,119 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-var gOnSearchComplete = null;
-
 function* promise_first_result(inputText) {
   yield promiseAutocompleteResultPopup(inputText);
 
   let firstResult = gURLBar.popup.richlistbox.firstChild;
   return firstResult;
 }
 
-
-add_task(function*() {
-  let tab = gBrowser.selectedTab = gBrowser.addTab("about:mozilla");
-  let tabs = [tab];
-  registerCleanupFunction(function* () {
-    for (let tab of tabs)
-      gBrowser.removeTab(tab);
-    yield PlacesUtils.bookmarks.remove(bm);
-  });
+const TEST_URL = "http://mochi.test:8888/browser/browser/base/content/test/urlbar/print_postdata.sjs";
 
-  yield promiseTabLoadEvent(tab);
+add_task(function* setup() {
+  yield PlacesUtils.keywords.insert({ keyword: "get",
+                                      url: TEST_URL + "?q=%s" });
+  yield PlacesUtils.keywords.insert({ keyword: "post",
+                                      url: TEST_URL,
+                                      postData: "q=%s" });
+  registerCleanupFunction(function* () {
+    yield PlacesUtils.keywords.remove("get");
+    yield PlacesUtils.keywords.remove("post");
+    while (gBrowser.tabs.length > 1) {
+      yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
+    }
+  });
+});
 
-  let bm = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
-                                                url: "http://example.com/?q=%s",
-                                                title: "test" });
-  yield PlacesUtils.keywords.insert({ keyword: "keyword",
-                                      url: "http://example.com/?q=%s" });
+add_task(function* get_keyword() {
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
 
-  let result = yield promise_first_result("keyword something");
+  let result = yield promise_first_result("get something");
   isnot(result, null, "Expect a keyword result");
 
   let types = new Set(result.getAttribute("type").split(/\s+/));
   Assert.ok(types.has("keyword"));
   is(result.getAttribute("actiontype"), "keyword", "Expect correct `actiontype` attribute");
-  is(result.getAttribute("title"), "example.com", "Expect correct title");
+  is(result.getAttribute("title"), "mochi.test:8888", "Expect correct title");
 
   // We need to make a real URI out of this to ensure it's normalised for
   // comparison.
   let uri = NetUtil.newURI(result.getAttribute("url"));
-  is(uri.spec, makeActionURI("keyword", {url: "http://example.com/?q=something", input: "keyword something"}).spec, "Expect correct url");
+  is(uri.spec, PlacesUtils.mozActionURI("keyword",
+                                        { url: TEST_URL + "?q=something",
+                                          input: "get something"}),
+     "Expect correct url");
 
   let titleHbox = result._titleText.parentNode.parentNode;
   ok(titleHbox.classList.contains("ac-title"), "Title hbox element sanity check");
   is_element_visible(titleHbox, "Title element should be visible");
-  is(result._titleText.textContent, "example.com: something", "Node should contain the name of the bookmark and query");
+  is(result._titleText.textContent, "mochi.test:8888: something",
+     "Node should contain the name of the bookmark and query");
 
   let urlHbox = result._urlText.parentNode.parentNode;
   ok(urlHbox.classList.contains("ac-url"), "URL hbox element sanity check");
   is_element_hidden(urlHbox, "URL element should be hidden");
 
   let actionHbox = result._actionText.parentNode.parentNode;
   ok(actionHbox.classList.contains("ac-action"), "Action hbox element sanity check");
   is_element_visible(actionHbox, "Action element should be visible");
   is(result._actionText.textContent, "", "Action text should be empty");
 
   // Click on the result
   info("Normal click on result");
-  let tabPromise = promiseTabLoadEvent(tab);
+  let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
   EventUtils.synthesizeMouseAtCenter(result, {});
   yield tabPromise;
-  is(tab.linkedBrowser.currentURI.spec, "http://example.com/?q=something", "Tab should have loaded from clicking on result");
+  is(tab.linkedBrowser.currentURI.spec, TEST_URL + "?q=something",
+     "Tab should have loaded from clicking on result");
 
   // Middle-click on the result
   info("Middle-click on result");
-  result = yield promise_first_result("keyword somethingmore");
+  result = yield promise_first_result("get somethingmore");
   isnot(result, null, "Expect a keyword result");
   // We need to make a real URI out of this to ensure it's normalised for
   // comparison.
   uri = NetUtil.newURI(result.getAttribute("url"));
-  is(uri.spec, makeActionURI("keyword", {url: "http://example.com/?q=somethingmore", input: "keyword somethingmore"}).spec, "Expect correct url");
+  is(uri.spec, PlacesUtils.mozActionURI("keyword",
+                                        { url: TEST_URL + "?q=somethingmore",
+                                          input: "get somethingmore" }),
+     "Expect correct url");
 
   tabPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
   EventUtils.synthesizeMouseAtCenter(result, {button: 1});
   let tabOpenEvent = yield tabPromise;
   let newTab = tabOpenEvent.target;
-  tabs.push(newTab);
-  yield promiseTabLoadEvent(newTab);
-  is(newTab.linkedBrowser.currentURI.spec, "http://example.com/?q=somethingmore", "Tab should have loaded from middle-clicking on result");
+  yield BrowserTestUtils.browserLoaded(newTab.linkedBrowser);
+  is(newTab.linkedBrowser.currentURI.spec,
+     TEST_URL + "?q=somethingmore",
+     "Tab should have loaded from middle-clicking on result");
 });
+
+
+add_task(function* post_keyword() {
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "about:mozilla");
+
+  let result = yield promise_first_result("post something");
+  isnot(result, null, "Expect a keyword result");
+
+  let types = new Set(result.getAttribute("type").split(/\s+/));
+  Assert.ok(types.has("keyword"));
+  is(result.getAttribute("actiontype"), "keyword", "Expect correct `actiontype` attribute");
+  is(result.getAttribute("title"), "mochi.test:8888", "Expect correct title");
+
+  is(result.getAttribute("url"),
+     PlacesUtils.mozActionURI("keyword", { url: TEST_URL,
+                                           input: "post something",
+                                           "postData": "q=something" }),
+     "Expect correct url");
+
+  // Click on the result
+  info("Normal click on result");
+  let tabPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  EventUtils.synthesizeMouseAtCenter(result, {});
+  yield tabPromise;
+  is(tab.linkedBrowser.currentURI.spec, TEST_URL,
+     "Tab should have loaded from clicking on result");
+
+  let postData = yield ContentTask.spawn(tab.linkedBrowser, null, function* () {
+    return content.document.body.textContent;
+  });
+  is(postData, "q=something", "post data was submitted correctly");
+});
--- a/browser/base/content/test/urlbar/browser_urlbarEnter.js
+++ b/browser/base/content/test/urlbar/browser_urlbarEnter.js
@@ -14,32 +14,32 @@ add_task(function* () {
   EventUtils.synthesizeKey("VK_RETURN", {});
   yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
 
   // Check url bar and selected tab.
   is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress");
   is(gBrowser.selectedTab, tab, "New URL was loaded in the current tab");
 
   // Cleanup.
-  gBrowser.removeCurrentTab();
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
 
 add_task(function* () {
   info("Alt+Return keypress");
-  let tab = gBrowser.selectedTab = gBrowser.addTab(START_VALUE);
   // due to bug 691608, we must wait for the load event, else isTabEmpty() will
   // return true on e10s for this tab, so it will be reused even with altKey.
-  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, START_VALUE);
 
+  let tabOpenPromise = BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
   gURLBar.focus();
   EventUtils.synthesizeKey("VK_RETURN", {altKey: true});
 
   // wait for the new tab to appear.
-  yield BrowserTestUtils.waitForEvent(gBrowser.tabContainer, "TabOpen");
+  yield tabOpenPromise;
 
   // Check url bar and selected tab.
   is(gURLBar.textValue, TEST_VALUE, "Urlbar should preserve the value on return keypress");
   isnot(gBrowser.selectedTab, tab, "New URL was loaded in a new tab");
 
   // Cleanup.
-  gBrowser.removeTab(tab);
-  gBrowser.removeCurrentTab();
+  yield BrowserTestUtils.removeTab(tab);
+  yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
 });
copy from browser/base/content/test/general/print_postdata.sjs
copy to browser/base/content/test/urlbar/print_postdata.sjs
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -394,18 +394,16 @@ file, You can obtain one at http://mozil
 
           let url = this.value;
           if (!url) {
             return;
           }
 
           let mayInheritPrincipal = false;
           let postData = null;
-          let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
-          let matchLastLocationChange = true;
 
           let action = this._parseActionUrl(url);
           if (action) {
             switch (action.type) {
               case "visiturl":
                 // Unifiedcomplete uses fixupURI to tell if something is a visit
                 // or a search, and passes out the fixedURI as the url param.
                 // By using that uri we would end up passing a different string
@@ -415,19 +413,26 @@ file, You can obtain one at http://mozil
                 // mozilla, would note the string has a scheme, and try to load
                 // http://mozilla.com/run instead of searching "mozilla/run".
                 // So, if we have the original input at hand, we pass it through
                 // and let the docshell handle it.
                 if (action.params.input) {
                   url = action.params.input;
                   break;
                 }
-                // Fall-through.
+                url = action.params.url;
+                break;
+              case "remotetab":
+                url = action.params.url;
+                break;
               case "keyword":
-              case "remotetab":
+                if (action.params.postData) {
+                  postData = getPostDataStream(action.params.postData);
+                }
+                mayInheritPrincipal = true;
                 url = action.params.url;
                 break;
               case "switchtab":
                 url = action.params.url;
                 if (this.hasAttribute("actiontype")) {
                   this.handleRevert();
                   let prevTab = gBrowser.selectedTab;
                   if (switchToTabHavingURI(url) && isTabEmpty(prevTab)) {
@@ -440,61 +445,66 @@ file, You can obtain one at http://mozil
                 if (selectedOneOff && selectedOneOff.engine) {
                   // Replace the engine with the selected one-off engine.
                   action.params.engineName = selectedOneOff.engine.name;
                 }
                 const actionDetails = {
                   isSuggestion: !!action.params.searchSuggestion,
                   isAlias: !!action.params.alias
                 };
-                [url, postData] = this._recordSearchEngineLoad(
+                [url, postData] = this._parseAndRecordSearchEngineLoad(
                   action.params.engineName,
                   action.params.searchSuggestion || action.params.searchQuery,
                   event,
                   where,
                   openUILinkParams,
                   actionDetails
                 );
                 break;
             }
-            this._loadURL(url, postData, where, openUILinkParams,
-                          matchLastLocationChange, mayInheritPrincipal);
-            return;
-          }
-
-          // If there's a selected one-off button and the input value is a
-          // search query (or "keyword" in URI-fixup terminology), then load a
-          // search using the one-off's engine.
-          if (selectedOneOff && selectedOneOff.engine) {
+          } else if (selectedOneOff && selectedOneOff.engine) {
+            // If there's a selected one-off button and the input value is a
+            // search query (or "keyword" in URI-fixup terminology), then load a
+            // search using the one-off's engine.
             let value = this.oneOffSearchQuery;
             let fixup;
             try {
               fixup = Services.uriFixup.getFixupURIInfo(
                 value,
                 Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP
               );
             } catch (ex) {}
-            if (fixup && fixup.keywordProviderName) {
-              [url, postData] =
-                this._recordSearchEngineLoad(selectedOneOff.engine, value,
-                                             event, where, openUILinkParams);
-              this._loadURL(url, postData, where, openUILinkParams,
-                            matchLastLocationChange, mayInheritPrincipal);
+            if (!fixup || !fixup.keywordProviderName) {
+              return;
+            }
+
+            [url, postData] =
+              this._parseAndRecordSearchEngineLoad(selectedOneOff.engine, value,
+                                                   event, where, openUILinkParams);
+          } else {
+            // This is a fallback for add-ons and old testing code that directly
+            // set value and try to confirm it. UnifiedComplete should always
+            // resolve to a valid url.
+            try {
+              new URL(url);
+            } catch (ex) {
+              let lastLocationChange = gBrowser.selectedBrowser.lastLocationChange;
+              getShortcutOrURIAndPostData(url).then(data => {
+                if (where != "current" ||
+                    gBrowser.selectedBrowser.lastLocationChange == lastLocationChange) {
+                  this._loadURL(data.url, data.postData, where,
+                                openUILinkParams, data.mayInheritPrincipal);
+                }
+              });
               return;
             }
           }
 
-          getShortcutOrURIAndPostData(url).then(({url, postData, mayInheritPrincipal}) => {
-            if (url) {
-              matchLastLocationChange =
-                lastLocationChange == gBrowser.selectedBrowser.lastLocationChange;
-              this._loadURL(url, postData, where, openUILinkParams,
-                            matchLastLocationChange, mayInheritPrincipal);
-            }
-          });
+          this._loadURL(url, postData, where, openUILinkParams,
+                        mayInheritPrincipal);
         ]]></body>
       </method>
 
       <property name="oneOffSearchQuery">
         <getter><![CDATA[
           // this.textValue may be an autofilled string.  Search only with the
           // portion that the user typed, if any, by preferring the autocomplete
           // controller's searchString (including handleEnterInstance.searchString).
@@ -504,34 +514,33 @@ file, You can obtain one at http://mozil
         ]]></getter>
       </property>
 
       <method name="_loadURL">
         <parameter name="url"/>
         <parameter name="postData"/>
         <parameter name="openUILinkWhere"/>
         <parameter name="openUILinkParams"/>
-        <parameter name="matchLastLocationChange"/>
         <parameter name="mayInheritPrincipal"/>
         <body><![CDATA[
           this.value = url;
           gBrowser.userTypedValue = url;
           if (gInitialPages.includes(url)) {
             gBrowser.selectedBrowser.initialPageLoadedFromURLBar = url;
           }
           try {
             addToUrlbarHistory(url);
           } catch (ex) {
             // Things may go wrong when adding url to session history,
             // but don't let that interfere with the loading of the url.
             Cu.reportError(ex);
           }
 
           let params = {
-            postData: postData,
+            postData,
             allowThirdPartyFixup: true,
           };
           if (openUILinkWhere == "current") {
             params.indicateErrorPageLoad = true;
             params.allowPinnedTabHostChange = true;
             params.disallowInheritPrincipal = !mayInheritPrincipal;
             params.allowPopups = url.startsWith("javascript:");
           } else {
@@ -544,20 +553,16 @@ file, You can obtain one at http://mozil
             }
           }
 
           // Focus the content area before triggering loads, since if the load
           // occurs in a new tab, we want focus to be restored to the content
           // area when the current tab is re-selected.
           gBrowser.selectedBrowser.focus();
 
-          if (openUILinkWhere == "current" && !matchLastLocationChange) {
-            return;
-          }
-
           if (openUILinkWhere != "current") {
             this.handleRevert();
           }
 
           try {
             openUILinkIn(url, openUILinkWhere, params);
           } catch (ex) {
             // This load can throw an exception in certain cases, which means
@@ -569,17 +574,17 @@ file, You can obtain one at http://mozil
 
           if (openUILinkWhere == "current") {
             // Ensure the start of the URL is visible for usability reasons.
             this.selectionStart = this.selectionEnd = 0;
           }
         ]]></body>
       </method>
 
-      <method name="_recordSearchEngineLoad">
+      <method name="_parseAndRecordSearchEngineLoad">
         <parameter name="engineOrEngineName"/>
         <parameter name="query"/>
         <parameter name="event"/>
         <parameter name="openUILinkWhere"/>
         <parameter name="openUILinkParams"/>
         <parameter name="searchActionDetails"/>
         <body><![CDATA[
           let engine =
--- a/toolkit/components/places/PlacesUtils.jsm
+++ b/toolkit/components/places/PlacesUtils.jsm
@@ -409,16 +409,21 @@ this.PlacesUtils = {
    *          The action type.
    * @param   params
    *          A JS object of action params.
    * @returns A moz-action URI as a string.
    */
   mozActionURI(type, params) {
     let encodedParams = {};
     for (let key in params) {
+      // Strip null or undefined.
+      // Regardless, don't encode them or they would be converted to a string.
+      if (params[key] === null || params[key] === undefined) {
+        continue;
+      }
       encodedParams[key] = encodeURIComponent(params[key]);
     }
     return "moz-action:" + type + "," + JSON.stringify(encodedParams);
   },
 
   /**
    * Determines whether or not a ResultNode is a Bookmark folder.
    * @param   aNode
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -267,16 +267,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "PromiseUtils",
                                   "resource://gre/modules/PromiseUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Task",
                                   "resource://gre/modules/Task.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesSearchAutocompleteProvider",
                                   "resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "PlacesRemoteTabsAutocompleteProvider",
                                   "resource://gre/modules/PlacesRemoteTabsAutocompleteProvider.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "BrowserUtils",
+                                  "resource://gre/modules/BrowserUtils.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "textURIService",
                                    "@mozilla.org/intl/texttosuburi;1",
                                    "nsITextToSubURI");
 
 /**
  * Storage object for switch-to-tab entries.
  * This takes care of caching and registering open pages, that will be reused
@@ -907,17 +909,17 @@ Search.prototype = {
     // Though, if there's no heuristic result, we start searching immediately,
     // since autocomplete may be waiting for us.
     if (hasHeuristic) {
       yield this._sleep(Prefs.delay);
       if (!this.pending)
         return;
     }
 
-    if (this._enableActions) {
+    if (this._enableActions && this._searchTokens.length > 0) {
       yield this._matchSearchSuggestions();
       if (!this.pending)
         return;
     }
 
     for (let [query, params] of queries) {
       yield conn.executeCached(query, params, this._onResultRow.bind(this));
       if (!this.pending)
@@ -947,28 +949,29 @@ Search.prototype = {
     // Ensure to fill any remaining space.
     yield Promise.all(this._remoteMatchesPromises);
   }),
 
   *_matchFirstHeuristicResult(conn) {
     // We always try to make the first result a special "heuristic" result.  The
     // heuristics below determine what type of result it will be, if any.
 
-    if (this._searchTokens.length > 0) {
-      // This may be a Places keyword.
-      let matched = yield this._matchPlacesKeyword();
+    let hasSearchTerms = this._searchTokens.length > 0 ;
+
+    if (this._enableActions && hasSearchTerms) {
+      // It may be a search engine with an alias - which works like a keyword.
+      let matched = yield this._matchSearchEngineAlias();
       if (matched) {
         return true;
       }
     }
 
-    if (this.pending && this._enableActions) {
-      // If it's not a Places keyword, then it may be a search engine
-      // with an alias - which works like a keyword.
-      let matched = yield this._matchSearchEngineAlias();
+    if (this.pending && hasSearchTerms) {
+      // It may be a Places keyword.
+      let matched = yield this._matchPlacesKeyword();
       if (matched) {
         return true;
       }
     }
 
     let shouldAutofill = this._shouldAutofill;
     if (this.pending && shouldAutofill) {
       // It may also look like a URL we know from the database.
@@ -981,17 +984,17 @@ Search.prototype = {
     if (this.pending && shouldAutofill) {
       // Or it may look like a URL we know about from search engines.
       let matched = yield this._matchSearchEngineUrl();
       if (matched) {
         return true;
       }
     }
 
-    if (this.pending && this._enableActions) {
+    if (this.pending && hasSearchTerms && this._enableActions) {
       // If we don't have a result that matches what we know about, then
       // we use a fallback for things we don't know about.
 
       // We may not have auto-filled, but this may still look like a URL.
       // However, even if the input is a valid URL, we may not want to use
       // it as such. This can happen if the host would require whitelisting,
       // but isn't in the whitelist.
       let matched = yield this._matchUnknownUrl();
@@ -1115,33 +1118,36 @@ Search.prototype = {
 
   _matchPlacesKeyword: function* () {
     // The first word could be a keyword, so that's what we'll search.
     let keyword = this._searchTokens[0];
     let entry = yield PlacesUtils.keywords.fetch(this._searchTokens[0]);
     if (!entry)
       return false;
 
-    // Build the url.
-    let searchString = this._trimmedOriginalSearchString;
-    let queryString = "";
-    let queryIndex = searchString.indexOf(" ");
-    if (queryIndex != -1) {
-      queryString = searchString.substring(queryIndex + 1);
+    let searchString = this._trimmedOriginalSearchString.substr(keyword.length + 1);
+
+    let url = null, postData = null;
+    try {
+      [url, postData] =
+        yield BrowserUtils.parseUrlAndPostData(entry.url.href,
+                                               entry.postData,
+                                               searchString);
+    } catch (ex) {
+      // It's not possible to bind a param to this keyword.
+      return false;
     }
-    // We need to escape the parameters as if they were the query in a URL
-    queryString = encodeURIComponent(queryString).replace(/%20/g, "+");
-    let escapedURL = entry.url.href.replace("%s", queryString);
 
     let style = (this._enableActions ? "action " : "") + "keyword";
     let actionURL = PlacesUtils.mozActionURI("keyword", {
-      url: escapedURL,
+      url,
       input: this._originalSearchString,
+      postData,
     });
-    let value = this._enableActions ? actionURL : escapedURL;
+    let value = this._enableActions ? actionURL : url;
     // The title will end up being "host: queryString"
     let comment = entry.url.host;
 
     this._addMatch({ value, comment, style, frecency: FRECENCY_DEFAULT });
     return true;
   },
 
   _matchSearchEngineUrl: function* () {
@@ -1201,17 +1207,17 @@ Search.prototype = {
       return false;
 
     let alias = this._searchTokens[0];
     let match = yield PlacesSearchAutocompleteProvider.findMatchByAlias(alias);
     if (!match)
       return false;
 
     match.engineAlias = alias;
-    let query = this._searchTokens.slice(1).join(" ");
+    let query = this._trimmedOriginalSearchString.substr(alias.length + 1);
 
     this._addSearchEngineMatch(match, query);
     return true;
   },
 
   _matchCurrentSearchEngine: function* () {
     let match = yield PlacesSearchAutocompleteProvider.getDefaultMatch();
     if (!match)
--- a/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
+++ b/toolkit/components/places/tests/unifiedcomplete/head_autocomplete.js
@@ -270,17 +270,19 @@ var addBookmark = Task.async(function* (
     parentGuid: (yield PlacesUtils.promiseItemGuid(parentId)),
     title: aBookmarkObj.title || "A bookmark",
     url: aBookmarkObj.uri
   });
   let itemId = yield PlacesUtils.promiseItemId(bm.guid);
 
   if (aBookmarkObj.keyword) {
     yield PlacesUtils.keywords.insert({ keyword: aBookmarkObj.keyword,
-                                        url: aBookmarkObj.uri.spec });
+                                        url: aBookmarkObj.uri.spec,
+                                        postData: aBookmarkObj.postData
+                                      });
   }
 
   if (aBookmarkObj.tags) {
     PlacesUtils.tagging.tagURI(aBookmarkObj.uri, aBookmarkObj.tags);
   }
 });
 
 function addOpenPages(aUri, aCount=1) {
--- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search.js
@@ -22,20 +22,26 @@ add_task(function* test_keyword_searc() 
   yield addBookmark({ uri: uri1, title: "Bookmark title", keyword: "key"});
 
   do_print("Plain keyword query");
   yield check_autocomplete({
     search: "key term",
     matches: [ { uri: NetUtil.newURI("http://abc/?search=term"), title: "abc", style: ["keyword", "heuristic"] } ]
   });
 
+  do_print("Plain keyword UC");
+  yield check_autocomplete({
+    search: "key TERM",
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=TERM"), title: "abc", style: ["keyword", "heuristic"] } ]
+  });
+
   do_print("Multi-word keyword query");
   yield check_autocomplete({
     search: "key multi word",
-    matches: [ { uri: NetUtil.newURI("http://abc/?search=multi+word"), title: "abc", style: ["keyword", "heuristic"] } ]
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=multi%20word"), title: "abc", style: ["keyword", "heuristic"] } ]
   });
 
   do_print("Keyword query with +");
   yield check_autocomplete({
     search: "key blocking+",
     matches: [ { uri: NetUtil.newURI("http://abc/?search=blocking%2B"), title: "abc", style: ["keyword", "heuristic"] } ]
   });
 
--- a/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_keyword_search_actions.js
@@ -10,68 +10,140 @@
  *
  * Also test for bug 249468 by making sure multiple keyword bookmarks with the
  * same keyword appear in the list.
  */
 
 add_task(function* test_keyword_search() {
   let uri1 = NetUtil.newURI("http://abc/?search=%s");
   let uri2 = NetUtil.newURI("http://abc/?search=ThisPageIsInHistory");
-  yield PlacesTestUtils.addVisits([
-    { uri: uri1, title: "Generic page title" },
-    { uri: uri2, title: "Generic page title" }
-  ]);
-  yield addBookmark({ uri: uri1, title: "Bookmark title", keyword: "key"});
+  let uri3 = NetUtil.newURI("http://abc/?search=%s&raw=%S");
+  let uri4 = NetUtil.newURI("http://abc/?search=%s&raw=%S&mozcharset=ISO-8859-1");
+  yield PlacesTestUtils.addVisits([{ uri: uri1 },
+                                   { uri: uri2 },
+                                   { uri: uri3 }]);
+  yield addBookmark({ uri: uri1, title: "Keyword", keyword: "key"});
+  yield addBookmark({ uri: uri1, title: "Post", keyword: "post", postData: "post_search=%s"});
+  yield addBookmark({ uri: uri3, title: "Encoded", keyword: "encoded"});
+  yield addBookmark({ uri: uri4, title: "Charset", keyword: "charset"});
+  yield addBookmark({ uri: uri2, title: "Noparam", keyword: "noparam"});
+  yield addBookmark({ uri: uri2, title: "Noparam-Post", keyword: "post_noparam", postData: "noparam=1"});
 
   do_print("Plain keyword query");
   yield check_autocomplete({
     search: "key term",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}), title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=term", input: "key term"}),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+  });
+
+  do_print("Plain keyword UC");
+  yield check_autocomplete({
+    search: "key TERM",
+    matches: [ { uri: NetUtil.newURI("http://abc/?search=TERM"),
+                 title: "abc", style: ["keyword", "heuristic"] } ]
   });
 
   do_print("Multi-word keyword query");
   yield check_autocomplete({
     search: "key multi word",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi+word", input: "key multi word"}), title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=multi%20word", input: "key multi word"}),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
   });
 
   do_print("Keyword query with +");
   yield check_autocomplete({
     search: "key blocking+",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}), title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=blocking%2B", input: "key blocking+"}),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
   });
 
   do_print("Unescaped term in query");
   // ... but note that UnifiedComplete calls encodeURIComponent() on the query
   // string when it builds the URL, so the expected result will have the
   // ユニコード substring encoded in the URL.
   yield check_autocomplete({
     search: "key ユニコード",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=" + encodeURIComponent("ユニコード"), input: "key ユニコード"}), title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=" + encodeURIComponent("ユニコード"), input: "key ユニコード"}),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
   });
 
   do_print("Keyword that happens to match a page");
   yield check_autocomplete({
     search: "key ThisPageIsInHistory",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}), title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=ThisPageIsInHistory", input: "key ThisPageIsInHistory"}),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
   });
 
   do_print("Keyword without query (without space)");
   yield check_autocomplete({
     search: "key",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}), title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key"}),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
   });
 
   do_print("Keyword without query (with space)");
   yield check_autocomplete({
     search: "key ",
     searchParam: "enable-actions",
-    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key "}), title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=", input: "key "}),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+  });
+
+  do_print("POST Keyword");
+  yield check_autocomplete({
+    search: "post foo",
+    searchParam: "enable-actions",
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=foo", input: "post foo", postData: "post_search=foo"}),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+  });
+
+  do_print("Bug 420328: no-param keyword with a param");
+  yield check_autocomplete({
+    search: "noparam foo",
+    searchParam: "enable-actions",
+    matches: [ makeSearchMatch("noparam foo", { heuristic: true }) ]
+  });
+  yield check_autocomplete({
+    search: "post_noparam foo",
+    searchParam: "enable-actions",
+    matches: [ makeSearchMatch("post_noparam foo", { heuristic: true }) ]
+  });
+
+  do_print("escaping with default UTF-8 charset");
+  yield check_autocomplete({
+    search: "encoded foé",
+    searchParam: "enable-actions",
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=fo%C3%A9&raw=foé", input: "encoded foé" }),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+  });
+
+  do_print("escaping with forced ISO-8859-1 charset");
+  yield check_autocomplete({
+    search: "charset foé",
+    searchParam: "enable-actions",
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=fo%E9&raw=foé", input: "charset foé" }),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+  });
+
+  do_print("Bug 359809: escaping +, / and @ with default UTF-8 charset");
+  yield check_autocomplete({
+    search: "encoded +/@",
+    searchParam: "enable-actions",
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=%2B%2F%40&raw=+/@", input: "encoded +/@" }),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
+  });
+
+  do_print("Bug 359809: escaping +, / and @ with forced ISO-8859-1 charset");
+  yield check_autocomplete({
+    search: "charset +/@",
+    searchParam: "enable-actions",
+    matches: [ { uri: makeActionURI("keyword", {url: "http://abc/?search=%2B%2F%40&raw=+/@", input: "charset +/@" }),
+                 title: "abc", style: [ "action", "keyword", "heuristic" ] } ]
   });
 
   yield cleanup();
 });
--- a/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_alias.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_searchEngine_alias.js
@@ -1,37 +1,47 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-
 add_task(function*() {
   // Note that head_autocomplete.js has already added a MozSearch engine.
   // Here we add another engine with a search alias.
-  Services.search.addEngineWithDetails("AliasedMozSearch", "", "doit", "",
+  Services.search.addEngineWithDetails("AliasedGETMozSearch", "", "get", "",
                                        "GET", "http://s.example.com/search");
-
+  Services.search.addEngineWithDetails("AliasedPOSTMozSearch", "", "post", "",
+                                       "POST", "http://s.example.com/search");
 
-  yield check_autocomplete({
-    search: "doit",
-    searchParam: "enable-actions",
-    matches: [ makeSearchMatch("doit", { engineName: "AliasedMozSearch", searchQuery: "", alias: "doit", heuristic: true }) ]
-  });
+  for (let alias of ["get", "post"]) {
+    yield check_autocomplete({
+      search: alias,
+      searchParam: "enable-actions",
+      matches: [ makeSearchMatch(alias, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+                                          searchQuery: "", alias, heuristic: true }) ]
+    });
+
+    yield check_autocomplete({
+      search: `${alias} `,
+      searchParam: "enable-actions",
+      matches: [ makeSearchMatch(`${alias} `, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+                                                searchQuery: "", alias, heuristic: true }) ]
+    });
 
-  yield check_autocomplete({
-    search: "doit ",
-    searchParam: "enable-actions",
-    matches: [ makeSearchMatch("doit ", { engineName: "AliasedMozSearch", searchQuery: "", alias: "doit", heuristic: true }) ]
-  });
+    yield check_autocomplete({
+      search: `${alias} mozilla`,
+      searchParam: "enable-actions",
+          matches: [ makeSearchMatch(`${alias} mozilla`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+                                                           searchQuery: "mozilla", alias, heuristic: true }) ]
+    });
 
-  yield check_autocomplete({
-    search: "doit mozilla",
-    searchParam: "enable-actions",
-        matches: [ makeSearchMatch("doit mozilla", { engineName: "AliasedMozSearch", searchQuery: "mozilla", alias: "doit", heuristic: true }) ]
-  });
+    yield check_autocomplete({
+      search: `${alias} MoZiLlA`,
+      searchParam: "enable-actions",
+          matches: [ makeSearchMatch(`${alias} MoZiLlA`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+                                                           searchQuery: "MoZiLlA", alias, heuristic: true }) ]
+    });
 
-  yield check_autocomplete({
-    search: "doit mozzarella mozilla",
-    searchParam: "enable-actions",
-        matches: [ makeSearchMatch("doit mozzarella mozilla", { engineName: "AliasedMozSearch", searchQuery: "mozzarella mozilla", alias: "doit", heuristic: true }) ]
-  });
+    yield check_autocomplete({
+      search: `${alias} mozzarella mozilla`,
+      searchParam: "enable-actions",
+          matches: [ makeSearchMatch(`${alias} mozzarella mozilla`, { engineName: `Aliased${alias.toUpperCase()}MozSearch`,
+                                                                      searchQuery: "mozzarella mozilla", alias, heuristic: true }) ]
+    });
+  }
 
   yield cleanup();
 });
--- a/toolkit/modules/BrowserUtils.jsm
+++ b/toolkit/modules/BrowserUtils.jsm
@@ -4,17 +4,22 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [ "BrowserUtils" ];
 
 const {interfaces: Ci, utils: Cu, classes: Cc} = Components;
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+  "resource://gre/modules/PlacesUtils.jsm");
+
 Cu.importGlobalProperties(['URL']);
 
 this.BrowserUtils = {
 
   /**
    * Prints arguments separated by a space and appends a new line.
    */
   dumpLn: function (...args) {
@@ -471,9 +476,75 @@ this.BrowserUtils = {
       let contentViewer = docShell.contentViewer;
       if (contentViewer && !contentViewer.permitUnload()) {
         return false;
       }
     }
 
     return true;
   },
+
+  /**
+   * Replaces %s or %S in the provided url or postData with the given parameter,
+   * acccording to the best charset for the given url.
+   *
+   * @return [url, postData]
+   * @throws if nor url nor postData accept a param, but a param was provided.
+   */
+  parseUrlAndPostData: Task.async(function* (url, postData, param) {
+    let hasGETParam = /%s/i.test(url)
+    let decodedPostData = postData ? unescape(postData) : "";
+    let hasPOSTParam = /%s/i.test(decodedPostData);
+
+    if (!hasGETParam && !hasPOSTParam) {
+      if (param) {
+        // If nor the url, nor postData contain parameters, but a parameter was
+        // provided, return the original input.
+        throw new Error("A param was provided but there's nothing to bind it to");
+      }
+      return [url, postData];
+    }
+
+    let charset = "";
+    const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/;
+    let matches = url.match(re);
+    if (matches) {
+      [, url, charset] = matches;
+    } else {
+      // Try to fetch a charset from History.
+      try {
+        // Will return an empty string if character-set is not found.
+        charset = yield PlacesUtils.getCharsetForURI(this.makeURI(url));
+      } catch (ex) {
+        // makeURI() throws if url is invalid.
+        Cu.reportError(ex);
+      }
+    }
+
+    // encodeURIComponent produces UTF-8, and cannot be used for other charsets.
+    // escape() works in those cases, but it doesn't uri-encode +, @, and /.
+    // Therefore we need to manually replace these ASCII characters by their
+    // encodeURIComponent result, to match the behavior of nsEscape() with
+    // url_XPAlphas.
+    let encodedParam = "";
+    if (charset && charset != "UTF-8") {
+      try {
+        let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                          .createInstance(Ci.nsIScriptableUnicodeConverter);
+        converter.charset = charset;
+        encodedParam = converter.ConvertFromUnicode(param) + converter.Finish();
+      } catch (ex) {
+        encodedParam = param;
+      }
+      encodedParam = escape(encodedParam).replace(/[+@\/]+/g, encodeURIComponent);
+    } else {
+      // Default charset is UTF-8
+      encodedParam = encodeURIComponent(param);
+    }
+
+    url = url.replace(/%s/g, encodedParam).replace(/%S/g, param);
+    if (hasPOSTParam) {
+      postData = decodedPostData.replace(/%s/g, encodedParam)
+                                .replace(/%S/g, param);
+    }
+    return [url, postData];
+  }),
 };