Bug 1310737 - Urlbar doesn't properly handle %S and POST keywords. r=adw draft
authorMarco Bonardo <mbonardo@mozilla.com>
Wed, 19 Oct 2016 15:09:01 +0200
changeset 431700 26788f336ba279d004dbae71423b435515ad7ae9
parent 431640 e3279760cd977aac30bd9e8032d3ee71f55d2a67
child 535443 48df232ed7c0f41a7edd32720d96070b3b88dcbd
push id34084
push usermak77@bonardo.net
push dateMon, 31 Oct 2016 08:49:04 +0000
reviewersadw
bugs1310737
milestone52.0a1
Bug 1310737 - Urlbar doesn't properly handle %S and POST keywords. r=adw MozReview-Commit-ID: 3IEqahvOwVH
browser/base/content/browser.js
browser/base/content/test/urlbar/browser_urlbarEnter.js
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_search_engine_alias.js
toolkit/modules/BrowserUtils.jsm
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -2059,151 +2059,97 @@ function loadURI(uri, referrer, postData
                  userContextId: userContextId,
                  originPrincipal,
                  forceAboutBlankViewerInCurrent,
                });
   } 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)
@@ -3470,61 +3416,47 @@ function openHomeDialog(aURL)
                                    Components.interfaces.nsISupportsString, homepageStr);
     } catch (ex) {
       dump("Failed to set the home page.\n"+ex+"\n");
     }
   }
 }
 
 var newTabButtonObserver = {
-  onDragOver: function (aEvent)
-  {
+  onDragOver(aEvent) {
     browserDragAndDrop.dragOver(aEvent);
   },
-
-  onDragExit: function (aEvent)
-  {
-  },
-
-  onDrop: function (aEvent)
-  {
+  onDragExit(aEvent) {},
+  onDrop: Task.async(function* (aEvent) {
     let links = browserDragAndDrop.dropLinks(aEvent);
-    Task.spawn(function*() {
-      for (let link of links) {
+    for (let link of links) {
+      if (link.url) {
         let data = yield getShortcutOrURIAndPostData(link.url);
-        if (data.url) {
-          // allow third-party services to fixup this URL
-          openNewTabWith(data.url, null, data.postData, aEvent, true);
-        }
+        // Allow third-party services to fixup this URL.
+        openNewTabWith(data.url, null, data.postData, aEvent, true);
       }
-    });
-  }
+    }
+  })
 }
 
 var newWindowButtonObserver = {
-  onDragOver: function (aEvent)
-  {
+  onDragOver(aEvent) {
     browserDragAndDrop.dragOver(aEvent);
   },
-  onDragExit: function (aEvent)
-  {
-  },
-  onDrop: function (aEvent)
-  {
+  onDragExit(aEvent) {},
+  onDrop: Task.async(function* (aEvent) {
     let links = browserDragAndDrop.dropLinks(aEvent);
-    Task.spawn(function*() {
-      for (let link of links) {
+    for (let link of links) {
+      if (link.url) {
         let data = yield getShortcutOrURIAndPostData(link.url);
-        if (data.url) {
-          // allow third-party services to fixup this URL
-          openNewWindowWith(data.url, null, data.postData, true);
-        }
+        // Allow third-party services to fixup this URL.
+        openNewWindowWith(data.url, null, data.postData, true);
       }
-    });
-  }
+    }
+  })
 }
 
 const DOMLinkHandler = {
   init: function() {
     let mm = window.messageManager;
     mm.addMessageListener("Link:AddFeed", this);
     mm.addMessageListener("Link:SetIcon", this);
     mm.addMessageListener("Link:AddSearch", this);
@@ -6475,30 +6407,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/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);
 });
--- a/browser/base/content/urlbarBindings.xml
+++ b/browser/base/content/urlbarBindings.xml
@@ -406,18 +406,16 @@ file, You can obtain one at http://mozil
           let url = this.value;
           if (!url) {
             return;
           }
 
           let mayInheritPrincipal = false;
           let postData = null;
           let browser = gBrowser.selectedBrowser;
-          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
@@ -427,19 +425,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(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)) {
@@ -452,61 +457,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, browser, 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, browser, 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 = browser.lastLocationChange;
+              getShortcutOrURIAndPostData(url).then(data => {
+                if (where != "current" ||
+                    browser.lastLocationChange == lastLocationChange) {
+                  this._loadURL(data.url, browser, data.postData, where,
+                                openUILinkParams, data.mayInheritPrincipal);
+                }
+              });
               return;
             }
           }
 
-          getShortcutOrURIAndPostData(url).then(({url, postData, mayInheritPrincipal}) => {
-            if (url) {
-              matchLastLocationChange =
-                lastLocationChange == browser.lastLocationChange;
-              this._loadURL(url, browser, postData, where, openUILinkParams,
-                            matchLastLocationChange, mayInheritPrincipal);
-            }
-          });
+          this._loadURL(url, browser, 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).
@@ -517,34 +527,33 @@ file, You can obtain one at http://mozil
       </property>
 
       <method name="_loadURL">
         <parameter name="url"/>
         <parameter name="browser"/>
         <parameter name="postData"/>
         <parameter name="openUILinkWhere"/>
         <parameter name="openUILinkParams"/>
-        <parameter name="matchLastLocationChange"/>
         <parameter name="mayInheritPrincipal"/>
         <body><![CDATA[
           this.value = url;
           browser.userTypedValue = url;
           if (gInitialPages.includes(url)) {
             browser.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,
             currentBrowser: browser,
           };
           if (openUILinkWhere == "current") {
             params.indicateErrorPageLoad = true;
             params.allowPinnedTabHostChange = true;
             params.disallowInheritPrincipal = !mayInheritPrincipal;
             params.allowPopups = url.startsWith("javascript:");
@@ -558,20 +567,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.
           browser.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
@@ -583,17 +588,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
@@ -417,16 +417,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
@@ -908,17 +910,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)
@@ -948,28 +950,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.
@@ -982,17 +985,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();
@@ -1128,33 +1131,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* () {
@@ -1214,17 +1220,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_search_engine_alias.js
+++ b/toolkit/components/places/tests/unifiedcomplete/test_search_engine_alias.js
@@ -1,37 +1,51 @@
 /* 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) {
@@ -507,9 +512,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];
+  }),
 };