Source code
Revision control
Copy as Markdown
Other Tools
/* Any copyright is dedicated to the Public Domain.
let { setTimeout } = ChromeUtils.importESModule(
"resource://gre/modules/Timer.sys.mjs"
);
let { NetUtil } = ChromeUtils.importESModule(
"resource://gre/modules/NetUtil.sys.mjs"
);
/**
* Provide search suggestions in the OpenSearch JSON format.
*/
function handleRequest(request, response) {
// Get the query parameters from the query string.
let query = parseQueryString(request.queryString);
function convertToUtf8(str) {
return String.fromCharCode(...new TextEncoder().encode(str));
}
function writeSuggestions(q, completions = []) {
let jsonString = JSON.stringify([q, completions]);
// This script must be evaluated as UTF-8 for this to write out the bytes of
// the string in UTF-8. If it's evaluated as Latin-1, the written bytes
// will be the result of UTF-8-encoding the result-string *twice*, which
// will break the "I ❤️" case further down.
let stringOfUtf8Bytes = convertToUtf8(jsonString);
response.write(stringOfUtf8Bytes);
}
/**
* Sends `data` as suggestions directly. This is useful when testing rich
* suggestions, which do not conform to the object shape sent by
* writeSuggestions.
*
* @param {Array} data The data to send as suggestions.
*/
function writeSuggestionsDirectly(data) {
let jsonString = JSON.stringify(data);
let stringOfUtf8Bytes = convertToUtf8(jsonString);
response.setHeader("Content-Type", "application/json", false);
response.write(stringOfUtf8Bytes);
}
response.setStatusLine(request.httpVersion, 200, "OK");
let q = request.method == "GET" ? query.q : undefined;
if (q == "cookie") {
response.setHeader("Set-Cookie", "cookie=1");
writeSuggestions(q);
} else if (q == "no remote" || q == "no results") {
writeSuggestions(q);
} else if (q == "Query Mismatch") {
writeSuggestions("This is an incorrect query string", ["some result"]);
} else if (q == "Query Case Mismatch") {
writeSuggestions(q.toUpperCase(), [q]);
} else if (q == "") {
writeSuggestions("", ["The server should never be sent an empty query"]);
} else if (q?.startsWith("mo")) {
writeSuggestions(q, ["Mozilla", "modern", "mom"]);
} else if (q?.startsWith("I ❤️")) {
writeSuggestions(q, ["I ❤️ Mozilla"]);
} else if (q?.startsWith("stü")) {
writeSuggestions("st\\u00FC", ["stühle", "stüssy"]);
} else if (q?.startsWith("tailjunk ")) {
writeSuggestionsDirectly([
q,
[q + " normal", q + " tail 1", q + " tail 2"],
[],
{
"google:irrelevantparameter": [],
"google:badformat": {
"google:suggestdetail": [
{},
{ mp: "… ", t: "tail 1" },
{ mp: "… ", t: "tail 2" },
],
},
},
]);
} else if (q?.startsWith("tailjunk few ")) {
writeSuggestionsDirectly([
q,
[q + " normal", q + " tail 1", q + " tail 2"],
[],
{
"google:irrelevantparameter": [],
"google:badformat": {
"google:suggestdetail": [{ mp: "… ", t: "tail 1" }],
},
},
]);
} else if (q?.startsWith("tailalt ")) {
writeSuggestionsDirectly([
q,
[q + " normal", q + " tail 1", q + " tail 2"],
{
"google:suggestdetail": [
{},
{ mp: "… ", t: "tail 1" },
{ mp: "… ", t: "tail 2" },
],
},
]);
} else if (q?.startsWith("tail ")) {
writeSuggestionsDirectly([
q,
[q + " normal", q + " tail 1", q + " tail 2"],
[],
{
"google:irrelevantparameter": [],
"google:suggestdetail": [
{},
{ mp: "… ", t: "tail 1" },
{ mp: "… ", t: "tail 2" },
],
},
]);
} else if (q?.startsWith("richempty ")) {
writeSuggestionsDirectly([
q,
[q + " normal", q + " tail 1", q + " tail 2"],
[],
{
"google:irrelevantparameter": [],
"google:suggestdetail": [],
},
]);
} else if (q?.startsWith("letter ")) {
let letters = [];
for (
let charCode = "A".charCodeAt();
charCode <= "Z".charCodeAt();
charCode++
) {
letters.push("letter " + String.fromCharCode(charCode));
}
writeSuggestions(q, letters);
} else if (q?.startsWith("HTTP ")) {
response.setStatusLine(request.httpVersion, q.replace("HTTP ", ""), q);
writeSuggestions(q, [q]);
} else if (q == "invalidJSON") {
response.setHeader("Content-Type", "application/json", false);
response.write('["invalid"]');
} else if (q == "invalidContentType") {
response.setHeader("Content-Type", "text/xml", false);
writeSuggestions(q, ["invalidContentType response"]);
} else if (q?.startsWith("delay")) {
// Delay the response by delayMs milliseconds. 200ms is the default, less
// than the timeout but hopefully enough to abort before completion.
let match = /^delay([0-9]+)/.exec(q);
let delayMs = match ? parseInt(match[1]) : 200;
response.processAsync();
writeSuggestions(q, [q]);
setTimeout(() => response.finish(), delayMs);
} else if (q?.startsWith("slow ")) {
// Delay the response by 10 seconds so the client timeout is reached.
response.processAsync();
writeSuggestions(q, [q]);
setTimeout(() => response.finish(), 10000);
} else if (request.method == "POST") {
// This includes headers, not just the body
let requestText = NetUtil.readInputStreamToString(
request.bodyInputStream,
request.bodyInputStream.available()
);
// Only use the last line which contains the encoded params
let requestLines = requestText.split("\n");
let postParams = parseQueryString(requestLines[requestLines.length - 1]);
writeSuggestions(postParams.q, ["Mozilla", "modern", "mom"]);
} else {
response.setStatusLine(request.httpVersion, 404, "Not Found");
}
}
function parseQueryString(queryString) {
let query = {};
queryString.split("&").forEach(function (val) {
let [name, value] = val.split("=");
query[name] = decodeURIComponent(value).replace(/[+]/g, " ");
});
return query;
}