/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ 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; }