--- 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];
+ }),
};